codersun3
codersun3

Reputation: 175

D3.js pass variable into function

I am trying to modify code that updates a line graph every second.

The function takes a variable named path and uses it to make an animation on screen as though the line graph is moving.

Here is a code snippet. (The entire code, working example, and citation is linked at the end of this question.)

var path = svg.append("g")
    .attr("clip-path", "url(#clip)")
  .append("path")
    .datum(data)
    .attr("class", "line")
    .attr("d", line);

tick();

function tick() {

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

  // redraw the line, and slide it to the left
  path
      .attr("d", line)
      .attr("transform", null)
    .transition()
      .duration(500)
      .ease("linear")
      .attr("transform", "translate(" + x(-1) + ",0)")
      .each("end", tick);

  // pop the old data point off the front
  data.shift();

}

What I am trying to do is change the function so that it takes the path variable as an argument, so that I can use the same function (tick) for modifying various different paths.

However, when I change the above code to something like this:

   tick(path);

   function tick(path) { /*same as before*/ }

I get the TypeError path.attr is not a function.

And, if instead, I try to do something like this:

   tick(d3.select(path));

   function tick(path) { /*same as before*/ }

I get the error: cannot read property 'length' of undefined.

Can someone help me out?

Working example: Scroll down to second graph: http://bost.ocks.org/mike/path/

Working code: https://gist.githubusercontent.com/mbostock/1642874/raw/692ec5980f12f4b0cb38c43b41045fe2fe8e3b9e/index.html

Citation for code, example: mbostock on Github

Upvotes: 1

Views: 2782

Answers (1)

altocumulus
altocumulus

Reputation: 21578

The error is not caused by the first call to tick(path) but by subsequent calls when tick() gets called as a callback you registered by .each("end", tick);. These calls will not pass the required parameter path to your function. And, looking at D3's source I don't see a way of passing this parameter to the callback.

You can circumvent these issues by parameterising your callback function with the path you want it to act on:

tick(path)();

function tick(path) {
    var paramTick = function() {
      // push a new data point onto the back
      path.datum().push(random());

      // redraw the line, and slide it to the left
      path
          .attr("d", line)
          .attr("transform", null)
        .transition()
          .duration(500)
          .ease("linear")
          .attr("transform", "translate(" + x(-1) + ",0)")
          .each("end", paramTick);

      // pop the old data point off the front
      path.datum().shift();
    };
    return paramTick;
}

This one uses a closure to save the reference to the path for future calls. The function tick(path) defines an inner function paramTick() which is basically the function you used previously. It does, however, save the reference to the parameter path by means of a closure. Notice, that it passes the reference to itself as the callback parameter for the end event instead of tick(). This way you'll end up with a callback which gets parameterised by the call

tick(path)();

The call to tick(path) returns the parameterised version of your rendering logic which is immediately executed by the second set of parentheses.

I set up a working JSFiddle moving two paths to demonstrate this.


Although this does answer your question, it doesn't seem to be a perfect solution for the problem. Looking at the example moving two paths you will notice that they will get slightly out of sync every now and then. This is caused by utilizing different transitions for each path, which will not get perfectly synchronized.

In my opinion the better approch would be to group multiple paths into one g and apply the transition to this group. With only slight modifications to your original code (JSFiddle v2) this will give a smooth, synchronized movement of all paths.

var data = [
        d3.range(n).map(random),
        d3.range(n).map(random)
];

var paths = group.selectAll("path")
        .data(data)
        .enter()
          .append("path")
            .attr("class", function(d,i) {
                return "line" + i;
            })
            .attr("d", line);

tick();

function tick() {
      // push a new data point onto the back
    paths.each(function(d) {d.push(random());});

    // redraw the line
    paths
        .attr("d", line);

    // Apply the transition to the group
    group
        .attr("transform", null)
      .transition()
        .duration(500)
        .ease("linear")
        .attr("transform", "translate(" + x(-1) + ",0)")
        .each("end", tick);

    // pop the old data point off the front
    paths.each(function(d) {d.shift();});
}

Upvotes: 3

Related Questions