Skip to content

Instantly share code, notes, and snippets.

@callistabee
Last active April 28, 2018 01:17
Show Gist options
  • Save callistabee/31a9f59444c3758c1adc6ab13a704c21 to your computer and use it in GitHub Desktop.
Save callistabee/31a9f59444c3758c1adc6ab13a704c21 to your computer and use it in GitHub Desktop.
Staged Transition Groups in D3
license: apache-2.0

This example shows a bar chart transitioning between two overlapping subsets of data. Rather than moving everything at once, the staging breaks it down into sets of transitions that remove old marks, update live ones, and add new ones.

The staging is done with a technique similar to this one. A counter is used to keep track of outstanding transitions in a group and then move on the next one when all of the transitions have completed.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>Click to change data subset.</p>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="stage.js"></script>
</body>
</html>
const data = [
{name:"orange", count:30},
{name:"apple", count:40},
{name:"banana", count:50},
{name:"strawberry", count:60},
{name:"pear", count:5}
];
// create overlapping subsets
const subsets = [
data.slice(0,3),
data.slice(2,5)
];
// set up chart area
const margin = {
left:30,
right:30,
top:10,
bottom:30
};
const svg_height = 300;
const svg_width = 500;
const height = svg_height - margin.top - margin.bottom;
const width = svg_width - margin.left - margin.right;
const svg = d3.select("body").append("svg")
.attr("width", svg_width)
.attr("height", svg_height);
const chart = svg.append("g")
.attr("id", "chart")
.attr("transform", "translate(" + margin.left + "," + margin.top +")");
const xaxis = chart.append('g')
.attr("id", "x-axis")
.attr("transform", `translate(0,${height})`);
const yaxis = chart.append('g')
.attr("id", "y-axis");
function updatePlot(dataset) {
// get old tick marks, if they exist
const old_x = xaxis.node().__axis;
let old_ticks = undefined;
if (old_x) {
old_ticks = chart.selectAll("#x-axis g.tick")
.data(dataset.map((d) => d.name), old_x);
}
// select bars
const bars = chart.selectAll(".bar").data(dataset, (d) => d.name);
// make axis scales
const x = d3.scaleBand().domain(dataset.map((d)=>d.name)).rangeRound([0,width]).padding(0.1);
const y = d3.scaleLinear().domain([0,d3.max(dataset.map((d)=>d.count))]).rangeRound([height,0]);
// helper function to wrap the next transition group in a semaphore
const make_next_transition = function(num_transitions, next) {
let n = num_transitions-1;
return function () {
n--;
if (n === 0) {
next();
}
}
};
const transition_groups = {
// remove dead ticks and bars
remove: function () {
if (!old_x) {
// first time drawing the plot, just skip to drawing the new data
transition_groups['add_new']();
} else {
const next_transition = make_next_transition(2, transition_groups["rescale_y"]);
old_ticks
.exit()
.transition()
.style("fill-opacity", 0)
.remove()
.on("end", next_transition);
bars.exit().transition().style("fill-opacity", 0).remove()
.on("end", next_transition);
}
},
// rescale y-axis
rescale_y: function () {
yaxis.transition().call(d3.axisLeft(y)).on("end", transition_groups["move"]);
bars.transition()
.attr("y", (d) => y(d.count))
.attr("height", (d) => height - y(d.count));
},
// move remaining ticks and bars
move: function () {
const next_transition = make_next_transition(2, transition_groups["add_new"]);
old_ticks
.transition()
.attr("transform", (d) => "translate(" + (x(d) + x.bandwidth() / 2) + ", 0)")
.on("end", next_transition);
bars.transition()
.attr("x", (d) => x(d.name))
.attr("width", x.bandwidth())
.on("end", next_transition);
},
// and new bars and ticks
add_new: function () {
xaxis.transition().call(d3.axisBottom(x));
yaxis.transition().call(d3.axisLeft(y));
bars.enter().append("rect").attr("class", "bar")
.attr("x", (d) => x(d.name))
.attr("y", (d) => y(d.count))
.attr("height", (d) => height - y(d.count))
.attr("width", x.bandwidth())
.style("fill-opacity", 0)
.transition()
.style("fill-opacity", 1)
}
};
transition_groups["remove"]();
}
// cycle among subsets on click
let current_subset = 0;
chart.on("click", function() {
current_subset = (current_subset + 1) % subsets.length;
updatePlot(subsets[current_subset]);
});
// draw first subset on load
updatePlot(subsets[0]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment