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 `
+