Published
Edited
Dec 4, 2020
1 star
Insert cell
Insert cell
Insert cell
chart = {
const w = gridWidth * cellSize + 2 * svgPadding;
const h = gridHeight * cellSize + 2 * svgPadding;
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, w, h])
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.style("background", "white");

const g = svg
.append("g")
.attr("transform", `translate(${svgPadding}, ${svgPadding})`);

g.append("g").call(legend);
g.append("text")
.attr("x", w - svgPadding)
.attr("y", h - svgPadding)
.attr("dy", "-1em")
.attr("text-anchor", "end")
.text(d3.timeFormat("%x %X")(new Date(president.meta.timestamp)));

const cell = g
.append("g")
.selectAll("g")
.data(parsed)
.join("g")
.attr("transform", ([statecode]) => {
const [i, j] = position.get(statecode);
return `translate(${i * cellSize},${j * cellSize})`;
});

const inner = cell
.append("g")
.attr("class", "cell-inner")
.attr("transform", `translate(${cellPadding}, ${cellPadding})`);

inner
.append("rect")
.attr("width", cellInner)
.attr("height", cellInner)
.attr("fill", "#eee")
.attr("opacity", 0.5);

inner
.append("text")
.attr("pointer-events", "none")
.attr("text-anchor", "start")
.attr("x", 2)
.attr("y", 3)
.attr("dy", "0.71em")
.attr("font-size", cellInner / 4)
.text(([statecode]) => statecode);

inner
.append("path")
.attr("stroke", ([, { ninety }]) => color(ninety))
.attr("stroke-width", 1.75)
.attr("fill", "none")
.attr("d", ([, { ts }]) => line(ts));

inner
.filter(d => d[1].call)
.append("circle")
.attr("r", callCircleRadius)
.attr("cx", ([, { call }]) => x(call.time))
.attr("cy", ([, { call }]) => y(call[yType]))
.attr("fill", ([, { ninety }]) => color(ninety));

return svg.node();
}
Insert cell
Insert cell
import { president, presidentByState } from "1342cadc9354cc67"
Insert cell
parsed = await Promise.all(
Array.from(presidentByState.keys()).map(parseStateData)
)
Insert cell
parseStateData = key => {
const state = presidentByState.get(key);

const ts = state.timeseries
.filter(d => d.votes !== 0)
.map(({ votes, eevp, timestamp }) => ({
votes: votes,
percentage: eevp / 100,
time: new Date(timestamp)
}));

let call;
if (state.winnerCalledTimestamp) {
const beforeCall = ts
.filter(
d =>
!state.winnerCalledTimestamp || +d.time <= state.winnerCalledTimestamp
)
.reverse();
call = beforeCall.length ? beforeCall[0] : ts[0];
}

let ninety = ts.find(d => d.percentage > 0.9);
ninety = ninety ? ninety.time : new Date();

return [key, { ts, call, ninety }];
}
Insert cell
Insert cell
line = d3
.line()
.x(d => x(d.time))
.y(d => y(d[yType]))
Insert cell
callCircleRadius = 2.5
Insert cell
x = d3.scaleTime(d3.extent(parsed.flatMap(([, {ts}]) => ts.map(d => d.time))), [
0,
cellInner
])
Insert cell
y = d3
.scaleLinear(
d3.extent(parsed.flatMap(([, { ts }]) => ts.map(d => d[yType]))),
[cellInner, 0]
)
.nice(yType === "percentage")
Insert cell
color = d3.scaleSequential(
d3
.extent(
parsed.map(([, { ts }]) => {
const ninety = ts.find(d => d.percentage > 0.9);
return ninety ? ninety.time : new Date();
})
)
.reverse(),
t => d3.interpolateRainbow(t / 3 + 0.25)
)
Insert cell
Insert cell
legend = g =>
g
.attr("transform", `translate(${cellPadding},${cellPadding})`)
.call(g =>
g
.append("rect")
.attr("width", cellInner)
.attr("height", cellInner)
.attr("fill", "#eee")
.attr("opacity", 0.5)
)
.call(g =>
g
.append("path")
.attr("stroke", "black")
.attr("fill", "none")
.attr("d", `M 0 ${cellInner + 0.5} H ${cellInner} V 0`)
)
.call(xAxis)
.call(yAxis)
.call(colorLegend)
Insert cell
xAxis = g =>
g
.append("g")
.attr("transform", `translate(0, ${cellInner})`)
.call(
d3
.axisBottom(x)
.ticks(4)
.tickFormat(d => d.getDate())
)
.call(g => g.select(".domain").remove())
.call(g =>
g
.select(".tick:nth-child(3) text")
.append("tspan")
.attr("x", 0)
.attr("dy", "1em")
.text(
x
.domain()[0]
.toLocaleString("default", { month: "long", year: "numeric" })
)
)
Insert cell
yAxis = g =>
g
.append("g")
.attr("transform", `translate(${cellInner}, 0)`)
.call(
d3
.axisRight(y)
.ticks(3)
.tickFormat(yType === "percentage" ? d3.format(".0%") : d3.format(","))
)
.call(g => g.select(".domain").remove())
.call(g =>
g.select(".tick:last-of-type text").text(function(d, i) {
return (
this.textContent +
(yType === "percentage" ? " of votes counted" : " votes counted")
);
})
)
Insert cell
colorLegend = g =>
g
.append("g")
.attr("transform", `translate(${3 * cellSize}, 5)`)
.call(g =>
g
.append("path")
.attr("d", `M -4 4 4 -4`)
.attr("stroke", "black")
.attr("stroke-width", 1.75)
)
.call(g => g.append("circle").attr("r", callCircleRadius))
.call(g =>
g
.append("text")
.attr("dy", "0.31em")
.attr("dx", "1em")
.text("Race called")
)
.selectAll("g.color-legend")
.data([...color.domain(), d3.mean(color.domain())].sort())
.join("g")
.attr("transform", (d, i) => `translate(0, ${(i + 1) * 20})`)
.call(g =>
g
.append("path")
.attr("d", `M -4 4 4 -4`)
.attr("stroke", d => color(d))
.attr("stroke-width", 1.75)
)
.call(g =>
g
.append("text")
.attr("dy", "0.31em")
.attr("dx", "1em")
.text(
d => "90% counted by " + d3.timeFormat("%b %d %H:%M")(new Date(d))
)
)
Insert cell
Insert cell
position = // grid`
// NV,AZ
// GA,PA`

grid`
, , , , , , , , , ,ME
, , , , ,WI, , , ,VT,NH
WA,ID,MT,ND,MN,IL,MI, ,NY,MA,
OR,NV,WY,SD,IA,IN,OH,PA,NJ,CT,RI
CA,UT,CO,NE,MO,KY,WV,VA,MD,DE,
,AZ,NM,KS,AR,TN,NC,SC,DC, ,
, , ,OK,LA,MS,AL,GA, , ,
HI,AK, ,TX, , , , ,FL, , `
Insert cell
function grid() {
const positionById = new Map;
d3.csvParseRows(String.raw.apply(String, arguments).replace(/^\n|\n$/g, ""), (row, j) => {
row.forEach((id, i) => {
if (id = id.trim()) {
positionById.set(id, [i, j]);
}
});
});
return positionById;
}
Insert cell
gridWidth = d3.max(position, ([, [i]]) => i) + 1
Insert cell
gridHeight = d3.max(position, ([, [, j]]) => j) + 1
Insert cell
cellSize = 80
Insert cell
cellPadding = 1
Insert cell
cellInner = cellSize - cellPadding * 2
Insert cell
svgPadding = 5
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
import { radio } from "@jashkenas/inputs"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more