Search code examples
javascriptd3.jstransitionupdating

How to transition multiple lines in d3.js plot?


I have been trying to adapt the chained transition script of Mike Bostock to work with multiple lines but I do not get it to work. After the first display the lines and labels fly out of the plot and do not show anymore Whereas everything gets updated (I can see the values of the lines changing when inspecting the javascript console) . I do not understand what I am doing wrong. I will post the (lengthy) code here below (apologies for the length). I would appreciate any help, thank you!

<!DOCTYPE html>
<head>
<title>Modified Chained Transitions</title>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v3.min.js"></script>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  margin: auto;
  position: relative;
  width: 960px;
}

text {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}

form {
  position: absolute;
  right: 10px;
  top: 10px;
}

</style>
</head>
<body>
<br>
  <button type="button"> Request data</button>

  <div id='chart'> </div>
</body>
<script>

var margin = {top: 20, right: 80, bottom: 30, left: 50},
    width = 750 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var parseDate = d3.time.format("%Y%m%d").parse;

var xScale = d3.time.scale()
    .range([0, width]);

var yScale = d3.scale.linear()
    .range([height, 0]);

var color = d3.scale.category10();

var xAxis = d3.svg.axis()
    .scale(xScale)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(yScale)
    .orient("left");

var line = d3.svg.line()
                .interpolate("basis")
                .x(function(d) { return xScale(d.date); })
                .y(function(d) { return yScale(d.temperature); });

var svg = d3.select("body").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 + ")");


var getNewData = function() {
    var data = [];
    var counter = 0;
  function generate(){
        var startDate = new Date;
      counter += 1;
        var range = counter % 2 === 0 ? 10 : 100; 
        for (i = 0; i < 100; i++) {
            data[i] = {"date": new Date(startDate - i),
                "New York": Math.random() * (range - 1), 
                "San Francisco": Math.random() * (range - 1),
                "Austin": Math.random() * (range - 10)};
        }
        return data;
    } 
    return {
        new: function () {return generate()}
    };
}; // function getNewData() 

var newData = getNewData();
data = newData.new();

color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));

var cities = color.domain().map(function(name) {
  return {
    name: name,
    values: data.map(function(d) {
      return { date: d.date, temperature: +d[name]};
    })
  };
});

xScale.domain(d3.extent(data, function(d) { return d.date; }));
yScale.domain([
  d3.min(cities, function(c) {
    return d3.min(c.values, function(v) { return v.temperature; }); }),
  d3.max(cities, function(c) {
    return d3.max(c.values, function(v) { return v.temperature; }); })
]);

svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)
    .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Temperature (ºF)");

var city = svg.selectAll(".city")
    .data(cities)
    .enter().append("g")
    .attr("class", "city");

city.append("path")
    .attr("class", "line")
    .attr("d", function(d) { return line(d.values); })
    .style("stroke", function(d) { return color(d.name); });

city.append("text")
    .datum(function(d) { return {name: d.name, values: d.values[0]}; })
        .attr("class", "label")
    .attr("transform", function(d) { return "translate(" +
      xScale(d.values.date) +  "," + yScale(d.values.temperature) + ")"; })
    .attr("x", 3)
    .attr("dy", ".35em")
    .text(function(d) { return d.name; });

d3.selectAll("button").on("click", change);

function change() {

    data = newData.new();
    color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));
    cities = color.domain().map(function(name) {
      return {
        name: name,
        values: data.map(function(d) {
          return { date: d.date, temperature: +d[name]};
        })
      };
    });
    console.log(cities[0].values[0]);

    xScale.domain(d3.extent(data, function(d) { return d.date; }));
    yScale.domain([
      d3.min(cities, function(c) {
        return d3.min(c.values, function(v) { return v.temperature; }); }),
      d3.max(cities, function(c) {
        return d3.max(c.values, function(v) { return v.temperature; }); })
    ]);

    var t0 = svg.transition().duration(750);
    t0.selectAll(".line")
        .attr("d", function(cities) { return line(cities.values); })
        .style("stroke", function(cities) { return color(cities.name); });
    t0.selectAll(".label").attr("transform", 
         "translate(0,0)").text(function(cities) { return cities.name; });


    var t1 = t0.transition();
//  t1.selectAll(".line").attr("d", line(data));
//  t1.select(".line")
    t1.selectAll(".line")
//      t1.selectAll(".city")
        .attr("d", function(cities) { return line(cities.values); })
        .style("stroke", function(cities) { return color(cities.name); });
    t1.select(".y.axis").call(yAxis);
    t1.select(".x.axis").call(xAxis);
    t1.select(".label")
        .attr("transform", function(d) { return "translate(" +
      xScale(d.values.date) +  "," + 
            yScale(d.values.temperature) + ")"; });
} // function change() 

</script>
</html>

