1

I have a stacked area graph that animates when the data changes.

The data shows values for a given date range (e.g., Dec. 10-15). When I add expand my date range to include an earlier date (e.g., Dec. 9-15), the data array has additional data prepended to the front.

When the paths animate though, the effect is very weird and undesirable.

See this jsfiddle showing the effect (click the Update button): http://jsfiddle.net/qprn9ta9/

This seems to be because of the fact that I am adding new points to the front of the SVG area, and the interpolation is animating them around based on the index.

How can I avoid this? Is there something built into D3 that can help? I looked for some sort of key function like you use with data(), but didn't find anything similar. Any other ideas?

MindJuice
  • 4,121
  • 3
  • 29
  • 41

3 Answers3

0

If you need to add points to one of your dataset0, but dataset1 did not change, add a "spacer value", repeating the last value of dataset1.

The array lengths need to match, or else the animation will be glitchy, because the markup structure of the SVG needs to change to accomodate the data, you wont be changing only positions over time.

Before (different lengths for datasets):

test_data0 = [{"0": 0.1, "1": 0.1}, {"0": 0.2, "1": 0.6}, {"0": 0.3, "1": 0.4}, {"0": 0.1, "1": 0.6}, {"0": 0.3, "1": 0.1}, {"0": 0.0, "1": 0.3}, {"0": 0.3, "1": 0.1}, {"0": 0.3, "1": 0.2}, {"0": 0.2, "1": 0.3}]
test_data1 = [{"0": 0.2, "1": 0.2}, {"0": 0.0, "1": 0.0}, {"0": 0.2, "1": 0.6}, {"0": 0.3, "1": 0.4}, {"0": 0.1, "1": 0.6}, {"0": 0.3, "1": 0.1}, {"0": 0.0, "1": 0.3}, {"0": 0.3, "1": 0.1}, {"0": 0.3, "1": 0.2}, {"0": 0.2, "1": 0.3}]

After (same length for two datasets):

test_data0 = [{"0": 0.1, "1": 0.1}, {"0": 0.2, "1": 0.6}, {"0": 0.3, "1": 0.4}, {"0": 0.1, "1": 0.6}, {"0": 0.3, "1": 0.1}, {"0": 0.0, "1": 0.3}, {"0": 0.3, "1": 0.1}, {"0": 0.3, "1": 0.2}, {"0": 0.2, "1": 0.3}, {"0": 0.2, "1": 0.3}]
test_data1 = [{"0": 0.2, "1": 0.2}, {"0": 0.0, "1": 0.0}, {"0": 0.2, "1": 0.6}, {"0": 0.3, "1": 0.4}, {"0": 0.1, "1": 0.6}, {"0": 0.3, "1": 0.1}, {"0": 0.0, "1": 0.3}, {"0": 0.3, "1": 0.1}, {"0": 0.3, "1": 0.2}, {"0": 0.2, "1": 0.3}]

So, the solution forces you to treat your datasets with more care in advance.

jsfiddle:

http://jsfiddle.net/2o372wu3/

Community
  • 1
  • 1
caulitomaz
  • 2,141
  • 14
  • 20
  • Sorry if I wasn't clear, but both datasets are getting additional values. The dataset0 is at time0 and dataset1 is at time1. They are intentionally different lengths, because that is where the problem lies. I'm trying to show more data. – MindJuice Jan 06 '16 at 18:34
  • By dataset, I mean "0" and "1" in the data arrays. – MindJuice Jan 06 '16 at 18:36
  • What is your desired behavior for the animation? "All elements before the new elements compresses on the X axis, and the new elements animate from Y=0 to Y=final"? – caulitomaz Jan 06 '16 at 18:41
  • That would be much better than the current behavior. Would probably look better if the new value started out at Y=y_value_of_previous_first_element. – MindJuice Jan 06 '16 at 18:44
  • I'm thinking that I need to add a duplicate first point, draw the graph immediately, then draw it again with the new first point, but with a transition. – MindJuice Jan 06 '16 at 18:45
  • Maybe it will be less prone to SVG glitching if you actually stack two separated `area-chart`s . I'm sure there is some example of adding dynamic animated values to area-charts somewhere on http://bl.ocks.org/mbostock . – caulitomaz Jan 06 '16 at 18:50
  • You would have the same problem even with a single area chart (and in fact I do have the same problem in another simpler area chart). – MindJuice Jan 06 '16 at 18:52
  • Also, probably overkill solution: Get new dataset (dataset1). Interpolate values of dataset0 to match length of new dataset1 (only works if elements were added, not removed). We'll call this dataset2. Change graph from dataset0 to dataset2 without animating. Change from dataset2 to dataset1 by animating. – caulitomaz Jan 06 '16 at 18:54
  • Yeah, that's what I was getting at above with the "draw it again" comment. Also, when the new data set has fewer points, I would need to do some other jiggery pokery. I suspect something along these lines will be necessary though. – MindJuice Jan 06 '16 at 22:02
  • Punt! I decided to just not animate in the case where the number of data points changes. It still animates in the case where there are the same number of data points as last time though. – MindJuice Jan 07 '16 at 00:21
  • Cool! Curious if someone comes up with an elegant solution to this problem. – caulitomaz Jan 07 '16 at 00:29
