Published
Edited
Nov 10, 2020
4 forks
19 stars
Insert cell
Insert cell
chart = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, gridWidth * cellSize, gridHeight * cellSize])
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.style("background", "white");

svg.append("g").call(legend);
svg
.append("text")
.attr("x", gridWidth * cellSize - 2)
.attr("y", gridHeight * cellSize - 2)
.attr("dy", "-1em")
.attr("text-anchor", "end")
.attr("font-size", 5)
.text(d3.timeFormat("%x %X")(new Date(president.meta.timestamp)));

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

cell
.append("text")
.attr("pointer-events", "none")
.attr("y", 3)
.attr("dy", "0.71em")
.text(([statecode]) => statecode);

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

// triangle
inner
.append("path")
.attr("d", `M ${triangle.A} ${triangle.B} ${triangle.C} Z`)
.attr("fill", "#eee")
.attr("stroke", "#eee")
.attr("stroke-width", 0.5);

// Republican (right) win zone
inner
.append("path")
.attr("d", `M ${triangle.AB} ${triangle.B} ${triangle.BC} Z`)
// .attr("stroke", red)
// .attr("stroke-width", 0.5)
.attr("fill", red)
.attr("opacity", ([, { winner }]) => (winner === "dem" ? 0 : 1))
.attr("fill-opacity", ([, { winner }]) =>
winner === "rep" ? 1 : winner === "dem" ? 0.1 : 0.1
);

// Democrat (left) win zone
inner
.append("path")
.attr("d", `M ${triangle.AC} ${triangle.C} ${triangle.BC} Z`)
// .attr("stroke", blue)
// .attr("stroke-width", 0.5)
.attr("fill", blue)
.attr("opacity", ([, { winner }]) => (winner === "rep" ? 0 : 1))
.attr("fill-opacity", ([, { winner }]) =>
winner === "dem" ? 1 : winner === "rep" ? 0.1 : 0.1
);

inner
.append("path")
.attr("stroke", "black")
.attr("stroke-width", 0.5)
.attr("fill", "none")
.attr("d", ([, { pathData }]) => line(pathData));

return svg.node();
}
Insert cell
Insert cell
parsed = Array.from(presidentByState.keys()).map(parseStateData)
Insert cell
parseStateData = key => {
const stateData = presidentByState.get(key);

const rep = stateData.candidates.find(d => d.party_id === "republican");
const dem = stateData.candidates.find(d => d.party_id === "democrat");

const ts = stateData.timeseries.map(d => ({
rep_vote_share:
d.vote_shares[rep.candidate_key] /
(d.vote_shares[rep.candidate_key] + d.vote_shares[dem.candidate_key]) ||
0.5,
counted: d.eevp / 100,
time: new Date(d.timestamp)
}));

// to draw line
const pathData = ts.map(({ rep_vote_share, counted }) => {
const diameter = cellInner * counted;
const radius = diameter / 2;
const mid = cellInner / 2;
return [mid + radius * (rep_vote_share - 0.5), triangleHeight * counted];
});

// determine winner
const latest = ts[ts.length - 1];
const z = shareNeeded(latest.rep_vote_share, latest.counted);
const tooClose = false; // Math.abs(latest.rep_vote_share - 0.5) < 0.02;
const padding = 0; // 0.1
const winner = tooClose
? null
: z <= 0 - padding
? "rep"
: z >= 1 + padding
? "dem"
: null;

return [key, { rep, dem, ts, pathData, winner }];
}
Insert cell
import { president, presidentByState } from "1342cadc9354cc67"
Insert cell
Insert cell
triangle = {
const A = [cellInner / 2, 0];
const B = [cellInner, triangleHeight];
const C = [0, triangleHeight];
const AB = midpoint(A, B);
const AC = midpoint(A, C);
const BC = midpoint(B, C);
return { A, B, C, AB, AC, BC };
}
Insert cell
triangleHeight = (Math.sqrt(3) / 2) * cellInner
Insert cell
red = "#dd2c35"
Insert cell
blue = "#0080c9"
Insert cell
midpoint = ([ax, ay], [bx, by]) => [(ax + bx) / 2, (ay + by) / 2]
Insert cell
import { shareNeeded } from "7dde4d42ff1f3a76"
Insert cell
line = d3.line()
Insert cell
legend = g =>
g
.attr("transform", `translate(1, 10)`)
.call(g =>
g
.append("path")
.attr("d", `M 0 0 H 20 M 4 -4 L 0 0 4 4 M 16 -4 L 20 0 16 4`)
.attr("stroke", "black")
.attr("fill", "none")
)
.call(g =>
g
.append("path")
.attr("d", `M 0 0 H 20 M 4 -4 L 0 0 4 4 M 16 -4 L 20 0 16 4`)
.attr("transform", "translate(10,10) rotate(90)")
.attr("stroke", "black")
.attr("fill", "none")
)
.call(g =>
g
.append("text")
.attr("x", 23)
.attr("y", 3)
.text("Vote margin")
)
.call(g =>
g
.append("text")
.attr("x", 23)
.attr("y", 23)
.text("Share of vote counted")
)
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 = 40
Insert cell
cellPadding = 3
Insert cell
cellInner = cellSize - cellPadding * 2
Insert cell
d3 = require("d3@6")
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