d3 v4 line chart transition not working Ask Question

I would like my line to draw like this example:

https://bl.ocks.org/shimizu/f7ef798894427a99efe5e173e003260d

The code below does not make any transitions, the chart just appears.

I’m aware of browser caching and that is not the issue. I’ve also tried changing the duration and that doesn’t help either. I feel like I’m probably not being explicit about how I want d3 to transition, but I’m unsure how to give d3 what it wants. Your help is greatly appreciated.

EDIT: x-axis domain: [0, 1]. y-axis domain: [-18600, -3300].

// Here's just a few rows of the data
data = [{"threshold": 0.0, "loss": -18600},
        {"threshold": 0.008571428571428572, "loss": -18600},
        {"threshold": 0.017142857142857144, "loss": -18600}]

var svg = d3.select("svg"),
    margin = {top: 20, right: 20, bottom: 30, left: 20},
    width = +svg.attr("width") - 400 - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom;


var x = d3.scaleLinear()
    .range([0, width]);

var y = d3.scaleLinear()
    .range([0, height]);

var line = d3.line()
    .x(d => x(d.threshold))
    .y(d => y(d.loss));

var g = svg.append("g")
    .attr("transform", "translate(" + (margin.left + 50) + "," + margin.top + ")");

d3.json("static/data/thresh_losses.json", function(thisData) {
 draw(thisData);
});

let draw = function(data) {
    $("svg").empty()
    var x = d3.scaleLinear()
        .range([0, width]);

    var y = d3.scaleLinear()
        .range([0, height]);

    var line = d3.line()
        .x(d => x(d.threshold))
        .y(d => y(d.loss));

    var g = svg.append("g")
        .attr("transform", "translate(" + (margin.left + 50) + "," + margin.top + ")");

    d3.selectAll("g").transition().duration(3000).ease(d3.easeLinear);

    x.domain([0, d3.max(data, d => d.threshold)]);
    y.domain([d3.max(data, d => d.loss), d3.min(data, d => d.loss)]);

    g.append("g")
        .attr("class", "axis axis--x")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x))
        .append("text")
        .attr("class", "axis-title")
        .attr("y", 18)
        .attr("dy", "1em")
        .attr("x", (height/2) - 40)
        .attr("dx", "1em")
        .style("text-anchor", "start")
        .attr("fill", "#5D6971")
        .text("Threshold");

    g.append("g")
        .attr("class", "axis axis--y")
        .call(d3.axisLeft(y))
        .append("text")
       .attr("class", "axis-title")
       .attr("transform", "rotate(-90)")
       .attr("y", -40)
       .attr("dy", ".71em")
       .attr("x", -height/2 + 40)
       .attr("dx", ".71em")
       .style("text-anchor", "end")
       .attr("fill", "#5D6971")
       .text("Profit ($)");

    var line_stuff = g.selectAll(".line")
        .data([data]);

    line_stuff.enter().append("path").classed("line", true)
           .merge(line_stuff);

    g.selectAll(".line")
      .transition()
      .duration(10000)
      .ease(d3.easeLinear)
      .attr("d", line);
};

One thought on “d3 v4 line chart transition not working Ask Question”

  1. From the D3 documentation:

    To apply a transition, select elements, call selection.transition, and then make the desired changes.

    I found this in the code:

    d3.selectAll("g").transition().duration(3000).ease(d3.easeLinear);
    

    This won’t animate anything, because there’s no .attr() or .style() at the end—no “desired changes” are being made. It’s a transition with no changes to make.

    Now, let’s look at this:

    g.selectAll(".line")
      .transition()
      .duration(10000)
      .ease(d3.easeLinear)
      .attr("d", line);
    

    This almost fulfills the requirements. It selects .line, creates the transition (and customizes it), and sets the d attribute. If you have d set elsewhere, then this would to transition the path from being empty to having all the data, only…

    D3 doesn’t transition strings that way. After first checking if the attribute is a number or color, D3 settles on using something called interpolateString. You’d think interpolateString would change characters from a to ab to abc, but actually, all it does is look for numbers within the string, and interpolate those, leaving the rest of the string constant. The upshot is, you just can’t animate a string like d from empty to having data unless you do it yourself.

    Here’s how you can do that, using attrTween (note: not a good idea):

    .attrTween("d", function() {
      return function(t) {
        const l = line(data);
        return l.substring(0, Math.ceil(l.length * t));
      };
    })
    

    This will actually transition between no text to the entire text of the d attribute. However, because of the way SVG paths work, this doesn’t look very good.

    There is another way, as demonstrated in the example you linked to (and also mentioned by Ryan Morton in a comment): transitioning the stroke-dashoffset. Here’s how you would do that:

    line_stuff.enter().append("path").classed("line", true)
      .merge(line_stuff)
      .attr('d', line)
      .attr("fill", "none")
      .attr("stroke", "black")
      .attr("stroke-dasharray", function(d) {
        return this.getTotalLength()
      })
      .attr("stroke-dashoffset", function(d) {
        return this.getTotalLength()
      });
    
    g.selectAll(".line")
      .transition()
      .duration(10000)
      .ease(d3.easeLinear)
      .attr("stroke-dashoffset", 0);
    

    Essentially, the first part tells D3 to:

    • create the line, make the fill invisible (so you can see the line)
    • make the stroke dashes equal to the total length of the line
    • offset the dashes, so that the line is completely hidden at the start

    The next part sets up the transition and tells it to transition the offset to 0 (at which point the line will be completely visible because each dash is the same length as the line itself).

    If you want to transition the fill, you could change .attr("fill", "none") to .attr("fill", "#fff"), and then do something like this:

    g.selectAll(".line")
      .transition()
      .delay(10000)
      .duration(2000)
      .ease(d3.easeLinear)
      .attr('fill', '#000');
    

    This would use .delay() to wait for the first transition to finish before changing the background from white to black. Note that opacity might be better to animate for performance.

Leave a Reply

Your email address will not be published. Required fields are marked *