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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more