Updated coerced data

This commit is contained in:
BooshPC 2025-01-31 11:27:31 -05:00
parent 9050cbc4a4
commit df1c290dc6

@ -1,7 +1,8 @@
```js ```js
const parseTime = d3.timeParse("%b %d, %Y"); const parseTime = d3.timeParse("%b %d, %Y");
const formatYear = d3.utcFormat("%Y"); const formatYear = d3.utcFormat("%Y");
const coerceGameData = (d,i) => ({date: parseTime(d.date), year: parseInt(formatYear(parseTime(d.date))), spread: Number(d.spread)}); 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 oddsfile = FileAttachment("./data/odds_data.json").json().then((D) => D.map(coerceGameData)); const oddsfile = FileAttachment("./data/odds_data.json").json().then((D) => D.map(coerceGameData));
``` ```
```js ```js
@ -14,66 +15,120 @@ display(oddsByYear);
```js ```js
function oddsPlot(data, {width} = {}) { function oddsPlot(data, {width} = {}) {
// Declare the chart dimensions and margins.
const height = 500;
const marginTop = 20; const marginTop = 20;
const marginRight = 30; const marginRight = 20;
const marginBottom = 30; const marginBottom = 20;
const marginLeft = 40; const marginLeft = 20;
const radius = 3;
const padding = 1.5;
const height =
longestString(oddsByYear)[1].length * (radius * 2 + padding) +
marginBottom +
marginTop +
50;
// Declare the x (horizontal position) scale. //d3.max(oddsfile, (d) => (d.spread));
const x = d3.scaleTime([1952,2025], [marginLeft, width-marginRight]); const color = d3
.scaleLinear()
.domain([0, d3.max(oddsfile, (d) => d.spread)])
.range([0, 1]);
// Declare the y (vertical position) scale. const x = d3
const y = d3.scaleLinear([0, 600], [height - marginBottom, marginTop]); .scaleLinear()
.domain(d3.extent(oddsfile, (d) => d.year))
.range([marginLeft, width - marginRight]);
// Declare the line generator. const svg = d3
const line = d3.line() .create("svg")
.x(d => x(d.year))
.y(d => y(d.count()))
// .y(d => y(total(d, "year", d.year)));
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width) .attr("width", width)
.attr("height", height) .attr("height", height)
.attr("viewBox", [0, 0, width, height]) .attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;"); .attr("style", "max-width: 100%; height: auto;");
// Add the x-axis. svg
svg.append("g") .append("g")
.attr("transform", `translate(0,${height - marginBottom})`) .attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0)); .call(d3.axisBottom(x).tickSizeOuter(0));
// Add the y-axis, remove the domain line, add grid lines and a label. svg
svg.append("g") .append("g")
.attr("transform", `translate(${marginLeft},0)`) .selectAll()
.call(d3.axisLeft(y).ticks(height / 40)) .data(
.call(g => g.select(".domain").remove()) dodge(oddsfile, { radius: radius * 2 + padding, x: (d) => x(d.year) })
.call(g => g.selectAll(".tick line").clone() )
.attr("x2", width - marginLeft - marginRight) .join("circle")
.attr("stroke-opacity", 0.1)) .attr("fill", (d) => d3.interpolateRainbow(color(d.data.spread)))
.attr("cx", (d) => d.x)
// Append a path for the line. .attr("cy", (d) => height - marginBottom - radius - padding - d.y)
svg.append("path") .attr("r", radius)
.attr("fill", "none") .append("title")
.attr("stroke", "steelblue") .text((d) => `${formatDate(d.data.date)}\n${d.data.fav}(${d.data.sF}) vs. ${d.data.und}(${d.data.sU})`);
.attr("stroke-width", 1.5)
.attr("d", line(data));
return svg.node(); return svg.node();
} }
``` ```
```js ```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 cant 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) function total(data, label, find)
{ {
return data.reduce(function(count, entry) { return data.reduce(function(count, entry) {
return count + (entry[label] === find ? 1 : 0); return count + (entry[label] === find ? 1 : 0);
}, 0); }, 0);
} }
function longestString(map) {
return Array.from(map).sort(function (a, b) {
return b[1].length - a[1].length;
})[0];
}
``` ```
<div class="grid grid-cols-1"> <div class="grid grid-cols-1">
<div class="card">${resize((width) => oddsPlot(oddsByYear, {width}))}</div> <div class="card">${resize((width) => oddsPlot(oddsByYear, {width}))}</div>
</div> </div>