Public
Edited
Sep 16, 2024
Importers
Insert cell
Insert cell
import {
keyframes as oldKeyframes
} from "@d3/bar-chart-race"
Insert cell
keyframes = oldKeyframes.map(([a, b]) => [a, b.map((obj) => ({...obj, name: obj.name + " Long Long Long Long Long Long Long Long Long"}))])
Insert cell
function if_no_prev_then_enter(d, y, prev, height, n, first) {
if (first) {
return y(d.rank);
}
if (prev.get(d) && prev.get(d).rank < n) {
return y(prev.get(d).rank);
}
return height;
}
Insert cell
function if_no_next_then_exit(d, y, next, height, n) {
if (next.get(d) && next.get(d).rank < n) {
return y(next.get(d).rank);
}
return height;
}
Insert cell
function bars(svg, x, y, height, color, n, prev, next) {
let bar = svg.append("g")
.attr("fill-opacity", 0.6)
.selectAll("rect");

return ([date, data], transition, first) => 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) || height)
.attr("y", d => if_no_prev_then_enter(d, y, prev, height, n, first))
.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) || height)
.attr("y", d => if_no_next_then_exit(d, y, next, height, n))
.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 axis(svg, marginTop, barSize, n, x, y) {
const g = svg.append("g")
.attr("transform", `translate(0,${marginTop})`);

const tickFormat = undefined;
const axis = d3.axisTop(x)
.ticks(width / 500, tickFormat)
.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
formatNumber = d3.format(",d")
Insert cell
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function(t) {
this.textContent = formatNumber(i(t));
};
}
Insert cell
function isAlignEnd(d, x, marginLeft) {
return x(d.value) > (marginLeft + width / 2);
}
Insert cell
function labels(svg, n, x, y, height, prev, next, marginLeft) {
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, first) => 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) || height})`)
.attr("transform", d => `translate(${x((prev.get(d) || d).value)},${if_no_prev_then_enter(d, y, prev, height, n, first)})`)
.attr("text-anchor", d => isAlignEnd((prev.get(d) || d), x, marginLeft, width) ? "end" : "start")
.attr("y", y.bandwidth() / 2)
.attr("x", d => isAlignEnd((prev.get(d) || d), x, marginLeft, width) ? -6 : 6)
.attr("dy", "-0.25em")
.text(d => d.name)
.call(text => text.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", d => isAlignEnd((prev.get(d) || d), x, marginLeft, width) ? -6 : 6)
.attr("dy", "1.15em")),
update => update,
exit => exit.transition(transition).remove()
// .attr("transform", d => `translate(${x((next.get(d) || d).value)}, ${y((next.get(d) || d).rank) || height})`)
.attr("transform", d => `translate(${x((next.get(d) || d).value)}, ${if_no_next_then_exit(d, y, next, height, n)})`)
.attr("text-anchor", d => isAlignEnd((next.get(d) || d), x, marginLeft, width) ? "end" : "start")
.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) || height})`)
.attr("text-anchor", d => isAlignEnd(d, x, marginLeft, width) ? "end" : "start")
.attr("x", d => isAlignEnd(d, x, marginLeft, width) ? -6 : 6)
.call(g => g.select("tspan")
.tween("text", d => textTween((prev.get(d) || d).value, d.value))
.attr("x", d => isAlignEnd(d, x, marginLeft, width) ? -6 : 6)
)
);
}
Insert cell
formatDate = d3.utcFormat("%Y")
Insert cell
function ticker(svg, barSize, marginTop, n, startDate) {
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", marginTop + barSize * (n - 0.45))
.attr("dy", "0.32em")
.text(formatDate(startDate));

return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
Insert cell
{
const n = 12;
const barSize = 48;
const margin = {top: 16, bottom: 6, left: 0, right: 6};
const height = margin.top + barSize * n + margin.bottom;
const duration = 250;

const x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
const y = d3.scaleBand()
.domain(d3.range(n+1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1);
const colorScale = d3.scaleOrdinal(d3.schemeTableau10);
const color = d => colorScale(d.name);
const nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name);
const prev = new Map(nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a])));
const next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");

const updateBars = bars(svg, x, y, height, color, n, prev, next);
const updateAxis = axis(svg, margin.top, barSize, n, x, y);
const updateLabels = labels(svg, n, x, y, height, prev, next, margin.left);
const updateTicker = ticker(svg, barSize, margin.top, n, keyframes[0][0]);

