Published
Edited
Feb 10, 2020
Importers
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
keyframes = {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
const k = (kb - ka) / duration;
for (let i = 0; i < k; ++i) {
const t = i / k;
const date = new Date(ka * (1 - t) + kb * t);
keyframes.push([
date,
rank(name => a.get(name) * (1 - t) + b.get(name) * t)
// .map(r => {
// return Object.assign(r, {
// // topics: topics[s.speaker].reduce((topics, topic) => {
// // if (topic.end > )
// // })
// });
// })
]);
}
}
keyframes.push([new Date(kb), rank(name => b.get(name))]);
return keyframes;
}
Insert cell
Insert cell
Insert cell
Insert cell
reverseData = [...data].reverse()
Insert cell
datevalues = data.map(d => [
d.date,
new Map(
names.map(name => [
name,
(
reverseData.find(d2 => d2.name === name && +d2.date <= +d.date) || {
value: 0
}
).value * 1000
])
)
])
Insert cell
data = processed.data
Insert cell
processed = rawData.reduce(
(state, row) => {
state.topics[row.speaker].push({
topic: row.topic,
start: state.durations[row.speaker],
end: state.durations[row.speaker] + row.elapsed
});

const value = (state.durations[row.speaker] += row.elapsed);
state.data.push({
date: row.timestamp,
value,
name: row.speaker
});
return state;
},
{
data: [
{
value: 0,
date: subtractDate(
parseDate(rawData.find(d => d.timestamp).timestamp),
{
seconds: +rawData.find(d => d.timestamp).elapsed
}
),
name: "biden"
}
],
durations: Object.fromEntries(names.map(sp => [sp, 0])),
topics: Object.fromEntries(names.map(sp => [sp, []]))
}
)
Insert cell
title = s => s[0].toUpperCase() + s.slice(1)
Insert cell
input = fetch(
'https://int.nyt.com/newsgraphics/2019/debates/2019-12-19-dem-debate/data.json'
).then(res => res.json())
Insert cell
rawData = input
.filter(r => r.speaker && r.speaker !== 'moderator' && r.timestamp)
.map(({ elapsed, timestamp, speaker, topic }) => ({
elapsed: +elapsed,
timestamp: parseDate(timestamp),
speaker,
topic
}))
Insert cell
Insert cell
parseDate = {
const parser = d3.timeParse('%I:%M:%S %p');
return t => {
const date = parser(t);
if (!date) return date;
date.setFullYear(debateDate.getFullYear());
date.setMonth(debateDate.getMonth());
date.setDate(debateDate.getDate());
return date;
};
}
Insert cell
formatDate = d3.timeFormat('%-I:%M %p')
Insert cell
formatNumber = n => {
const secs = n / 1e3;
const mins = Math.floor(secs / 60);
const fmt =
mins +
':' +
Math.round(secs - mins * 60)
.toString()
.padStart(2, '0');
// console.log(n, fmt);
return fmt;
}
Insert cell
Insert cell
color = () => d3.scaleOrdinal(d3.schemeTableau10)(0)
Insert cell
function labels(svg) {
let label = svg
.append("g")
.style("font", "bold 12px var(--sans-serif)")
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.selectAll("text");

return ([date, data], transition) =>
(label = label
.data(data.slice(0, n), d => d.name)
.join(
enter =>
enter
.append("text")
.attr(
"transform",
d => `translate(${x(prev(d).value)},${y(prev(d).rank)})`
)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.attr("dy", "-0.25em")
.text(d => title(d.name))
.call(text =>
text
.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "1.15em")
),
update => update,
exit =>
exit
.transition(transition)
.remove()
.attr(
"transform",
d => `translate(${x(next(d).value)},${y(next(d).rank)})`
)
.call(g =>
g
.select("tspan")
.tween("text", d => textTween(d.value, next(d).value))
)
)
.call(bar =>
bar
.transition(transition)
.attr("transform", d => `translate(${x(d.value)},${y(d.rank)})`)
.call(g =>
g
.select("tspan")
.tween("text", d => textTween(prev(d).value, d.value))
)
));
}
Insert cell
function axis(svg) {
const g = svg.append("g").attr("transform", `translate(0,${margin.top})`);

const axis = d3
.axisTop(x)
.ticks(width / 160, '%-M:%S')
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));

return (_, transition) => {
g.transition(transition).call(axis);
g.select(".tick:first-of-type text").remove();
g.selectAll(".tick:not(:first-of-type) line").attr("stroke", "white");
g.select(".domain").remove();
};
}
Insert cell
x.domain()
Insert cell
x = d3.scaleUtc(
[0, d3.max(data, d => d.date) - d3.min(data, d => d.date)],
[margin.left, width - margin.right]
)
Insert cell
function bars(svg) {
let bar = svg
.append("g")
.attr("fill-opacity", 0.6)
.selectAll("rect");

return ([date, data], transition) =>
(bar = bar
.data(data.slice(0, n), d => d.name)
.join(
enter =>
enter
.append("rect")
.attr("fill", color)
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", d => y(prev(d).rank))
.attr("width", d => x(prev(d).value) - x(0)),
update => update,
exit =>
exit
.transition(transition)
.remove()
.attr("y", d => y(next(d).rank))
.attr("width", d => x(next(d).value) - x(0))
)
.call(bar =>
bar
.transition(transition)
.attr("y", d => y(d.rank))
.attr("width", d => x(d.value) - x(0))
));
}
Insert cell
prev(keyframes[1][2])
Insert cell
getDelta = delta => frameEntry => {
const idx =
keyframes.findIndex(f => f[1].includes(frameEntry)) +
delta * viewof speed.value;
if (idx >= 0 && idx < keyframes.length) {
return keyframes[idx][1].find(f => f.name === frameEntry.name);
}
return frameEntry;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
subtractDate = import('https://unpkg.com/date-fns@2.9.0/esm/sub/index.js').then(
m => m.default
)
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