Read one day of New York’s highway speeds and guess what kind of day it was: six rounds, with a twist at the end
The line is the speed of New York City’s highways through one day, from midnight to midnight: the median over 40 road segments where E-ZPass readers time the same cars at both ends. Read its shape, then pick the day. From round 2, the grey dashed line is an ordinary weekday, for comparison.
D =FileAttachment("data/guess-the-day.json").json()mutable game = ({round:0,picked:null,score:0})
// The four choices of a round: the answer and three others, in an order fixed by the round number.options = {const r = D.rounds[game.round], rand =seeded(game.round+11);const others =shuffle(D.choices.filter(c => c !== r.answer), rand).slice(0,3);returnshuffle([r.answer,...others], rand);}
{const r = D.rounds[game.round], done = game.picked!==null, hours = d3.range(96).map(i => i /4);const series = (day, who) => D.speeds[day].map((v, i) => ({h: hours[i], v, who}));const marks = [Plot.ruleY([30])];if (game.round>0) marks.push(Plot.line(series(D.rounds[0].day,"an ordinary weekday"), {x:"h",y:"v",stroke:"gray",strokeDasharray:"5 4",strokeWidth:1.6}));if (done && r.compare!== D.rounds[0].day) marks.push(Plot.line(series(r.compare,"the same weekday in 2019"), {x:"h",y:"v",stroke:"gray",strokeWidth:2})); marks.push(Plot.line(series(r.day,"mystery day"), {x:"h",y:"v",stroke: accent,strokeWidth:3,defined: d => d.v!==null}));return Plot.plot({width: plotW,height: plotW <500?280:380,x: {label:"Time of day (hours)",domain: [0,24],ticks: [0,3,6,9,12,15,18,21,24]},y: {label:"Speed (km/h)",domain: [30,95],grid:true}, marks });}
md`${game.picked!==null?`The mystery day: **${D.rounds[game.round].date}**.`:"The mystery day is the thick line."}${game.round>0?"Dashed grey: an ordinary Tuesday.":""}${game.picked!==null&& D.rounds[game.round].compare!== D.rounds[0].day?"Solid grey: the same Wednesday in 2019.":""}`
{const r = D.rounds[game.round], last = game.round=== D.rounds.length-1, done = game.picked!==null;const right = done && game.picked=== r.answer;const pick = c => { if (!done) mutable game = {...game,picked: c,score: game.score+ (c === r.answer?1:0)}; };const next = () => { mutable game = last ? {round:0,picked:null,score:0} : {...game,round: game.round+1,picked:null}; };const button = c => {const b =html`<button class="gtd-choice ${done && c === r.answer?"right": done && c === game.picked?"wrong":""}">${c}</button>`; b.onclick= () =>pick(c); b.disabled= done;return b; };const nextButton =html`<button class="gtd-next">${last ?"Play again":"Next round →"}</button>`; nextButton.onclick= next;const verdict =!done ?"": right ?"✅ **Right!**":`❌ **Not quite.** It was: ${r.answer[0].toLowerCase() + r.answer.slice(1)}.`;const final = done && last ?md`**Final score: ${game.score} of ${D.rounds.length}.** ${game.score=== D.rounds.length?"Perfect: you read traffic like a traffic engineer.": game.score>=4?"Very good: you can read a day from its speeds.":"Play again: the shapes get easier once you know what to look for."}`:"";returnhtml`<div class="gtd"> <p class="gtd-head"><b>Round ${game.round+1} of ${D.rounds.length}</b> · score ${game.score}</p> <div class="gtd-choices">${options.map(button)}</div>${done ?html`<div class="gtd-reveal">${md`${verdict}${r.why}`}${final}<p>${nextButton}</p></div>`:""} </div>`;}
What it shows
Every kind of day leaves its own fingerprint in traffic speeds: the morning and afternoon rush of a working day, the late, lazy afternoon of a Sunday, the getaway before a holiday. Two rounds are not about who drives at all. In the blizzard the roads were empty and still slow: snow lowers the speed of free flow, the top of the fundamental diagram (Lecture 3). In the lockdown, demand collapsed and the jams went with it.
Data: New York City Department of Transportation, DOT Traffic Speeds, NYC Open Data. Blizzard: NBC New York, “Blizzard of 2026” (19.7 inches in Central Park, 22–23 February 2026).
seeded = s => () => { s = (s +0x6D2B79F5) |0;let t =Math.imul(s ^ (s >>>15),1| s); t = (t +Math.imul(t ^ (t >>>7),61| t)) ^ t;return ((t ^ (t >>>14)) >>>0) /4294967296; }shuffle = (a, rand) => { a = [...a];for (let i = a.length-1; i >0; i--) { const j =Math.floor(rand() * (i +1)); [a[i], a[j]] = [a[j], a[i]]; } return a; }// Colours that follow the light/dark theme switch: plots redraw when the reader toggles the theme.theme = Generators.observe(notify => {const read = () =>notify(document.body.classList.contains("quarto-dark") ?"dark":"light");read();const watcher =newMutationObserver(read); watcher.observe(document.body, {attributes:true,attributeFilter: ["class"]});return () => watcher.disconnect();})accent = theme ==="dark"?"#c9a0ff":"#57068c"// Plot width: Plot's default 640 px, except on a phone, where a plot is drawn to fit its card.plotW = Generators.observe(notify => {let last;const read = () => {const main =document.querySelector("main"), fit = main ? main.clientWidth-50:640;const w =!document.querySelector(".reveal") && fit <640?Math.max(240,Math.floor(fit)) :640;if (w !== last) notify(last = w); };read();window.addEventListener("resize", read);return () =>window.removeEventListener("resize", read);})