Published
Edited
May 24, 2020
Importers
Insert cell
Insert cell
Insert cell
data = d3.csvParse(await FileAttachment("teotw_race.csv").text(), d3.autoType)
Insert cell
Insert cell
chart = {
replay;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);

yield svg.node();

for (const keyframe of keyframes) {
const transition = svg.transition()
.duration(duration)
.ease(d3.easeLinear);

// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);

updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);

invalidation.then(() => svg.interrupt());
await transition.end();
}
}
Insert cell
duration = 250
Insert cell
n = names.size
Insert cell
names = new Set(data.map(d => d.character))
Insert cell
datevalues = Array.from(d3.rollup(data, ([d]) => d.value, d => +d.chapter_order, d => d.chapter, d => d.character))
// .map(([date, data]) => [new Date(date), data])
.map(([chapterOrder, data]) => [chapterOrder, data])
.sort(([a], [b]) => d3.ascending(a, b))
Insert cell
function rank(value) {
const data = Array.from(names, name => ({name, value: value(name)}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
Insert cell
k = 3
Insert cell
keyframes = {
const keyframes = [];
let ka, a, kb, b, ma, mb, cha;
let curr = 1;
for ([[ka, ma], [kb, mb]] of d3.pairs(datevalues)) {
// ka = curr
// curr += 1
// kb = curr
for (const key of ma.keys()) { a = ma.get(key); cha = key; }
for (const key of mb.keys()) { b = mb.get(key) }

for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
ka * (1 - t) + kb * t,
rank(name => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t),
cha
]);
}
}
keyframes.push([kb, rank(name => b.get(name) || 0)]);
return keyframes;
}
Insert cell
nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name)
Insert cell
Insert cell
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.get(d) || d).rank))
.attr("width", d => x((prev.get(d) || d).value) - x(0))
.call(rect => rect.append('title').text(d=> d.name)),
update => update,
exit => exit.transition(transition).remove()
.attr("y", d => y((next.get(d) || d).rank))
.attr("width", d => x((next.get(d) || 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
function labels(svg) {
let label = svg.append("g")
.style('font-size', fontSize)
.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.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.attr("dy", "-0.25em")
.classed("bar-text", true)
.text(d => d.name)
.call(text => text.append('title').text(d=> d.name))
.call(text => text.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "1.15em").classed("bar-text", true).classed("bar-number", true)),
update => update,
exit => exit.transition(transition).remove()
.attr("transform", d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)
.call(g => g.select("tspan").tween("text", d => textTween(d.value, (next.get(d) || 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.get(d) || d).value, d.value))));
}
Insert cell
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function(t) {
this.textContent = formatNumber(i(t));
};
}
Insert cell
formatNumber = d3.format(",d")
Insert cell
function axis(svg) {
const g = svg.append("g")
.attr("transform", `translate(0,${margin.top})`);

const axis = d3.axisTop(x)
.ticks(5)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));

return (_, transition) => {
g.transition(transition).call(axis);
g.select(".tick:first-of-type text").remove();
g.selectAll('text').style('font-size', fontSize)
g.selectAll(".tick:not(:first-of-type) line").attr("stroke", "white");
g.select(".domain").remove();
};
}
Insert cell
function ticker(svg) {
const now = svg.append("text")
.style("font-size", `${barSize/3}px`)
.style("font-family", `sans-serif`)
.attr("text-anchor", "end")
.attr("x", width - 6)
.attr("y", margin.top + barSize * (n - 0.45))
.attr("dy", "0.32em")
.classed("chapter-text", true)
.text(keyframes[0][2]);

return ([chapterOrder, rank, chapter], transition) => {
transition.end().then(() => now.text(chapter));
};
}
Insert cell
fontSize = width / 40
Insert cell
formatDate = d3.utcFormat("%Y")
Insert cell
color = {
const scale = d3.scaleOrdinal(["#888888", "#bbbbbb", "green", "#5B8B9B", "#77C0A0", "#264D54", "#324280", "#D27AE9", "#867AC4", "#72329E", "#BEE091", "#48853A", "#8BEC6E", "#8B9662", "#663B11", "#F3A852", "#E0502F", "#932846", "#A17965", "#E7799E", "#EC3686", "#414B17", "#F0D447", "#4141F5", "#E743F4"]).domain(["last appeared", "last mentioned", "continue", "not specified", "Female", "Male", "Andoran", "Unknown nationality", "Shienaran", "Malkieri", "Manetherenite", "Illianer", "Taraboner", "Ghealdanin", "Aridholin", "Essenian", "Far Madding", "Darmovanin", "Saldaean", "Murandian" ])
const categoryByName = new Map(data.map(d => [d.character, d[colorChoice]]))
return d => scale(categoryByName.get(d.name));
}
Insert cell
x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])
Insert cell
y = d3.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1)
Insert cell
height = margin.top + barSize * n + margin.bottom
Insert cell
barSize = width / 10
Insert cell
margin = ({top: 40, right: 6, bottom: 6, left: 0})
Insert cell
d3 = require("d3@5", "d3-array@2")
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