Published
Edited
Nov 17, 2020
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { presidentByState, president } from "1342cadc9354cc67"
Insert cell
import { select } from "@jashkenas/inputs"
Insert cell
file = presidentByState.get(stateKey)
Insert cell
a = file.candidates.find(d => d.party_id === "republican")
Insert cell
b = file.candidates.find(d => d.party_id === "democrat")
Insert cell
stateName = file.state_name
Insert cell
timeseries = file.timeseries.map(d => ({
x:
d.vote_shares[a.candidate_key] /
(d.vote_shares[a.candidate_key] + d.vote_shares[b.candidate_key]) || 0.5,
y: d.eevp / 100,
t: new Date(d.timestamp)
}))
Insert cell
final = timeseries.slice(-1)[0].x
Insert cell
color1 = "#dd2c35"
Insert cell
color2 = "#0080c9"
Insert cell
counties = []
Insert cell
// how's this file different??
(await FileAttachment("summary.json").json()).races[0].timeseries
Insert cell
Insert cell
accumulate = arr => {
const result = [];
const _N = d3.sum(arr, ({ n }) => n);
let _a = 0;
let _n = 0;
arr.forEach(function({ a, n }) {
_a += a;
_n += n;
result.push({ x: _a / _n, y: _n / _N });
});
return result;
}
Insert cell
getBest = data =>
accumulate(data.slice().sort((a, b) => d3.descending(a.a / a.n, b.a / b.n)))
Insert cell
getWorst = data =>
accumulate(data.slice().sort((a, b) => d3.ascending(a.a / a.n, b.a / b.n)))
Insert cell
bestRun = getBest(counties)
Insert cell
worstRun = getWorst(counties)
Insert cell
line = d3
.line()
.x(d => x(d.x))
.y(d => y(d.y))
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
renderHypotheticals = g =>
g
.selectAll("g.hypothetical")
.data([bestRun, worstRun])
.join("g")
.attr("class", "hypothetical")
.each(function(d, i) {
const uid = DOM.uid("hypo");
const sel = d3.select(this);
const path = sel
.append("path")
.attr("id", uid.id)
.attr("stroke-dasharray", "3,3")
.attr("d", line(i ? d : d.slice().reverse()));
sel
.append("text")
.attr("x", d => path.node().getTotalLength() / 2)
.attr("dy", 15)
.attr("fill", "white")
.attr("stroke", "none")
.attr("text-anchor", "middle")
.append("textPath")
.attr("href", uid.href)
.text(`If ${(i ? b : a).last_name} counties reported first`);
})
Insert cell
Insert cell
timeTicks = [
d3.min(timeseries, d => d.t),
...d3.timeHour
.range(...d3.extent(timeseries, d => d.t))
.filter(d => timeScale(d) < .99)
]
Insert cell
timeScale = d3.scaleTime(timeseries.map(d => d.t), timeseries.map(d => d.y))
Insert cell
timeFormat = d3.timeFormat("%I:%M %p")
Insert cell
timeAxis = g =>
g
.selectAll("g")
.data(timeTicks)
.join("g")
.attr("transform", d => `translate(${width}, ${y(timeScale(d))})`)
.call(g =>
g
.append("line")
.attr("stroke", d => (timeScale(d) > 0.5 ? "white" : "#ccc"))
.attr("x2", -5)
)
.call(g =>
g
.append("text")
.attr("text-anchor", "end")
.attr("x", -7)
.attr("dy", "0.3em")
.attr("fill", d => (timeScale(d) > 0.5 ? "white" : "#ccc"))
.text(d => timeFormat(d))
)
Insert cell
pctAxis = g =>
g
.selectAll("g")
.data([.25, .5, .6, .7, .8, 0.9, 0.95, 0.99])
.join("g")
.attr("transform", d => `translate(0, ${y(d)})`)
.call(g =>
g
.append("line")
.attr("stroke", d => (d > 0.5 ? "white" : "#ccc"))
.attr("x2", 5)
)
.call(g =>
g
.append("text")
.attr("x", 7)
.attr("dy", "0.3em")
.attr("fill", d => (d > 0.5 ? "white" : "#ccc"))
.text(d => pct(d))
)
Insert cell
pct = d3.format(".0%")
Insert cell
Insert cell
// currentShare ∈ [0,1] fraction of votes so far cast for Party A
// votesCounted ∈ [0,1] fraction of total votes that have been counted
// shareNeeded ∈ [0,1] fraction of remaining votes needed for Party A to win
value = (currentShare, votesCounted) =>
votesCounted === 1
? currentShare > .5
? -Infinity
: currentShare < .5
? Infinity
: NaN
: (currentShare * votesCounted - 0.5) / (votesCounted - 1)
Insert cell
// solve for z: z = (x * y - t) / (y - 1)
// solve for y: y = (t - z) / (x - z) and x!=z and t!=x
// solve for x: x = (t + (y - 1) * z) / y and y!=0 and y!=1
// solve for t: t = x y - y z + z and y!=1
Insert cell
contour = z => x => (0.5 - z) / (x - z)
Insert cell
newContours = d3
.range(0.1, 1, 0.1)
.filter(z => z !== 0.5)
.map(z => ({
z,
data: [...d3.range(0, 1, 0.02), 1]
.map(x => ({ x, y: contour(z)(x) }))
.filter(({ x, y }) => y >= 0 && y <= 1)
}))
Insert cell
_color = d3.scaleSequential(
[0, 1],
d3.interpolateRgb.gamma(2.2)(color1, color2)
)
Insert cell
color = d =>
d >= 1
? `url(${window.location.href}#checkerboard2)`
: d < 0
? `url(${window.location.href}#checkerboard1)`
: "#eee"
Insert cell
defs = `
${makeCheckerboard("checkerboard1", color1, checkSize)}
${makeCheckerboard("checkerboard2", color2, checkSize)}
${makeCheckerboard("smallCheckerboard1", color1, 5)}
${makeCheckerboard("smallCheckerboard2", color2, 5)}
</pattern>
`
Insert cell
makeCheckerboard = (name, color, checkSize) => `
<pattern id="${name}" patternUnits="userSpaceOnUse" width="${checkSize *
2}" height="${checkSize * 2}">
<rect width="${checkSize * 2}" height="${checkSize *
2}" x="0" y="0" fill="#ddd" />
<rect width="${checkSize * 2}" height="${checkSize *
2}" x="0" y="0" fill="${color}" opacity="0.3"/>
<rect width="${checkSize}" height="${checkSize}" x="0" y="0" fill="${color}" opacity="${
checkSize < 10 ? 1 : 0.2
}" />
<rect width="${checkSize}" height="${checkSize}" x="${checkSize}" y="${checkSize}" fill="${color}" opacity="${
checkSize < 10 ? 1 : 0.2
}" />`
Insert cell
checkSize = 16
Insert cell
thresholds = [-Infinity, ...d3.range(0, 1, .1), 1]
Insert cell
grid = {
const q = 4; // The level of detail, e.g., sample every 4 pixels in x and y.
const x0 = x.range()[0] - q / 2,
x1 = x.range()[1] + q;
const y0 = y.range()[0] - q / 2,
y1 = y.range()[1] + q;
const n = Math.ceil((x1 - x0) / q);
const m = Math.ceil((y1 - y0) / q);
const grid = new Array(n * m);
for (let j = 0; j < m; ++j) {
for (let i = 0; i < n; ++i) {
grid[j * n + i] = value(x.invert(i * q + x0), y.invert(j * q + y0));
}
}
grid.x = -q;
grid.y = -q;
grid.k = q;
grid.n = n;
grid.m = m;
return grid;
}
Insert cell
// Converts from grid coordinates (indexes) to screen coordinates (pixels).
transform = ({type, value, coordinates}) => {
return {type, value, coordinates: coordinates.map(rings => {
return rings.map(points => {
return points.map(([x, y]) => ([
grid.x + grid.k * x,
grid.y + grid.k * y
]));
});
})};
}
Insert cell
contours = d3
.contours()
.size([grid.n, grid.m])
.thresholds(thresholds)(grid)
.map(transform)
Insert cell
x = d3.scaleLinear(zoomScale(yDomain0), [0, width])
Insert cell
y = d3.scaleLinear([yDomain0, 1], [0, height])
Insert cell
zoomScale = d3.interpolate([0, 1], [0.49, 0.51])
Insert cell
height = Math.floor((width + 28) / 4) * 4
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
md`---

## To-do

- Rip out unnecessary D3 contour code, since there’s a simple analytic solution, DUH!
- Respect the domains such that it’s easier to zoom in on one part
- Animate zooming in on the end of the funnel!!
- Account for fuzzy estimate of votes remaining!!
- Code-code line by which way it’s leaning?
- Leave room for some info on what’s remaining?
- Bring back the “same updates in a different order” counterfactual cone of paths, which may be very important this year as the plausibiity of shifts is challenged
- transition b/w time equal-interval and percentage equal-interval`
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