Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active October 27, 2020 14:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Kcnarf/9e4813ba03ef34beac6e to your computer and use it in GitHub Desktop.
Save Kcnarf/9e4813ba03ef34beac6e to your computer and use it in GitHub Desktop.
D3.selectAll(...).transition() Explained
license: lgpl-3.0

I hope this sequel will help you to understand how a d3-transition works, specifically when it is applied on multiple elements.

The main objective of this sequel is to illustrate that:

  • selectAll(...).transition() defines several transitions
  • each transition runs independantly

This sequel also illustrates:

  • the use of transition().delay(...), transition().duration()
  • the lifecycle of a transition (scheduled/starting/running/ending/stopped)

This sequel uses D3 v3.5.5.

If you look at the code, you will see that:
  • I use named transition in order to run several transition on the same element
  • I stop running transitions by overriding the adequate named transitions
.top-container {
position: relative;
width: 960px;
height: 160px;
}
input {
margin-right: 10px;
}
span.comment {
color: gray;
}
span.space-2 {
margin-left: 10px;
}
span.space-4 {
margin-left: 20px;
}
span.space-6 {
margin-left: 30px;
}
.explanation-area {
position: absolute;
left: 380px;
top: 0px;
}
svg{
position: absolute;
top: 0px;
}
line {
stroke-width: 1.5px;
stroke: #000;
}
circle {
fill-opacity: .2;
stroke-width: 1.5px;
fill: #000;
}
path {
stroke: #000;
stroke-width: 1.5px;
}
var width = 960,
height = 500,
delay = 1000,
duration = 1000,
circleInitialPosition = 200,
circleCount = 7,
circleSpacing = width/(circleCount+1),
circleMovingIncrement = 30,
data = d3.range(circleCount),
explanations = _makeExplanations(),
explanationArea = d3.select(".explanation-area");
var svg = d3.select("body").insert("svg", ".top-container")
.attr("width", width)
.attr("height", height);
_goToStage(0);
function _playStage0() {
//prepare the field, add circles to svg
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("transform", function(d,i) { return "translate(" + circleSpacing*(i+1) + "," + circleInitialPosition + ")" ;})
.attr("r", 10)
.style("stroke", '#000');
}
function _playStage1() {
//Highlight selected circles (bigger, green stroke)
svg.selectAll("circle")
.transition("playStage1")
.attr('r', 15)
.style('stroke', 'green')
.ease('bounce')
.duration(duration);
}
function _rewindStage1() {
//interrupt stage1's transitions (running and scheduled)
svg.selectAll('circle')
.transition("playStage1")
.duration(0)
//rewind to stage1's initial state
svg.selectAll("circle")
.transition("rewindStage1")
.attr('r', 10)
.style("stroke", '#000');
}
function _playStage2() {
//Draw one scheduled transition per circle
//Each transitiopn as a head, a body, a tail, a start time, and an end time
var drawnTransitions = svg.selectAll(".drawn-transition")
.data(data)
.enter().append("g")
.classed("drawn-transition", true)
.attr("transform", function(d,i) { return "translate(" + circleSpacing*(i+1) + "," + circleInitialPosition + ")" ;})
var tails = drawnTransitions.append("path")
.classed("tail", true)
.attr("d", d3.svg.symbol().size(20))
.style("fill", 'grey')
.style("stroke", 'grey');
var bodies = drawnTransitions.append("line")
.classed("body", true)
.attr({x1: 0, y1: 0, x2: 0, y2: 0})
.attr("stroke-dasharray", "5")
.style("stroke", 'grey');
var heads = drawnTransitions.append("path")
.classed("head", true)
.attr("d", d3.svg.symbol().type("triangle-down").size(40))
.style("fill", 'grey')
.style("stroke", 'grey');
var starts = drawnTransitions.append("text")
.classed("start-time", true)
.attr("transform", function (d, i) { return "translate(-10, " + (circleMovingIncrement*(i+1) + 15) + ")"; })
.text(function(d, i) {return "start: " + i + "s"})
.attr("text-anchor", "end")
.attr("font-size", "13px")
.style("fill", 'grey')
.attr("fill-opacity", 0)
.style("stroke", 'green')
.attr("stroke-width", 2)
.attr("stroke-opacity", 0);
var ends = drawnTransitions.append("text")
.classed("end-time", true)
.attr("transform", function (d, i) { return "translate(-10, " + (circleMovingIncrement*(i+1) + 30) + ")"; })
.text(function(d, i) {return "end: " + (2*i+1) + "s"})
.attr("text-anchor", "end")
.attr("font-size", "13px")
.style("fill", 'grey')
.attr("fill-opacity", 0)
.style("stroke", 'red')
.attr("stroke-width", 2)
.attr("stroke-opacity", 0);
//Animate each transition (longer bodies, adequate position of heads, show start-times and end-times)
bodies.transition("playStage2")
.attr("y2", function(d, i) { return circleMovingIncrement*(i+1); })
.duration(duration);
heads.transition("playStage2")
.attr("transform", function (d, i) { return "translate(0, " + circleMovingIncrement*(i+1) + ")"; })
.duration(duration);
starts.transition("playStage2")
.attr("fill-opacity", 1)
.duration(duration);
ends.transition("playStage2")
.attr("fill-opacity", 1)
.duration(duration);
//Prepare animation for the next stage: add an 'executed-body' on each transition
var executedBodies = drawnTransitions.append("line")
.attr("class", "executed-body")
.attr({x1: 0, y1: 0, x2: 0, y2: 0})
.style("stroke", 'grey');
}
function _rewindStage2() {
//interrupt stage2's transitions (running and scheduled)
svg.selectAll('.drawn-transition')
.transition("playStage2")
.duration(0)
//rewind to stage2's initial state
svg.selectAll(".drawn-transition")
.transition("rewindStage2")
.style("fill-opacity", 0)
.style("stroke-opacity", 0)
.remove();
}
function _playStage3() {
//Run each transition, move circles down
svg.selectAll('circle')
.transition("playStage3")
.attr('cy', function(d, i) { return circleMovingIncrement*(i+1); })
.delay(function(d, i) { return delay*(i); })
.duration(function(d, i) { return duration*(i+1); })
.ease('linear')
//Create transitions for each drawn-transition.
//Graphically speaking, those transitions does nothing
//Those transitions allow synchronization between sub-transitions that applie on sub-elements of each drawn-transition
var drawnTransitionAnimations = svg.selectAll(".drawn-transition")
.transition("playStage3")
.delay(function(d, i) { return delay*i; })
.duration(function(d, i) { return duration*(i+1); })
//Run each transition, make the body of each drawn-transition 'solid' (instead of being dashed)
drawnTransitionAnimations.each(function (d, i) {
d3.select(this).select(".executed-body")
.transition("playStage3")
.attr("y2", circleMovingIncrement*(i+1))
.ease('linear')
})
drawnTransitionAnimations.each(function (d, i) {
d3.select(this).select(".body")
.transition("playStage3")
.attr("y1", circleMovingIncrement*(i+1))
.ease('linear')
})
//Run each transition, highlight each start-time
drawnTransitionAnimations.each('start', function () {
d3.select(this).select('.start-time')
.attr("stroke-opacity", 1)
.transition("playStage3")
.attr("stroke-opacity", 0)
})
//Run each transition, highlight each end-time
drawnTransitionAnimations.each('end', function () {
d3.select(this).select('.end-time')
.attr("stroke-opacity", 1)
.transition("playStage3")
.attr("stroke-opacity", 0)
})
}
function _rewindStage3() {
//interrupt stage3's transitions (running and scheduled)
svg.selectAll('circle')
.transition("playStage3")
.duration(0)
svg.selectAll('.drawn-transition')
.transition("playStage3")
.duration(0)
svg.selectAll('.executed-body')
.transition("playStage3")
.duration(0)
svg.selectAll('.body')
.transition("playStage3")
.duration(0)
svg.selectAll('.start-time')
.transition("playStage3")
.duration(0)
svg.selectAll('.end-time')
.transition("playStage3")
.duration(0)
//rewind to stage3's initial state
svg.selectAll('circle')
.transition("rewindStage3")
.attr('cy', 0);
svg.selectAll(".executed-body")
.transition("rewindStage3")
.attr('y2', 0);
svg.selectAll(".body")
.transition("rewindStage3")
.attr('y1', 0);
svg.selectAll(".start-time")
.transition("rewindStage3")
.attr('stroke-opacity', 0);
svg.selectAll(".end-time")
.transition("rewindStage3")
.attr('stroke-opacity', 0);
}
function _goToStage(n) {
switch (parseInt(n)) {
case 0:
_rewindStage3();
_rewindStage2();
_rewindStage1();
_playStage0();
break;
case 1:
_rewindStage3();
_rewindStage2();
_playStage0();
_playStage1();
break;
case 2:
_rewindStage3();
_playStage0();
_playStage1();
_playStage2();
break;
case 3:
_playStage0();
_playStage1();
_playStage2();
_playStage3();
break;
}
_updateExplanations(n);
}
function _makeExplanations() {
return [
{
stageIndex: 0,
explanation: "<== Choose a line of code for more explanations."
},
{
stageIndex: 1,
explanation: "<em>d3.selectAll(...)</em> selects several elements. In this example, it selects the " + circleCount + " circles."
},
{
stageIndex: 2,
explanation: "<b>selectAll(...).transition() schedules<sup>*</sup> SEVERAL transitions</b><br> As explained in <a href='http://bost.ocks.org/mike/transition/#per-element' target='_blank'>Transitions Are per-Element and Exclusive</a>, <em>selectAll(...).transition()</em> schedules 1 transition per selected element. This example schedules " + circleCount + " transitions, one per circle. Each transition has its own <em>delay</em>, <em>duration</em>, and end value of the <em>cy</em> attribute. <em>delay</em> and <em>duration</em> allows to derive the start time and the end time of a transition.<br><br><sup>*</sup>In the D3 world, <em>scheduling</em> a transition means <em>defining</em> a transition, ie. setting its properties."
},
{
stageIndex: 3,
explanation: "As explained in <a href='http://bost.ocks.org/mike/transition/#life-cycle' target='_blank'>The Life of a Transition</a>, when the scheduling of transitions is complete, each transition waits until it can start, then runs, and then stops.<br><br><b>Each transition runs independantly</b><br>When a transition stops or is interupted, this has no side-effect on other transitions, even on sibling transitions (ie. defined by the same JavaScript lines of code). As a proof, note that when one of the transitions of this example stops, others transitions to the right still continue to run!<br> Synchronization between transitions comes with identical delays and/or durations."
}
]
}
function _updateExplanations (index) {
var explanation = explanations[index].explanation;
//update explanations
explanationArea.transition()
.style("opacity", 0)
.each('end', function() {
explanationArea.html(explanation);
})
.transition()
.style("opacity", 1);
}
{
"name": "world",
"children": [
{
"name": "Asia",
"color": "#f58321",
"children": [
{"name": "China", "weight": 14.84, "code": "CN"},
{"name": "Japan", "weight": 5.91, "code": "JP"},
{"name": "India", "weight": 2.83, "code": "IN"},
{"name": "South Korea", "weight": 1.86, "code": "KR"},
{"name": "Russia", "weight": 1.8, "code": "RU"},
{"name": "Indonesia", "weight": 1.16, "code": "ID"},
{"name": "Turkey", "weight": 0.97, "code": "TR"},
{"name": "Saudi Arabia", "weight": 0.87, "code": "SA"},
{"name": "Iran", "weight": 0.57, "code": "IR"},
{"name": "Thaïland", "weight": 0.53, "code": "TH"},
{"name": "United Arab Emirates", "weight": 0.5, "code": "AE"},
{"name": "Hong Kong", "weight": 0.42, "code": "HK"},
{"name": "Israel", "weight": 0.4, "code": "IL"},
{"name": "Malasya", "weight": 0.4, "code": "MY"},
{"name": "Singapore", "weight": 0.39, "code": "SG"},
{"name": "Philippines", "weight": 0.39, "code": "PH"}
]
},
{
"name": "North America",
"color": "#ef1621",
"children": [
{"name": "United States", "weight": 24.32, "code": "US"},
{"name": "Canada", "weight": 2.09, "code": "CA"},
{"name": "Mexico", "weight": 1.54, "code": "MX"}
]
},
{
"name": "Europe",
"color": "#77bc45",
"children": [
{"name": "Germany", "weight": 4.54, "code": "DE"},
{"name": "United Kingdom", "weight": 3.85, "code": "UK"},
{"name": "France", "weight": 3.26, "code": "FR"},
{"name": "Italy", "weight": 2.46, "code": "IT"},
{"name": "Spain", "weight": 1.62, "code": "ES"},
{"name": "Netherlands", "weight": 1.01, "code": "NL"},
{"name": "Switzerland", "weight": 0.9, "code": "CH"},
{"name": "Sweden", "weight": 0.67, "code": "SE"},
{"name": "Poland", "weight": 0.64, "code": "PL"},
{"name": "Belgium", "weight": 0.61, "code": "BE"},
{"name": "Norway", "weight": 0.52, "code": "NO"},
{"name": "Austria", "weight": 0.51, "code": "AT"},
{"name": "Denmark", "weight": 0.4, "code": "DK"},
{"name": "Ireland", "weight": 0.38, "code": "IE"}
]
},
{
"name": "South America",
"color": "#4aaaea",
"children": [
{"name": "Brazil", "weight": 2.39, "code": "BR"},
{"name": "Argentina", "weight": 0.79, "code": "AR"},
{"name": "Venezuela", "weight": 0.5, "code": "VE"},
{"name": "Colombia", "weight": 0.39, "code": "CO"}
]
},
{
"name": "Australia",
"color": "#00acad",
"children": [
{"name": "Australia", "weight": 1.81, "code": "AU"}
]
},
{
"name": "Africa",
"color": "#f575a3",
"children": [
{"name": "Nigeria", "weight": 0.65, "code": "NG"},
{"name": "Egypt", "weight": 0.45, "code": "EG"},
{"name": "South Africa", "weight": 0.42, "code": "ZA"}
]
},
{"name": "Rest of the World",
"color": "#592c94",
"children": [
{"name": "Rest of the World", "weight": 9.41, "code": "RotW"}
]
}
]
}
continent 1950 1955 1960 1965 1970 1975 1980 1985 1990 1995 2000 2005 2010 2015 2020 2025 2030 2035 2040 2045 2050 2055 2060 2065 2070 2075 2080 2085 2090 2095 2100 color
Oceania 10 10 20 20 20 20 20 20 30 30 30 30 40 40 40 50 50 50 50 50 60 60 60 60 60 70 70 70 70 70 70 #a5a190
Northern America 170 190 200 220 230 240 250 270 280 300 320 330 350 360 370 380 400 410 420 430 430 440 450 460 470 470 480 490 490 500 500 #ea9439
Latin America & Caribbean 170 190 220 250 290 320 360 400 450 390 530 560 600 630 660 690 720 740 760 770 780 790 790 790 780 770 760 750 740 730 710 #d85a44
Europe 550 580 610 640 660 680 690 710 720 730 730 730 740 740 740 740 740 730 730 720 720 710 700 690 680 670 670 660 660 660 650 #3884a9
Africa 230 250 290 320 370 420 480 550 630 720 810 910 1030 1190 1350 1520 1700 1900 2100 2310 2530 2750 2960 3180 3390 3600 3800 3990 4160 4320 4470 #539344
Asia 1400 1540 1690 1880 2130 2390 2630 2910 3210 3480 3720 3940 4170 4420 4620 4800 4950 5060 5150 5220 5260 5270 5260 5230 5190 5130 5070 5000 4930 4860 4780 #fcd25b
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>D3.selectAll(...).transition() Explained</title>
<meta content="Explaining D3.selectAll(...).transition() behaviour" name="description">
</head>
<body>
<link rel="stylesheet" href="d3_selectAll_transition_explained.css">
<div class='top-container'>
<form action="">
<input type="radio" name="step" onclick="_goToStage(this.value)" value=0 checked><span class="comment">start</span><br>
<input type="radio" name="step" onclick="_goToStage(this.value)" value=1><span class="space-2">d3.selectAll("circle")</span><br>
<input type="radio" name="step" onclick="_goToStage(this.value)" value=2><span class="space-4">.transition()</span><br>
<input type="radio" name="step" style="visibility:hidden"><span class="space-6">.attr("delay", func(d,i){return 1000*i})</span><br>
<input type="radio" name="step" style="visibility:hidden"><span class="space-6">.attr("duration", func(d,i){return 1000*(i+1)})</span><br>
<input type="radio" name="step" style="visibility:hidden"><span class="space-6">.attr("cy", func(d,i){return 30*(i+1)})</span><br>
<input type="radio" name="step" onclick="_goToStage(this.value)" value=3><span class="comment">end</span>
</form>
<div class="explanation-area">&nbsp;</div>
</div>
<hr/>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="d3_selectAll_transition_explained.js"></script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment