Published
Edited
Nov 11, 2020
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { president, presidentByState } from "1342cadc9354cc67"
Insert cell
parsed = Array.from(presidentByState.keys()).map(parseStateData)
Insert cell
parsed.find(d => d[0] === "GA")[1]
Insert cell
a = presidentByState.get("MA").candidates.find(d => d.party_id === "republican")
Insert cell
b = presidentByState.get("MA").candidates.find(d => d.party_id === "democrat")
Insert cell
parseStateData = key => {
const state = presidentByState.get(key);

const margins = state.timeseries
.filter(({ votes }) => votes)
.map(({ votes, vote_shares, timestamp }) => ({
a: Math.round(vote_shares[a.candidate_key] * votes),
b: Math.round(vote_shares[b.candidate_key] * votes),
timestamp: new Date(timestamp)
}))
.map(({ a, b, timestamp }, i, arr) =>
!i
? { da: a, db: b, timestamp }
: { da: a - arr[i - 1].a, db: b - arr[i - 1].b, timestamp }
)
.map(({ da, db, timestamp }) => ({ margin: da - db, timestamp, key }))
.filter(
({ timestamp }) =>
xUpperBoundOption === "mostRecent" ||
(timestamp >= electionNight[0] && timestamp <= electionNight[1])
);

const marginsArrays = margins.map(({ margin, timestamp }) => [
+timestamp,
margin
]);

const corr =
marginsArrays.length > 2
? stats.sampleCorrelation(...d3.transpose(marginsArrays))
: 0;

const regressionFn = stats.linearRegressionLine(
stats.linearRegression(marginsArrays)
);

const regressionLine = d3
.extent(margins, d => d.timestamp)
.map(d => ({ timestamp: d, margin: regressionFn(+d) }));

return [key, { margins, marginsArrays, corr, regressionLine }];
}
Insert cell
Insert cell
line = d3
.line()
.x(d => x(d.timestamp))
.y(d => y(d.margin))
Insert cell
callCircleRadius = 2.5
Insert cell
electionNight = [
new Date("2020-11-03T23:00:00Z"),
new Date("2020-11-04T08:00:00Z")
]
Insert cell
x = d3.scaleTime(
xUpperBoundOption === "electionNight"
? electionNight
: d3.extent(
parsed.flatMap(([, { margins }]) => margins.map(d => d.timestamp))
),
[0, cellInner]
)
Insert cell
y = d3.scaleLinear(yDomain, [cellInner, 0]).clamp(true)
Insert cell
yDomain = {
const allMargins = parsed.flatMap(([, { margins }]) =>
margins.map(d => d.margin)
);
const extent = [0.01, 0.99].map(p => Math.abs(d3.quantile(allMargins, p)));
const max = Math.max(...extent);
return [-max, max];
}
Insert cell
color = d3.scaleLinear([-maxCorr, 0, maxCorr], ["black", "#ccc", "black"])
Insert cell
maxCorr = Math.max(...parsed.map(([, { corr }]) => Math.abs(corr)))
Insert cell
red = "#dd2c35"
Insert cell
blue = "#0080c9"
Insert cell
function halo(text) {
text
.select(function() {
return this.parentNode.insertBefore(this.cloneNode(true), this);
})
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 4)
.attr("stroke-linejoin", "round");
}
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 / 2 +
0.5} H ${cellInner} M ${cellInner} ${cellInner} V 0`
)
)
.call(xAxis)
.call(yAxis)
.call(colorLegend)
Insert cell
xAxis = g =>
g
.append("g")
.attr("transform", `translate(0, ${y(0)})`)
.call(
d3
.axisBottom(x)
.ticks(4)
.tickFormat(d =>
d[xUpperBoundOption === "mostRecent" ? "getDate" : "getHours"]()
)
)
.call(g => g.select(".domain").remove())
.call(g =>
g
.append("text")
.attr("fill", "black")
.attr("x", cellInner / 2)
.attr("dy", "2.5em")
.attr("text-anchor", "middle")
.text(
x.domain()[0].toLocaleString("default", {
month: "long",
day: xUpperBoundOption === "mostRecent" ? undefined : "numeric",
year: "numeric"
})
)
)
Insert cell
yAxis = g =>
g
.append("g")
.attr("transform", `translate(${cellInner}, 0)`)
.call(
d3
.axisRight(y)
.ticks(3)
.tickFormat(
d =>
`Net ${d3.format(",")(Math.abs(d))} ${
d > 0
? "for " + a.last_name
: d < 0
? "for " + b.last_name
: "votes"
}`
)
)
.call(g => g.select(".domain").remove())
Insert cell
colorLegend = g =>
g
.append("g")
.attr("transform", `translate(${3 * cellSize}, ${y(0)})`)
.selectAll("g.color-legend")
.data(color.domain())
.join("g")
.attr("transform", (d, i) => `translate(0, ${(i - 1) * 20})`)
.call(g =>
g
.append("path")
.attr("d", d => `M -4 ${d * 4} 4 ${-d * 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 =>
d === 0
? "No correlation between when vote was counted and candidate favored"
: "Votes counted later tended toward " + (d > 0 ? a : b).last_name
)
)
Insert cell
Insert cell
position = 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
stats = require("simple-statistics")
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