Getting a better performance on repeatedly method on d3

For example, I need to calculate a Math.sqrt of my data for each attr, how can I calculate only one time the Math.sqrt(d)?

var circle = svgContainer.data(dataJson).append("ellipse")
    .attr("cx", function(d) {
        return Math.sqrt(d) + 1
    })
    .attr("cy", function(d) {
        return Math.sqrt(d) + 2
    })
    .attr("rx", function(d) {
        return Math.sqrt(d) + 3
    })
    .attr("ry", function(d) {
        return Math.sqrt(d) + 4
    });

Has any elegant/performative mode? I'm thinking this way:

var aux;
var circle = svgContainer.data(dataJson).append("ellipse")
    .attr("cx", function(d) {
        aux = Math.sqrt(d);
        return aux + 1
    })
    .attr("cy", function(d) {
        return aux + 2
    })
    .attr("rx", function(d) {
        return aux + 3
    })
    .attr("ry", function(d) {
        return aux + 4
    });

Answers:

Answer

An underestimated feature of D3 is the concept of local variables which were introduced with version 4. These variables allow you to store information on a node (that is the reason why it is called local) independent of the data which might have been bound to that node. You don't have to bloat your data to store additional information.

D3 locals allow you to define local state independent of data.

Probably the major advantage of using local variables over other approaches is the fact that it smoothly fits into the classic D3 approach; there is no need to introduce another loop whereby keeping the code clean.

Using local variables to just store a pre-calculated value is probably the simplest use case one can imagine. On the other hand, it perfectly illustrates what D3's local variables are all about: Store some complex information, which might require heavy lifting to create, locally on a node, and retrieve it for later use further on in your code.

Shamelessly copying over and adapting the code from Gerardo's answer the solution can be implemented like this:

var svg = d3.select("svg");

var data = d3.range(100, 1000, 100);

var roots = d3.local();   // This is the instance where our square roots will be stored

var ellipses = svg.selectAll(null)
  .data(data)
  .enter()
  .append("ellipse")
  .attr("fill", "gainsboro")
  .attr("stroke", "darkslateblue")
  .attr("cx", function(d) {
    return roots.set(this, Math.sqrt(d)) * 3;  // Calculate and store the square root
  })
  .attr("cy", function(d) {
    return roots.get(this) * 3;                // Retrieve the previously stored root
  })
  .attr("rx", function(d) {
    return roots.get(this) + 3;                // Retrieve the previously stored root
  })
  .attr("ry", function(d) {
    return roots.get(this) + 4;                // Retrieve the previously stored root
  });
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>

Answer

Probably, the most idiomatic way for doing this in D3 is using selection.each, which:

Invokes the specified function for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]).

So, in your case:

circle.each(function(d){

    //calculates the value just once for each datum:
    var squareRoot = Math.sqrt(d)

    //now use that value in the DOM element, which is 'this':
    d3.select(this).attr("cx", squareRoot)
        .attr("cy", squareRoot)
        //etc...

    });

Here is a demo:

var svg = d3.select("svg");

var data = d3.range(100, 1000, 100);

var ellipses = svg.selectAll(null)
  .data(data)
  .enter()
  .append("ellipse")
  .attr("fill", "gainsboro")
  .attr("stroke", "darkslateblue")
  .each(function(d) {
    var squareRoot = Math.sqrt(d);
    d3.select(this)
      .attr("cx", function(d) {
        return squareRoot * 3
      })
      .attr("cy", function(d) {
        return squareRoot * 3
      })
      .attr("rx", function(d) {
        return squareRoot + 3
      })
      .attr("ry", function(d) {
        return squareRoot + 4
      });
  })
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>

Another common approach in D3 codes is setting a new data property in the first attr method, and retrieving it latter:

.attr("cx", function(d) {
        //set a new property here
        d.squareRoot = Math.sqrt(d.value);
        return d.squareRoot * 3
    })
    .attr("cy", function(d) {
        //retrieve it here
        return d.squareRoot * 3
    })
    //etc...

That way you also perform the calculation only once per element.

Here is the demo:

var svg = d3.select("svg");

var data = d3.range(100, 1000, 100).map(function(d) {
  return {
    value: d
  }
});

var ellipses = svg.selectAll(null)
  .data(data)
  .enter()
  .append("ellipse")
  .attr("fill", "gainsboro")
  .attr("stroke", "darkslateblue")
  .attr("cx", function(d) {
    d.squareRoot = Math.sqrt(d.value);
    return d.squareRoot * 3
  })
  .attr("cy", function(d) {
    return d.squareRoot * 3
  })
  .attr("rx", function(d) {
    return d.squareRoot + 3
  })
  .attr("ry", function(d) {
    return d.squareRoot + 4
  });
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>

PS: by the way, your solution with var aux will not work. Try it and you'll see.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.