Refining UX
This commit is contained in:
parent
9795e91740
commit
537226040d
212
src/index.md
212
src/index.md
@ -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")
|
||||||
|
{
|
||||||
|
data = d.filter(function (game) {
|
||||||
|
const whitelist = teamValue;
|
||||||
return whitelist.indexOf(game.fav) > -1 || whitelist.indexOf(game.und) > -1;
|
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 {
|
} else {
|
||||||
leftColor = `hsl(167,100%,${aboveLightness(d.pScore.sF - d.sF)}%)`;
|
blockColor = "#555";
|
||||||
}
|
|
||||||
|
|
||||||
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 `
|
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>
|
Loading…
x
Reference in New Issue
Block a user