I am going to make this a bit verbose for two reasons:
- To prove that I have put effort into trying to solve the problem I am presenting, because there is too much ephemeral stuff on the internet
- D3 is awesome because it is complex, it is difficult to understand fully. Recent major versions have emphasised the use of data joining, intended to simplify the general update pattern.
Therefore this might be a bit of a write-up, but I am genuinely looking to understand the answer to my problem so please bear with me.
Context
I want to create a dynamic stacked horizontal bar chart that visualises the different stages of
In the example above the data binding is not by month, but by fruits.
The final state is fine but dynamic (transitioned) data changes will be hard.
This is an area of some complexity. I unashamedly confess I don't understand Bostock's use of stacks, taken from his above mentioned example:
// Compute a nested array of series where each series is [[x1, x2], [x1, x2],
// [x1, x2], …] representing the x-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique y- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([, I], z) => X[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
Nested data joins
Perhaps someone can shine further light on the above. Perhaps I am not understanding something about stacks (very likely ;). Perhaps if the chart were vertical and not horizontal things would be easier? Don't know.
I decided to abandon D3 Stacks and instead turn to data joining nested data, kind of going back to basics.
Repeated readings of Bostock's almost decade old post on the
Thank you for reading and happy new year :))
-* Unfortunately I can not post links to JSFiddle as the most recent D3 version supported is version 5 and I am (perhaps unnecessarily) using the most recent version, 7. Bostock has started using a platform that allows experimentation called Observable but I find to be confusing.
CodePudding user response:
Here is an example that uses the fruits dataset. The chart is animated so that the bars for one fruit are revealed at a time. I do this by giving the bars for each fruit a different transition delay.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// set up
const margin = { top: 10, right: 10, bottom: 20, left: 40 };
const width = 300 - margin.left - margin.right;
const height = 200 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width margin.left margin.right)
.attr('height', height margin.top margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// data
const data = [
{month: "Jan", apples: 3840, bananas: 1920, cherries: 960, dates: 400},
{month: "Feb", apples: 1600, bananas: 1440, cherries: 960, dates: 400},
{month: "March", apples: 640, bananas: 960, cherries: 640, dates: 400},
{month: "Apr", apples: 3120, bananas: 1480, cherries: 640, dates: 400}
];
const fruit = Object.keys(data[0]).filter(d => d != "month");
const months = data.map(d => d.month);
const stackedData = d3.stack()
.keys(fruit)(data);
const xMax = d3.max(stackedData[stackedData.length - 1], d => d[1]);
// scales
const x = d3.scaleLinear()
.domain([0, xMax]).nice()
.range([0, width]);
const y = d3.scaleBand()
.domain(months)
.range([0, height])
.padding(0.25);
const color = d3.scaleOrdinal()
.domain(fruit)
.range(d3.schemeTableau10);
// axes
const xAxis = d3.axisBottom(x).ticks(5, '~s');
const yAxis = d3.axisLeft(y);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.call(g => g.select('.domain').remove());
svg.append("g")
.call(yAxis)
.call(g => g.select('.domain').remove());
// draw bars
// create one group for each fruit
const layers = svg.append('g')
.selectAll('g')
.data(stackedData)
.join('g')
.attr('fill', d => color(d.key));
// transition for bars
const duration = 1000;
const t = d3.transition()
.duration(duration)
.ease(d3.easeLinear);
layers.each(function(_, i) {
// this refers to the group for a given fruit
d3.select(this)
.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => x(d[0]))
.attr('y', d => y(d.data.month))
.attr('height', y.bandwidth())
.transition(t)
// i is the index of this fruit.
// this will give the bars for each fruit a different delay
// so that the fruits will be revealed one at a time.
// using .each() instead of a normal data join is needed
// so that we have access to what fruit each bar belongs to.
.delay(i * duration)
.attr('width', d => x(d[1]) - x(d[0]));
});
</script>
</body>
</html>