0

Mike Bostock has great example of a multi-series linechart here, which draws three series using compact code. Rather than drawing each line individually from the raw data, he creates a new data object ("cities") to iterate over, then draws all three path elements in one go:

var city = svg.selectAll(".city")
  .data(cities)
.enter().append("g")

[... etc] But suppose we wanted to animate this chart, so that each city's line "unrolled" one after another? Is such a thing possible, using Bostock's data structure?

First we'd create a tween function to unroll the lines smoothly (as demonstrated in this question). Something like:

function animateLine() {
    var l = this.getTotalLength();
    i = d3.interpolateString("0," + l, l + "," + l);
    return function(t) { return i(t); };
};

But if we invoked this animation as a transition in Mike Bostock's line-drawing code, wouldn't it just unroll all three lines simultaneously, rather than one after another?

I can see a hacky solution, which would be to define three separate (but basically identical) functions to draw each line one at a time, based on some lightly modified raw data...

var AustinLine = d3.svg.line()
            .x(function(d) { return x(d.date); })
            .y(function(d) { return y(d.austin); });

var NewYorkLine = d3.svg.line()
            .x(function(d) { return x(d.date); })
            .y(function(d) { return y(d.newyork); });

... [etc.] called in three separate line-drawing functions, invoking our "animateLine" function –

function AustinPath () { 
                var line = svg.append("path")
                    .datum(data)
                    .attr("d", AustinLine)
                .transition()
                    .duration(2000)
                    .ease("linear")
                    .attrTween("stroke-dasharray", animateLine);

... [repeated twice more for the other cities: NewYorkPath and SanFranciscoPath]...

... then finally invoke these functions one after another, with the appropriate delays, to draw the lines one by one.

setTimeout(Austinpath, 500);
setTimeout(NewYorkPath, 2500);
setTimeout(SanFranciscoPath, 4500);

... [etc.]

This would unroll each line one by one, but the repetition in the code is mind-boggling, and totally contrary to the elegance of Mike Bostock's original example.

I know the hacky solution is terrible... but is there a more elegant alternative? Can Mike Bostock's original "enter().append" method be staggered, to draw three separate lines in turn?

Any suggestions greatly appreciated. Thanks in advance.

Community
  • 1
  • 1
victorz
  • 205
  • 1
  • 2
  • 4
  • You should be able to use the code you've linked to as-is, just add something like `.delay(function(d, i) { return i * 1000; })` after `.transition()`. – Lars Kotthoff Nov 03 '14 at 18:18
  • Thanks Lars – spot-on as always. Your solution implemented here: http://bl.ocks.org/alimzi/7ae652bbecebb2c03fac Only problem was that all lines displayed initially, *then* vanished and drew themselves out. I fixed that by toggling the "display" attribute during the transition. I'd love to mark this as "answered", but I don't think I can do that on a comment...? Also, I can't be the only member of the D3 community who wants to buy Lars a pony to say "thank-you" for all the help. – victorz Nov 03 '14 at 19:13
  • I'll add it as an answer, thanks. The way to fix the initial draw would be to set `d` to an empty path to begin with (e.g. `pathTween(0)`) before the transition. Oh and I wouldn't have space for a pony ;) – Lars Kotthoff Nov 03 '14 at 19:51

1 Answers1

0

The question you've linked to for the line unrolling uses D3's transition framework, so you don't need anything else to make them appear one after the other. The key here is the .delay() function, which allows you to specify the time after which a transition should start. It can take, as almost everything else in D3, a function that gets passed the current data and index instead of a static value.

So knowing that the unrolling takes a second, we can set the delay like this:

.delay(function(d, i) { return i * 1000; })

That is, delay the transition of each element by its index times 1 second. Adding this code just after .transition() takes care of what you want.

The only snag, as you've discovered, is that the lines will be drawn initially, before disappearing and unrolling. This is because you're setting the d attribute of the lines to its proper (end) value before starting the transition. To avoid this effect, simply set the stroke-dasharray property to the value it has at the beginning of the transition (animateLine(0)) before adding the transition -- "0," + this.getTotalLength(). Complete demo of this here.

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
  • Thanks Lars – the delay works perfectly. I'm afraid I'm still confused about the stroke-dasharray fix, however. Do I need to add an initialising value before the .transition() line – something like: .attr("stroke-dasharray", "0,0")? (That didn't work.) Or mess around with my animateLine function somehow, so that it's defined for zero, then use it... somewhere before the transition? Your solution sounds much more elegant than my messy toggling of the "display" attribute, but I can't quite implement it. – victorz Nov 03 '14 at 21:19