Public
Edited
Dec 5, 2023
Insert cell
Insert cell
Insert cell
title = 'Rivalry Counter'
Insert cell
Insert cell
transitionDuration = 6000
Insert cell
interpolation = 12
Insert cell
delay = 600
Insert cell
barSize = 50
Insert cell
margin = ({ top: 55 , right: 90, bottom: 6, left: 185 })
Insert cell
md`## Images`
Insert cell
md`Angier`
Insert cell
angier
Insert cell
angier = FileAttachment("Angier.png").image()
Insert cell
md`Bordentwins`
Insert cell
bordentwins
Insert cell
bordentwins = FileAttachment("bordentwins.png").image()
Insert cell
chart = {

const svg = d3.create('svg').attr("viewBox", [0, 0, width, height]);
svg.append('text').attr('class', 'title').attr('y', 24).text(title);

const updateAxis = axis(svg);
const updateBars = bars(svg);
const updateLeftLabels = leftLabels(svg);
const updateRightLabels = rightLabels(svg);
const updateTicker = ticker(svg);

yield svg.node();

for (const [index, keyframe] of Object.entries(keyframes)) {
const transition = svg
.transition()
.delay(index % interpolation === 1 ? delay : 0)
.duration(transitionDuration / interpolation)
.ease(d3.easeLinear);

// Extract the top bar’s value.
x.domain([0, keyframe.data[0].value]);
updateBars(keyframe, transition);
updateAxis(keyframe, transition);
updateLeftLabels(keyframe, transition);
updateRightLabels(keyframe, transition);
updateTicker(keyframe, transition);

invalidation.then(() => svg.interrupt());
await transition.end();
}
}
Insert cell
names = [...new Set(dateValues.flatMap(({ values }) => Object.keys(values)))]
Insert cell
n = Math.min(names.length, 12)
Insert cell
images = ({
'Angier': angier,
})
Insert cell
function rank(getValue) {
return names
.map(name => ({ name, value: getValue(name) }))
.sort(({ value: aValue }, { value: bValue }) => d3.descending(aValue, bValue))
.map(({ name, value }, index) => ({ name, value, rank: value === 0 ? n : index }));
}
Insert cell
keyframes = {
const lastDateValues = dateValues[dateValues.length - 1];
return [
...d3
.pairs(dateValues)
.flatMap(
([{ period, values: aValues }, { values: bValues }]) => d3.range(0, interpolation).map(i => {
const t = i / interpolation;

return { period, data: rank(name => (aValues[name] || 0) * (1 - t) + (bValues[name] || 0) * t) };
})
),
{ period: lastDateValues.period, data: rank(name => lastDateValues.values[name] || 0) }
]
}
Insert cell
nameframes = d3.group(keyframes.flatMap(({ data }) => data), ({ name }) => name)
Insert cell
nameframesValues = [...nameframes.values()]
Insert cell
Insert cell
Insert cell
height = margin.top + barSize * n + margin.bottom
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
function formatNumber(number) {
return `${d3.format(',d')(number)}`
}
Insert cell
function normalizeName(name) {
return name.endsWith('(remote)') ? name.slice(0, -' (remote)'.length) : name;
}
Insert cell
// Modified version of https://bl.ocks.org/mbostock/7555321
function wrapText(text, width) {
text.each(function() {
const text = d3.select(this);
const words = text.text().split(/\s+/).reverse();
let word;
let line = [];
let lineNumber = 0;
const lineHeight = 1.1; // ems
const x = text.attr('x');
const y = text.attr('y');
const dy = parseFloat(text.attr('dy')) || 0;
let tspan = text.text(null).append('tspan').attr('x', x).attr('y', y);
const tspans = [tspan];
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(' '));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(' '));
line = [word];
lineNumber += 1;
tspan = text
.append('tspan')
.attr('x', x)
.attr('y', y)
.attr('dy', `${lineNumber * lineHeight + dy}em`)
.text(word);
tspans.push(tspan);
}
}
for (const [index, tspan] of Object.entries(tspans)) {
tspan.attr('dy', `${(index - (tspans.length - 1) / 2) * lineHeight + dy}em`);
}
});
}
Insert cell
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return t => formatNumber(i(t));
}
Insert cell
function sumNonRemote(data) {
return d3.sum(data.filter(({ name }) => !name.endsWith('(remote)')).map(({ value }) => value));
}
Insert cell
function sumRemote(data) {
return d3.sum(data.filter(({ name }) => name.endsWith('(remote)')).map(({ value }) => value));
}
Insert cell
function bars(svg) {
let bar = svg.append('g').attr('fill-opacity', 0.85).selectAll('rect');

return ({ data }, transition) => {
bar = bar
.data(data.slice(0, n), d => d.name)
.join(
enter => enter
.append('rect')
.attr('fill', 'blue')
.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
function leftLabels(svg) {
let label = svg.append('g').attr('class', 'leftLabels').selectAll('text');
const imageSize = 40;

return ({ data }, transition) => {
label = label
.data(data.slice(0, n), d => d.name)
.join(
enter => {
const res = enter
.append('g')
.attr('transform', d => `translate(${x(0)},${y((prev.get(d) || d).rank)})`)
.attr('y', 26)
.attr('x', -6);
res
.append('text')
.text(d => d.name)
.attr('transform', d => `translate(${-imageSize - 12},26)`)
.call(wrapText, margin.left - imageSize - 12);

res
.append('svg:image')
.attr('href', d => images[normalizeName(d.name)])
.attr('width', imageSize)
.attr('height', imageSize)
.attr('transform', d => `translate(${-imageSize - 6},${(y.bandwidth() - imageSize) / 2})`);
return res;
},
update => update,
exit => exit
.transition(transition)
.remove()
.attr('transform', d => `translate(${x(0)},${y((next.get(d) || d).rank)})`)
)
.call(label => label
.transition(transition)
.attr('transform', d => `translate(${x(0)},${y(d.rank)})`)
);
};
}
Insert cell
function rightLabels(svg) {
let label = svg.append('g').attr('class', 'rightLabels').selectAll('text');

return ({ data }, transition) => {
label = label
.data(data.slice(0, n), d => d.name)
.join(
enter => enter
.append('text')
.attr('class', 'halo')
.attr('transform', d => `translate(${x((prev.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
.attr('y', 26)
.attr('x', 6),
update => update,
exit => exit
.transition(transition)
.remove()
.attr('transform', d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)
.textTween(d => textTween(d.value, (next.get(d) || d).value))
)
.call(label => label
.transition(transition)
.attr('transform', d => `translate(${x(d.value)},${y(d.rank)})`)
.textTween(d => textTween((prev.get(d) || 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).tickSizeOuter(0).tickSizeInner(-barSize * (n + y.padding()));

return (_, transition) => {
g.transition(transition).call(axis);
};
}
Insert cell
function ticker(svg) {
const now = svg
.append('text')
.attr('class', 'tickerTime halo')
.attr('x', width - 6)
.attr('y', height - 74)
.text(keyframes[0].period);
let nonRemote = svg
.append('text')
.attr('class', 'tickerTotal halo')
.attr('x', width - 6)
.attr('y', height - 40)
.property('_current', sumNonRemote(keyframes[0].data));

let remote = svg
.append('text')
.attr('class', 'tickerTotal halo')
.attr('x', width - 6)
.attr('y', height - 6)
.property('_current', sumRemote(keyframes[0].data));
return ({ period, data }, transition) => {
nonRemote = nonRemote
.call(total => total.transition(transition).textTween(function() {
const i = d3.interpolate(this._current, sumNonRemote(data));
return function(t) {
this._current = i(t);
return `Total non-remote: ${formatNumber(this._current)}`;
};
}));
remote = remote
.call(total => total.transition(transition).textTween(function() {
const i = d3.interpolate(this._current, sumRemote(data));
return function(t) {
this._current = i(t);
return this._current ? `Total remote: ${formatNumber(this._current)}` : '';
};
}));
transition.end().then(() => {
now.text(period);
});
};
}
Insert cell
d3 = require('d3-scale@3', 'd3-scale-chromatic@1', 'd3-array@2', 'd3-selection@1', 'd3-format@1', 'd3-ease@1', 'd3-interpolate@1', 'd3-axis@1', 'd3-transition@1')
Insert cell
md`Inspired by https://observablehq.com/@d3/bar-chart-race-explained and https://observablehq.com/@johnburnmurdoch/bar-chart-race`
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