Published
Edited
Nov 10, 2020
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
state = (await FileAttachment("iowa.json").json())["senate-iowa"]
Insert cell
a = state.regions.candidates.find(d => d.party_id === "republican")
Insert cell
b = state.regions.candidates.find(d => d.party_id === "democrat")
Insert cell
counties = state.regions.counties.map(({ name, results }) => ({
name,
a: results[a.candidate_key],
n: results[a.candidate_key] + results[b.candidate_key]
}))
Insert cell
timeseries = state.timeseries
.map(({ percent_counted, vote_share_counted, timestamp }) => {
return {
x:
vote_share_counted[a.candidate_key] /
(vote_share_counted[a.candidate_key] +
vote_share_counted[b.candidate_key]),
y: percent_counted,
t: new Date(timestamp)
};
})
.filter(d => !isNaN(d.x))
Insert cell
final = timeseries.slice(-1)[0].x
Insert cell
color1 = "#dd2c35"
Insert cell
color2 = "#0080c9"
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
.attr("fill", "white")
.selectAll("g")
.data(timeTicks)
.join("g")
.attr("transform", d => `translate(${width}, ${y(timeScale(d))})`)
.call(g =>
g
.append("line")
.attr("stroke", "white")
.attr("x2", -5)
)
.call(g =>
g
.append("text")
.attr("text-anchor", "end")
.attr("x", -7)
.attr("dy", "0.3em")
.text(d => timeFormat(d))
)
Insert cell
pctAxis = g =>
g
.attr("fill", "white")
.selectAll("g")
.data([.25, .5, .75])
.join("g")
.attr("transform", d => `translate(0, ${y(d)})`)
.call(g =>
g
.append("line")
.attr("stroke", "white")
.attr("x2", 5)
)
.call(g =>
g
.append("text")
.attr("x", 7)
.attr("dy", "0.3em")
.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
_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)`
: _color(d)
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([0, 1], [0, width])
Insert cell
y = d3.scaleLinear([0, 1], [0, height])
Insert cell
height = Math.floor((width + 28) / 4) * 4
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
import { ramp } from "@d3/color-legend"
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