yield svg.node();
// for (const keyframe of keyframes) {
for (const [i, keyframe] of keyframes.entries()) {
const transition = svg.transition()
.duration(duration)
.ease(d3.easeLinear);

x.domain([0, keyframe[1][0].value]);

updateBars(keyframe, transition, i == 0);
updateAxis(keyframe, transition);
updateLabels(keyframe, transition, i == 0);
updateTicker(keyframe, transition);

invalidation.then(() => svg.interrupt());
await transition.end();
}
}
Insert cell
Insert cell
keyframes[0]
Insert cell
width
Insert cell
// See also: https://observablehq.com/@observablehq/plot-horizontal-bar-chart-with-label?intent=fork
// For animation:
// https://observablehq.com/@fil/plot-animate-a-bar-chart/2
// https://observablehq.com/@fil/plot-animate-a-bar-chart
// https://observablehq.com/@bz-jm/plot-testing
{
const n = 12;
const barSize = 48;
const margin = {top: 16, bottom: 6, left: 0, right: 6};
const height = margin.top + barSize * n + margin.bottom;
const duration = 3250;

const x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
const y = d3.scaleBand()
.domain(d3.range(n+1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1);
const colorScale = d3.scaleOrdinal(d3.schemeTableau10);
const color = d => colorScale(d.name);
const nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name);
const prev = new Map(nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a])));
const next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));

var bindData = keyframes[0][1];
const barChart = Plot.barX(bindData, {
x: "value",
y: "name",
fill: "name",
sort: { y: "x", reverse: true, limit: n },
render: (i, s, v, d, c, next) => {
const g = next(i, s, v, d, c);
c.ownerSVGElement.updateBar = (values) =>
d3.select(g)
.selectAll("rect")
.transition()
.duration(duration)
.attr("width", (i) => s.x(values[i].value) - s.x(0))
.attr("fill", (i) => s.color(values[i].name));
return g;
}
});

const valueChart = Plot.text(bindData, {
text: "value",
y: "name",
x: "value",
textAnchor: "end",
dx: -3,
fill: "white",
render: (i, s, v, d, c, next) => {
const g = next(i, s, v, d, c);
c.ownerSVGElement.updateValue = function(values) {
return d3.select(g)
.selectAll("text")
.transition()
.duration(duration)
// .attr("x", (i) => s.x(values[i].value))
// .attr("dx", -3)
.attr("fill", "red")
.text((i) => s.x(values[i].value))
// .text((i) => values[i].value)
;
}
return g;
}
})

const textChart = Plot.text(bindData, {
text: "name",
y: "name",
x: "value",
textAnchor: "start",
dx: 3,
fill: "black",
render: (i, s, v, d, c, next) => {
const g = next(i, s, v, d, c);
c.ownerSVGElement.updateText = function(values) {
return d3.select(g)
.selectAll("text")
.transition()
.duration(duration)
.attr("fill", "blue")
.attr("x", (i) => s.x(values[i].value))
// .text((i) => values[i].name)
;
}
return g;
}
});

const chart = Plot.plot({
width: width,
marginLeft: 0,
marginRight: 300,
x: { axis: null },
y: { label: null },
marks: [
barChart,
valueChart,
textChart
]
})


const newValues = keyframes[3][1];
chart.updateBar(newValues).end();
chart.updateValue(newValues).end();
chart.updateText(newValues).end();



return chart

/*const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");

const updateBars = bars(svg, x, y, height, color, n, prev, next);
const updateAxis = axis(svg, margin.top, barSize, n, x, y);
const updateLabels = labels(svg, n, x, y, height, prev, next, margin.left);
const updateTicker = ticker(svg, barSize, margin.top, n, keyframes[0][0]);

yield svg.node();
// for (const keyframe of keyframes) {
for (const [i, keyframe] of keyframes.entries()) {
const transition = svg.transition()
.duration(duration)
.ease(d3.easeLinear);

x.domain([0, keyframe[1][0].value]);

updateBars(keyframe, transition, i == 0);
updateAxis(keyframe, transition);
updateLabels(keyframe, transition, i == 0);
updateTicker(keyframe, transition);

invalidation.then(() => svg.interrupt());
await transition.end();
}*/
}
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