Published
Edited
Mar 25, 2021
1 star
Insert cell
Insert cell
data = d3.csvParse(await FileAttachment("bar_chart_race_data_40.csv").text(), d3.autoType)
Insert cell
Insert cell
Insert cell
Insert cell
// width = 1000
Insert cell
chart = {
replay;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width + currency_x_shift, 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
Insert cell
Insert cell
Insert cell
duration = 250
Insert cell
Insert cell
Insert cell
Insert cell
data
Insert cell
Insert cell
d3.group(data, d => d.name)
Insert cell
Insert cell
data.filter(d => d.name === "Heineken")
Insert cell
Insert cell
n = new Set(data.map(d => d.name)).size
Insert cell
Insert cell
names = new Set(data.map(d => d.name))
Insert cell
Insert cell
datevalues = Array.from(d3.rollup(data, ([d]) => d.value, d => +d.date, d => d.name))
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b))
Insert cell
Insert cell
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));
const zero_count = data.filter(d => (d.value <= 0 | d.value == undefined)).length
for (let i = 0; i < data.length; ++i) {
if (data[i].value <= 0 | data[i].value == undefined) {
data[i].rank = n+n-zero_count-i-1;
} else {
data[i].rank = i;
}
}
return data;
}
Insert cell
Insert cell
rank(name => datevalues[0][1].get(name))
Insert cell
Insert cell
k = 10
Insert cell
Insert cell
function make_keyframes() {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank(name => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t)
]);
}
}
keyframes.push([new Date(kb), rank(name => b.get(name) || 0)]);
return keyframes;
}
Insert cell
keyframes = make_keyframes()
Insert cell
Insert cell
Insert cell
Insert cell
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(data))
.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)),
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
currency_x_shift = 85
Insert cell
currency_y_shift = "0em"
Insert cell
company_x_shift = -10
Insert cell
company_y_shift = "0.4em"
Insert cell
function labels(svg) {
let label = svg.append("g")
.style("font", "26px 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.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
.attr("y", y.bandwidth() / 2)
.attr("font-weight", "normal")
.attr("x", company_x_shift)
.attr("dy", company_y_shift)
.text(d => d.name)
.call(text => text.append("tspan")
// .attr("text-anchor", "start")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", currency_x_shift)
.attr("dy", currency_y_shift)),
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
Insert cell
Insert cell
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function(t) {
this.textContent = formatNumber(i(t));
};
}
Insert cell
Insert cell
Insert cell
formatNumber = d3.format("$,d")
Insert cell
Insert cell
function axis(svg) {
const g = svg.append("g")
.attr("transform", `translate(0,${margin.top})`);

const axis = d3.axisTop(x)
.ticks(width / 160)
.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
Insert cell
Insert cell
// function ticker(svg) {
// const now = svg.append("text")
// .style("font", `bold ${barSize}px var(--sans-serif)`)
// .style("font-variant-numeric", "tabular-nums")
// .attr("text-anchor", "end")
// .attr("x", width - 6)
// .attr("y", margin.top + barSize * (n - 0.45))
// .attr("dy", "0.32em")
// .text(formatDate(keyframes[0][0]));

// return ([date], transition) => {
// transition.end().then(() => now.text(formatDate(date)));
// };
// }
Insert cell
Insert cell
Insert cell
formatDate = d3.utcFormat("%Y")
Insert cell
Insert cell
function color(d) {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some(d => d.category !== undefined)) {
const categoryByName = new Map(data.map(d => [d.name, d.category]))
scale.domain(Array.from(categoryByName.values()));
return d => scale(categoryByName.get(d.name));
}
return d => scale(d.name);
}
Insert cell
Insert cell
Insert cell
Insert cell
x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])
Insert cell
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
Insert cell
height = margin.top + barSize * n + margin.bottom
Insert cell
barSize = 48
Insert cell
margin = ({top: 16, right: 6, bottom: 6, left: 0})
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
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