Refining visuals
This commit is contained in:
parent
de57008596
commit
0898b1b415
253
src/index.md
253
src/index.md
@ -2,133 +2,222 @@
|
|||||||
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 formatDate = d3.utcFormat("%b %d, %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));
|
const oddsfile = FileAttachment("./data/odds_data.json").json().then((D) => D.map(coerceGameData));
|
||||||
```
|
```
|
||||||
```js
|
```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
|
```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
|
```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 marginTop = 20;
|
||||||
const marginRight = 20;
|
const marginRight = 20;
|
||||||
const marginBottom = 20;
|
const marginBottom = 20;
|
||||||
const marginLeft = 20;
|
const marginLeft = 30;
|
||||||
const radius = 3;
|
const yearGroups = d3.group(data, (d) => d.year);
|
||||||
const padding = 1.5;
|
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 =
|
const height =
|
||||||
longestString(oddsByYear)[1].length * (radius * 2 + padding) +
|
gameCount * (blockHeight + padding) +
|
||||||
marginBottom +
|
marginBottom +
|
||||||
marginTop +
|
marginTop +
|
||||||
50;
|
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
|
const x = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain(d3.extent(oddsfile, (d) => d.year))
|
.domain(d3.extent(data, (d) => d.year))
|
||||||
.range([marginLeft, width - marginRight]);
|
.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
|
const svg = d3
|
||||||
.create("svg")
|
.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;");
|
.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
|
svg
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("transform", `translate(0,${height - marginBottom})`)
|
.attr("transform", `translate(0,${marginTop + blockHeight})`)
|
||||||
.call(d3.axisBottom(x).tickSizeOuter(0));
|
.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
|
svg
|
||||||
.append("g")
|
.append("g")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.data(
|
.data(data)
|
||||||
dodge(oddsfile, { radius: radius * 2 + padding, x: (d) => x(d.year) })
|
.join("g")
|
||||||
|
.attr(
|
||||||
|
"transform",
|
||||||
|
(d) => `translate(${x(d.year) - blockWidth / 2},${y(d)})`
|
||||||
)
|
)
|
||||||
.join("circle")
|
.html((d) => buildDualSquare(d, blockWidth, blockHeight));
|
||||||
.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})`);
|
|
||||||
|
|
||||||
return svg.node();
|
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) {
|
function longestString(map) {
|
||||||
return Array.from(map).sort(function (a, b) {
|
return Array.from(map).sort(function (a, b) {
|
||||||
return b[1].length - a[1].length;
|
return b[1].length - a[1].length;
|
||||||
})[0];
|
})[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 `
|
||||||
|
<rect x="0" y="0" height="${height}" width="${
|
||||||
|
width / 2
|
||||||
|
}" fill="${leftColor}" />
|
||||||
|
<rect x="${width / 2}" y="0" height="${height}" width="${
|
||||||
|
width / 2
|
||||||
|
}" fill="${rightColor}"/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
<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(oddsfile, {width}))}</div>
|
||||||
</div>
|
</div>
|
Loading…
x
Reference in New Issue
Block a user