0

So, this question has been driving me nuts all night. You are correct that the addition of new points to the front of the data throws off d3s interpolateString method when doing from one path to another. The best (hacky) fix I can come up with is to insert a point in the path before the transition starts:

// enter
var mp = svg.selectAll("path")
    .data(layers);

mp
    .enter().append("path")
    .style("fill", function(d) { return d.color; });

//update
mp
  .transition()
  .duration(2000)
  .tween("path", function(d) {
    var self = d3.select(this),
        cP = self.attr("d"),
        i = null;
    if (!cP){
      i = d3.interpolateString("", area(d.layer));
    } else {
        var idx = cP.indexOf("L"),
          fP = cP.substring(0, idx),
          lP = cP.substring(idx);          
                cP = fP + "L" + fP.slice(1) + lP;          
        i = d3.interpolateString(cP, area(d.layer));
    }
    return function(t) {
            self.attr("d", i(t));
    };
  });

Full code:

test_data0 = [{"0": 0.1, "1": 0.1}, {"0": 0.2, "1": 0.6}, {"0": 0.3, "1": 0.4}, {"0": 0.1, "1": 0.6}, {"0": 0.3, "1": 0.1}, {"0": 0.0, "1": 0.3}, {"0": 0.3, "1": 0.1}, {"0": 0.3, "1": 0.2}, {"0": 0.2, "1": 0.3}]
test_data1 = [{"0": 0.2, "1": 0.2}, {"0": 0.1, "1": 0.1}, {"0": 0.2, "1": 0.6}, {"0": 0.3, "1": 0.4}, {"0": 0.1, "1": 0.6}, {"0": 0.3, "1": 0.1}, {"0": 0.0, "1": 0.3}, {"0": 0.3, "1": 0.1}, {"0": 0.3, "1": 0.2}, {"0": 0.2, "1": 0.3}]

$('#update').click(function(){
    console.log('test')
    streamed_history(test_data1)
});
var width = 300,
    height = 200,
    colors = {'0': '#6ff500', '1': '#ffad0a'},
    feedbacks = [0, 1],
    stack = d3.layout.stack();
var svg = d3.select("#timeline").append("svg")
    .attr("width", width)
    .attr("height", height);
var y = d3.scale.linear()
    .domain([0, 1])
    .range([height, 0]);

streamed_history(test_data0)

function streamed_history(data) {
    data_array = feedbacks.map(function (f) {
        return data.map(function(element, i) { return {x: i, y: element[f]}; })
    }),
    layers = stack(data_array)
    layers = feedbacks.map(function (f, i) {
        return {layer: layers[i], feedback: f, color: colors[f]}
    })

    var x = d3.scale.linear()
        .domain([0, data.length - 1])
        .range([0, width]);

    var area = d3.svg.area()
        .x(function(d) { return x(d.x); })
        .y0(function(d) { return y(d.y0); })
        .y1(function(d) { return y(d.y0 + d.y); });
        
    //enter
    var mp = svg.selectAll("path")
        .data(layers);
        
    mp
        .enter().append("path")
        .style("fill", function(d) { return d.color; });

    //update
    mp
      .transition()
      .duration(2000)
      .tween("path", function(d) {
        var self = d3.select(this),
            cP = self.attr("d"),
            i = null;
        if (!cP){
          i = d3.interpolateString("", area(d.layer));
        } else {
         var idx = cP.indexOf("L"),
              fP = cP.substring(0, idx),
              lP = cP.substring(idx);          
     cP = fP + "L" + fP.slice(1) + lP;          
         i = d3.interpolateString(cP, area(d.layer));
        }
        return function(t) {
          self.attr("d", i(t));
        };
      });

}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="timeline"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<button id='update'>Update</button>
Mark
  • 106,305
  • 20
  • 172
  • 230
  • Thanks for the effort here Mark! I think the ideal animation (from my perspective) would be to have the new points slide in from left to right (as if they were just off to the left of the vertical axis line), and the remaining points would move over and compress into the remaining space. There is also the case of going the other way when you have fewer points. In that case, the graph could shift from right to left as the remaining points expanded to fill the space. – MindJuice Jan 14 '16 at 18:46
0

So, as it so often turns out, Mike Bostock has already addressed this issue here:

https://bost.ocks.org/mike/path/

He only illustrates the case of appending to the end, but the case of prepending can be done with some tweaks similar to the following:

// push a new data point onto the front
data.unshift(random());

// redraw the line shifted to the left by one data point, and then slide it to the right
path
    .attr("d", line)
    .attr("transform", 'translate(" + x(-1) + ")')
  .transition()
    .attr("transform", null);

// pop the old data point off the back
data.pop();
MindJuice
  • 4,121
  • 3
  • 29
  • 41