Refining UX

This commit is contained in:
Gabi 2025-02-04 16:44:46 -05:00
parent 9795e91740
commit 537226040d

@ -17,8 +17,9 @@ const coerceGameData = (d, i) => ({
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 teamNames = FileAttachment("./data/teams.json").json(); const teamNames = FileAttachment("./data/teams.json").json().then((d) => d.sort((a,b)=>a.label.localeCompare(b.label)))
``` ```
```js ```js
function parseWeek(w) function parseWeek(w)
{ {
@ -32,35 +33,55 @@ function parseWeek(w)
``` ```
```js ```js
const teamSelect = view(Inputs.select(teamNames, { teamNames.unshift({label:"All",alts:["All"]});
const teamSelect = Inputs.select(teamNames, {
format: (d) => d.label, format: (d) => d.label,
valueof: (d) => d.alts, valueof: (d) => d.alts,
value: teamNames[0].alts,
label: "Favorite" label: "Favorite"
})) })
const teamValue = Generators.input(teamSelect);
``` ```
```js ```js
const highlight = view(Inputs.radio([{"label":"Fav vs. Und", "value":0}, {"label":"Predicted Score","value":1}], {value: 0, format: (x) => x.label})); const highlight = view(
Inputs.radio(
new Map([
["Favorite vs. Underdog", 0],
["Predicted Score",1]
]),
{
value: 0,
}
)
);
``` ```
```js ```js
function oddsPlot(d, {width} = {}) { function oddsPlot(d, {width} = {}) {
const data = d.filter(function (game) { let data = {};
const whitelist = teamSelect; if(teamValue[0] != "All")
return whitelist.indexOf(game.fav) > -1 || whitelist.indexOf(game.und) > -1; {
}); data = d.filter(function (game) {
const whitelist = teamValue;
return whitelist.indexOf(game.fav) > -1 || whitelist.indexOf(game.und) > -1;
});
}else{
data = d;
}
const marginTop = 20; const marginTop = 20;
const marginRight = 20; const marginRight = 20;
const marginBottom = 20; const marginBottom = 20;
const marginLeft = 30; const marginLeft = 30;
const yearGroups = d3.group(data, (d) => d.year); const yearGroups = d3.group(data, (d) => d.year);
const yearN = Array.from(yearGroups).length; const yearN = d3.max(data, (d) => d.year)-1952;
const gameCount = longestString(yearGroups)[1].length; const gameCount = longestString(yearGroups)[1].length;
const padding = 1; const padding = 1;
const blockWidth = const blockWidth =
(width - marginLeft - marginRight) / Array.from(yearGroups).length - (width - marginLeft - marginRight) / yearN -
padding; padding;
const blockHeight = d3.max([4, 500/gameCount]); const blockHeight = blockWidth;
const height = const height =
gameCount * (blockHeight + padding) + gameCount * (blockHeight + padding) +
marginBottom + marginBottom +
@ -69,7 +90,7 @@ function oddsPlot(d, {width} = {}) {
const x = d3 const x = d3
.scaleLinear() .scaleLinear()
.domain(d3.extent(data, (d) => d.year)) .domain([1952,d3.max(data, (d) => d.year)])
.range([marginLeft, width - marginRight]); .range([marginLeft, width - marginRight]);
let yearCount = {}; let yearCount = {};
@ -82,19 +103,13 @@ function oddsPlot(d, {width} = {}) {
return marginTop + yearCount[d.year] * (blockHeight + padding); 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; background-color: grey;"); .attr("style", "max-width: 100%; height: auto; background-color: black;");
@ -117,25 +132,59 @@ function oddsPlot(d, {width} = {}) {
.ticks(yearInterval, "^c") .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(data)
.join("g") .join("g")
.attr("class","gameBlock")
.attr( .attr(
"transform", "transform",
(d) => `translate(${x(d.year) - blockWidth / 2},${y(d)})` (d) => `translate(${x(d.year) - blockWidth / 2},${y(d)})`
) )
.html((d) => buildDualSquare(d, blockWidth, blockHeight)); .html((d) => buildDualSquare(d, blockWidth, blockHeight))
.on("mouseover", function(event, d){
infoTextContainer.html("<b>game!</b>")
const destX = x(d.year)-blockWidth/2;
const destY = y(d);
showTooltip(destX, destY);
})
.on("mouseout", function(event, d){
hideToolTip();
});
const infoBox = svg.append("g")
.attr("class", "info-box")
.attr("x", 5)
.attr("y", 5)
.attr("transform", `translate(-1000,-1000)`)
const infoRect = infoBox.append("rect");
const infoTextContainer = infoBox.append("g")
.attr("fill", "white")
.attr("dy", 20)
.attr("width", 120)
function showTooltip(x,y)
{
infoBox.attr("transform", `translate(${x}, ${y})`);
infoBox.transition().delay(100).duration(500).ease(d3.easeBackOut).attr("opacity",1)
}
function hideToolTip()
{
infoBox.transition().duration(500).ease(d3.easeBackOut).attr("opacity",0)
.transition().delay(500).attr("transform", "translate(-1000,-1000)")
}
return svg.node(); return svg.node();
} }
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;
@ -145,79 +194,90 @@ function longestString(map) {
function buildDualSquare(d, width, height) { function buildDualSquare(d, width, height) {
const blockWidth = width; const blockWidth = width;
const blockHeight = height; const blockHeight = height;
const strokeWidth = 2;
let leftColor = "#EEE"; let blockColor = "#EEE";
let rightColor = "#EEE"; let fullStroke = "none";
let winFill = "none";
let beatSpreadFill = "none";
if(highlight.value==0) if(highlight==0)
{ {
if(teamSelect.indexOf(d.fav) > -1) if(d.sF > d.sU)
{ {
leftColor = "green"; winFill = "rgba(255,255,255,0.75)"
} }
if(teamSelect.indexOf(d.und) > -1) if(d.sF - d.sU > d.spread)
{ {
rightColor = "orange"; beatSpreadFill = "rgb(0,0,0,0.75)"
} }
if(Math.abs(d.sF - d.sU) < d.spread && d.sF < d.sU)
{
beatSpreadFill = "rgb(0,0,0,0.75)"
}
if(teamValue[0]=="All")
{
blockColor = "#F25781";
}else{
if(teamValue.indexOf(d.fav) > -1)
{
blockColor = "#F25781";
}
if(teamValue.indexOf(d.und) > -1)
{
blockColor = "#0277D1";
if(d.sU > d.sF)
{
winFill = "rgba(255,255,255,0.75)"
}
}
if(d.week == undefined)
{
fullStroke = "rgba(255,255,255,0.5)";
}
}
}else{ }else{
if (d.pScore.sF) { if (d.pScore.sF) {
const aboveLightness = d3 const combinedScore = d.sF + d.sU;
.scaleLinear() const combinedPredictedScore = d.pScore.sF + d.pScore.sU;
.domain([ const pDiff = Math.abs((combinedScore - combinedPredictedScore)/combinedScore);
0, blockColor = d3.interpolateOrRd(pDiff);
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 { } else {
leftColor = "#EEE"; blockColor = "#555";
rightColor = "#EEE";
} }
} }
return ` return `
<rect x="0" y="0" height="${height}" width="${ <rect x="0" y="0" height="${height}" width="${
width / 2 width
}" fill="${leftColor}" /> }" fill="${blockColor}" />
<rect x="${width / 2}" y="0" height="${height}" width="${ <rect x="${strokeWidth/2}" y="${strokeWidth/2}" height="${height-strokeWidth}" width="${
width / 2 width-strokeWidth
}" fill="${rightColor}"/> }" stroke="${fullStroke}" fill="none" stroke-width="${strokeWidth}"/>
<circle cx="${width/2}" cy="${height/2}" r="${width/4}" fill="${winFill}"/>
<circle cx="${width/2}" cy="${height/2}" r="${width/6}" fill="${beatSpreadFill}"/>
`; `;
} }
``` ```
```js
function buildToolTip()
{
}
```
<style>
.gameBlock
{
cursor: pointer;
}
</style>
<div class="grid grid-cols-2">
<div class="card">${teamSelect}</div>
</div>
<div class="grid grid-cols-1"> <div class="grid grid-cols-1">
<div class="card">${resize((width) => oddsPlot(oddsfile, {width}))}</div> <div class="card">${resize((width) => oddsPlot(oddsfile, {width}))}</div>
</div> </div>