block by Kcnarf 9e4813ba03ef34beac6e

D3.selectAll(...).transition() Explained

Full Screen

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:

This sequel also illustrates:

This sequel uses D3 v3.5.5.

If you look at the code, you will see that:

index.html

<!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>

d3_selectAll_transition_explained.css

.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;
  }

d3_selectAll_transition_explained.js

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);
}

globalEconomyByGDP.json

{
  "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"}
      ]
    }
  ]
}

globalPopulationByRegionUntil2100.csv

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