diff --git a/src/index.md b/src/index.md index da17fbf..94dbf4c 100644 --- a/src/index.md +++ b/src/index.md @@ -2,133 +2,222 @@ const parseTime = d3.timeParse("%b %d, %Y"); const formatYear = d3.utcFormat("%Y"); const formatDate = d3.utcFormat("%b %d, %Y"); -const coerceGameData = (d,i) => ({date: parseTime(d.date), year: parseInt(formatYear(parseTime(d.date))), spread: Number(d.spread), fav: d.fav, und: d.und, sF: d.sF, sU: d.sU}); +const coerceGameData = (d, i) => ({ + date: parseTime(d.date), + year: parseInt(d.week.slice(0, 4)), + spread: Number(d.spread), + fav: d.fav.replace(/\s\(\d\)/, ""), + und: d.und.replace(/\s\(\d\)/, ""), + sF: Number(d.sF), + sU: Number(d.sU), + ou: Number(d.ou), + week: parseWeek(d.week), + pScore: d.pScore +}) const oddsfile = FileAttachment("./data/odds_data.json").json().then((D) => D.map(coerceGameData)); ``` ```js -const oddsByYear = d3.group(oddsfile, d => d.year) +const teamNames = FileAttachment("./data/teams.json").json(); +``` +```js +function parseWeek(w) +{ + if(w.indexOf("Playoffs")>-1) + { + return undefined; + }else{ + return Number(w.replace(/.*\sWeek\s/, "")); + } +} ``` ```js -display(oddsByYear); +const teamSelect = view(Inputs.select(teamNames, { + format: (d) => d.label, + valueof: (d) => d.alts, + label: "Favorite" +})) +``` +```js +const highlight = view(Inputs.radio([{"label":"Fav vs. Und", "value":0}, {"label":"Predicted Score","value":1}], {value: 0, format: (x) => x.label})); ``` ```js -function oddsPlot(data, {width} = {}) { +function oddsPlot(d, {width} = {}) { + const data = d.filter(function (game) { + const whitelist = teamSelect; + return whitelist.indexOf(game.fav) > -1 || whitelist.indexOf(game.und) > -1; + }); + const marginTop = 20; const marginRight = 20; const marginBottom = 20; - const marginLeft = 20; - const radius = 3; - const padding = 1.5; + const marginLeft = 30; + const yearGroups = d3.group(data, (d) => d.year); + const yearN = Array.from(yearGroups).length; + const gameCount = longestString(yearGroups)[1].length; + const padding = 1; + const blockWidth = + (width - marginLeft - marginRight) / Array.from(yearGroups).length - + padding; + const blockHeight = d3.max([4, 500/gameCount]); const height = - longestString(oddsByYear)[1].length * (radius * 2 + padding) + + gameCount * (blockHeight + padding) + marginBottom + marginTop + 50; - //d3.max(oddsfile, (d) => (d.spread)); - const color = d3 - .scaleLinear() - .domain([0, d3.max(oddsfile, (d) => d.spread)]) - .range([0, 1]); - const x = d3 .scaleLinear() - .domain(d3.extent(oddsfile, (d) => d.year)) + .domain(d3.extent(data, (d) => d.year)) .range([marginLeft, width - marginRight]); + let yearCount = {}; + + const y = function (d) { + if (!yearCount[d.year]) { + yearCount[d.year] = 0; + } + yearCount[d.year]++; + return marginTop + yearCount[d.year] * (blockHeight + padding); + }; + + const playoffCount = gameCount - d3.count(longestString(yearGroups)[1], (d) => d.week); + + const yAxis = d3 + .scaleLinear() + .domain(d3.extent(data, (d) => d.week)) + .range([marginTop+blockHeight, (height - marginBottom-blockHeight)-(blockHeight*playoffCount)]); + const svg = d3 .create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) - .attr("style", "max-width: 100%; height: auto;"); + .attr("style", "max-width: 100%; height: auto; background-color: grey;"); + + + + + let yearInterval = yearN / 5; + + if(yearN < 15) + { + yearInterval = yearN; + }else if(yearN < 50){ + yearInterval = yearN / 2; + } svg .append("g") - .attr("transform", `translate(0,${height - marginBottom})`) - .call(d3.axisBottom(x).tickSizeOuter(0)); + .attr("transform", `translate(0,${marginTop + blockHeight})`) + .call( + d3 + .axisTop(x) + .ticks(yearInterval, "^c") + ); + + svg + .append("g") + .attr("transform", `translate(${marginLeft-blockHeight/2},0)`) + .call(d3.axisLeft(yAxis).ticks(gameCount-playoffCount, "^c")) svg .append("g") .selectAll() - .data( - dodge(oddsfile, { radius: radius * 2 + padding, x: (d) => x(d.year) }) + .data(data) + .join("g") + .attr( + "transform", + (d) => `translate(${x(d.year) - blockWidth / 2},${y(d)})` ) - .join("circle") - .attr("fill", (d) => d3.interpolateRainbow(color(d.data.spread))) - .attr("cx", (d) => d.x) - .attr("cy", (d) => height - marginBottom - radius - padding - d.y) - .attr("r", radius) - .append("title") - .text((d) => `${formatDate(d.data.date)}\n${d.data.fav}(${d.data.sF}) vs. ${d.data.und}(${d.data.sU})`); + .html((d) => buildDualSquare(d, blockWidth, blockHeight)); return svg.node(); } -``` - -```js -function dodge(data, { radius = 1, x = (d) => d } = {}) { - const radius2 = radius ** 2; - const circles = data - .map((d, i, data) => ({ x: +x(d, i, data), data: d })) - .sort((a, b) => a.x - b.x); - const epsilon = 1e-3; - let head = null, - tail = null; - - // Returns true if circle ⟨x,y⟩ intersects with any circle in the queue. - function intersects(x, y) { - let a = head; - while (a) { - if (radius2 - epsilon > (a.x - x) ** 2 + (a.y - y) ** 2) { - return true; - } - a = a.next; - } - return false; - } - - // Place each circle sequentially. - for (const b of circles) { - // Remove circles from the queue that can’t intersect the new circle b. - while (head && head.x < b.x - radius2) head = head.next; - // Choose the minimum non-intersecting tangent. - if (intersects(b.x, (b.y = 0))) { - let a = head; - b.y = Infinity; - do { - let y = a.y + Math.sqrt(radius2 - (a.x - b.x) ** 2); - if (y < b.y && !intersects(b.x, y)) b.y = y; - a = a.next; - } while (a); - } - - // Add b to the queue. - b.next = null; - if (head === null) head = tail = b; - else tail = tail.next = b; - } - - return circles; -} - -function total(data, label, find) -{ - return data.reduce(function(count, entry) { - return count + (entry[label] === find ? 1 : 0); - }, 0); -} function longestString(map) { return Array.from(map).sort(function (a, b) { return b[1].length - a[1].length; })[0]; } + +function buildDualSquare(d, width, height) { + const blockWidth = width; + const blockHeight = height; + + let leftColor = "#EEE"; + let rightColor = "#EEE"; + + if(highlight.value==0) + { + if(teamSelect.indexOf(d.fav) > -1) + { + leftColor = "green"; + } + if(teamSelect.indexOf(d.und) > -1) + { + rightColor = "orange"; + } + }else{ + if (d.pScore.sF) { + const aboveLightness = d3 + .scaleLinear() + .domain([ + 0, + d3.max([ + d3.max(oddsfile, (d) => d.pScore.sF - d.sF), + d3.max(oddsfile, (d) => d.pScore.sU - d.sU) + ]) + ]) + .range([50, 90]); + + const belowLightness = d3 + .scaleLinear() + .domain([ + d3.min([ + d3.min(oddsfile, (d) => d.pScore.sF - d.sF), + d3.min(oddsfile, (d) => d.pScore.sU - d.sU) + ]), + 0 + ]) + .range([90, 50]); + + if (d.pScore.sF == d.sF) { + leftColor = "red"; + } else if (d.pScore.sF > d.sF) { + leftColor = `hsl(43,100%,${belowLightness(d.pScore.sF - d.sF)}%)`; + } else { + leftColor = `hsl(167,100%,${aboveLightness(d.pScore.sF - d.sF)}%)`; + } + + if (d.pScore.sU == d.sU) { + rightColor = "red"; + } else if (d.pScore.sU > d.sU) { + rightColor = `hsl(43,100%,${belowLightness(d.pScore.sU - d.sU)}%)`; + } else { + rightColor = `hsl(167,100%,${aboveLightness(d.pScore.sU - d.sU)}%)`; + } + } else { + leftColor = "#EEE"; + rightColor = "#EEE"; + } + } + + + + return ` + + + `; +} ```
-
${resize((width) => oddsPlot(oddsByYear, {width}))}
+
${resize((width) => oddsPlot(oddsfile, {width}))}
\ No newline at end of file