Solution

  • I can help fix your transitions but I'm not sure what you are attempting to "chain". In the linked example, Bostock swaps one line for another (transition 1), then fits that line to a new domain (transition 2). You do not seem to want to swap lines, so you fit a new domain and then transition the lines to it (transition 1) but what's transition 2?

    Now to answer your more direct question of why your transitions aren't working, it's simply because you never update your data. In the linked example Bostock has both datasets bound to his line and then swaps which he's drawing in the line function. You, though, only ever have the original dataset bound. Quick fix is:

    function change() {
    
      ... //<-- get new data
    
      // bind your new data
      var cities = svg.selectAll(".city")
        .data(cities)
    
      // sub selection to transition line   
      cities
        .select(".line")
        .transition()
        .duration(750)
        .attr("d", function(d) { return line(d.values); })
        .style("stroke", function(d) { return color(d.name); })
    
      // concurrent sub selection to move labels
      cities
        .select(".label")
        .transition()
        .duration(750)
        .attr("transform", function(d){
           var last = d.values[0];
           return "translate(" + xScale(last.date) + "," +   yScale(last.temperature) + ")";
        })
    
    }
    

    Running code:

    <!DOCTYPE html>
    
    <head>
      <title>Modified Chained Transitions</title>
      <meta charset="utf-8">
      <script src="https://d3js.org/d3.v3.min.js"></script>
      <style>
        body {
          font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
          margin: auto;
          position: relative;
          width: 960px;
        }
        
        text {
          font: 10px sans-serif;
        }
        
        .axis path,
        .axis line {
          fill: none;
          stroke: #000;
          shape-rendering: crispEdges;
        }
        
        .x.axis path {
          display: none;
        }
        
        .line {
          fill: none;
          stroke: steelblue;
          stroke-width: 1.5px;
        }
        
        form {
          position: absolute;
          right: 10px;
          top: 10px;
        }
      </style>
    </head>
    
    <body>
      <br>
      <button type="button"> Request data</button>
    
      <div id='chart'> </div>
    </body>
    <script>
      var margin = {
          top: 20,
          right: 80,
          bottom: 30,
          left: 50
        },
        width = 500 - margin.left - margin.right,
        height = 500 - margin.top - margin.bottom;
    
      var parseDate = d3.time.format("%Y%m%d").parse;
    
      var xScale = d3.time.scale()
        .range([0, width]);
    
      var yScale = d3.scale.linear()
        .range([height, 0]);
    
      var color = d3.scale.category10();
    
      var xAxis = d3.svg.axis()
        .scale(xScale)
        .orient("bottom");
    
      var yAxis = d3.svg.axis()
        .scale(yScale)
        .orient("left");
    
      var line = d3.svg.line()
        .interpolate("basis")
        .x(function(d) {
          return xScale(d.date);
        })
        .y(function(d) {
          return yScale(d.temperature);
        });
    
      var svg = d3.select("body").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 + ")");
    
    
      var getNewData = function() {
        var data = [];
        var counter = 0;
    
        function generate() {
          var startDate = new Date;
          counter += 1;
          var range = counter % 2 === 0 ? 10 : 100;
          for (i = 0; i < 100; i++) {
            data[i] = {
              "date": new Date(startDate - i),
              "New York": Math.random() * (range - 1),
              "San Francisco": Math.random() * (range - 1),
              "Austin": Math.random() * (range - 10)
            };
          }
          return data;
        }
        return {
          new: function() {
            return generate()
          }
        };
      }; // function getNewData() 
    
      var newData = getNewData();
      data = newData.new();
    
      color.domain(d3.keys(data[0]).filter(function(key) {
        return key !== "date";
      }));
    
      var cities = color.domain().map(function(name) {
        return {
          name: name,
          values: data.map(function(d) {
            return {
              date: d.date,
              temperature: +d[name]
            };
          })
        };
      });
    
      xScale.domain(d3.extent(data, function(d) {
        return d.date;
      }));
      yScale.domain([
        d3.min(cities, function(c) {
          return d3.min(c.values, function(v) {
            return v.temperature;
          });
        }),
        d3.max(cities, function(c) {
          return d3.max(c.values, function(v) {
            return v.temperature;
          });
        })
      ]);
    
      svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);
    
      svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Temperature (ºF)");
    
      var city = svg.selectAll(".city")
        .data(cities)
        .enter().append("g")
        .attr("class", "city");
    
      city.append("path")
        .attr("class", "line")
        .attr("d", function(d) {
          return line(d.values);
        })
        .style("stroke", function(d) {
          return color(d.name);
        });
    
      city.append("text")
        .datum(function(d) {
          return {
            name: d.name,
            values: d.values[0]
          };
        })
        .attr("class", "label")
        .attr("transform", function(d) {
          return "translate(" +
            xScale(d.values.date) + "," + yScale(d.values.temperature) + ")";
        })
        .attr("x", 3)
        .attr("dy", ".35em")
        .text(function(d) {
          return d.name;
        });
    
      d3.selectAll("button").on("click", change);
    
      function change() {
    
        data = newData.new();
        color.domain(d3.keys(data[0]).filter(function(key) {
          return key !== "date";
        }));
        cities = color.domain().map(function(name) {
          return {
            name: name,
            values: data.map(function(d) {
              return {
                date: d.date,
                temperature: +d[name]
              };
            })
          };
        });
    
        xScale.domain(d3.extent(data, function(d) {
          return d.date;
        }));
    
        yScale.domain([
          d3.min(cities, function(c) {
            return d3.min(c.values, function(v) {
              return v.temperature;
            });
          }),
          d3.max(cities, function(c) {
            return d3.max(c.values, function(v) {
              return v.temperature;
            });
          })
        ]);
    
        var cities = svg.selectAll(".city")
          .data(cities)
    
        cities
          .select(".line")
          .transition()
          .duration(750)
          .attr("d", function(d) {
            return line(d.values);
          })
          .style("stroke", function(d) {
            return color(d.name);
          })
    
        cities
          .select(".label")
          .transition()
          .duration(750)
          .attr("transform", function(d) {
            var last = d.values[0];
            return "translate(" + xScale(last.date) + "," + yScale(last.temperature) + ")";
          })
    
        svg.selectAll(".y.axis")
          .transition()
          .duration(750)
          .call(yAxis);
    
        svg.selectAll(".x.axis")
          .transition()
          .duration(750)
          .call(xAxis);
    
      } // function change()
    </script>
    
    </html>