Reputation: 10817
Consider the code snippet
let circles = svg.selectAll("circle")
.data(data)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 2);
The three lines attr-cx
, attr-cy
, and attr-r
operate internally using the following pseudo-code:
foreach d in update-selection:
d.cx = (expression)
foreach d in update-selection:
d.cy = (expression)
foreach d in update-selection:
d.r = (constant)
Now suppose that we want to do it differently. We'd like to instead run:
foreach d in update-selection:
d.cx = (expression)
d.cy = (expression)
d.r = (constant)
by writing either
let circles = svg.selectAll("circle")
.data(data)
.myfunction(d => d);
or
let circles = svg.selectAll("circle")
.data(data)
.myfunction(d);
We might want to do this because:
attr-cx
, attr-cy
, and attr-r
is not just three statements, but a sequence of many dozens or hundreds of statements (that manipulate attributes, among other changes), and we'd like to isolate them into a separate block for readability and testability.How might you isolate the triple of attr
statements through a single function call?
Update
Towards Reusable Charts is a rare post from Mike Bostock suggesting a way to organize a visualization by separating the bulk of the code into a separate module. You know the rest: modularity facilitates reuse, enhances teamwork by programming against APIs, enables testing, etc. Other D3.js examples suffer for the most part from a reliance on monolithic programming that is more suited for discardable one-shot visualizations. Are you aware of other efforts to modularize D3.js code?
Upvotes: 2
Views: 436
Reputation: 102194
TL;DR: there is no performance gain in changing the chained attr
methods for a single function that sets all attributes at once.
We can agree that a typical D3 code is quite repetitive, sometimes with a dozen attr
methods chained. As a D3 programmer I'm used to it now, but I understand the fact that a lot of programmers cite that as their main complaint regarding D3.
In this answer I'll not discuss if that is good or bad, ugly or beautiful, nice or unpleasant. That would be just an opinion, and a worthless one. In this answer I'll focus on performance only.
First, let's consider a few hypothetical solutions:
Using d3-selection-multi
: that may seem as the perfect solution, but actually it changes nothing: in its source code, d3-selection-multi
simply gets the passed object and call selection.attr
several times, just like your first snippet.
However, if performance (your #1) is not an issue and your only concern is readability and testability (as in your #2), I'd go with d3-selection-multi
.
Using selection.each
: I believe that most D3 programmers will immediately think about encapsulating the chained attr
in an each
method. But in fact this changes nothing:
selection.each((d, i, n)=>{
d3.select(n[i])
.attr("foo", foo)
.attr("bar", bar)
//etc...
});
As you can see, the chained attr
are still there. It's even worse, not that we have an additional each
(attr
uses selection.each
internally)
Using selection.call
or any other alternative and passing the same chained attr
methods to the selection.
These are not adequate alternatives when it comes to performance. So, let's try another ways of improving performance.
Examining the source code of attr
we can see that, internally, it uses Element.setAttribute
or Element.setAttributeNS
. With that information, let's try to recreate your pseudocode with a method that loops the selection only once. For that, we'll use selection.each
, like this:
selection.each((d, i, n) => {
n[i].setAttribute("cx", d.x);
n[i].setAttribute("cy", d.y);
n[i].setAttribute("r", 2);
})
Finally, let's test it. For this benchmark I wrote a very simple code, setting the cx
, cy
and r
attributes of some circles. This is the default approach:
const data = d3.range(100).map(() => ({
x: Math.random() * 300,
y: Math.random() * 150
}));
const svg = d3.select("body")
.append("svg");
const circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cx", d=>d.x)
.attr("cy", d=>d.y)
.attr("r", 2)
.style("fill", "teal");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
And this the approach using setAttribute
in a single loop:
const data = d3.range(100).map(() => ({
x: Math.random() * 300,
y: Math.random() * 150
}));
const svg = d3.select("body")
.append("svg");
const circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.each((d, i, n) => {
n[i].setAttribute("cx", d.x);
n[i].setAttribute("cy", d.y);
n[i].setAttribute("r", 2);
})
.style("fill", "teal")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Finally, the most important moment: let's benchmark it. I normally use jsPerf, but it's down for me, so I'm using another online tool. Here it is:
https://measurethat.net/Benchmarks/Show/6750/0/multiple-attributes
And the results were disappointing, there is virtually no difference:
There is some fluctuation, sometimes one code is faster, but most of the times they are pretty equivalent.
However, it gets even worse: as another user correctly pointed in their comment, the correct and dynamic approach would involve looping again in your second pseudocode. That would make the performance even worse:
Therefore, the problem is that your claim ("No matter how fast the iteration control, it's still faster if we iterate once rather than three times") doesn't need to be necessarily true. Think like that: if you had a selection of 15 elements and 4 attributes, the question would be "is it faster doing 15 external loops with 4 internal loops each or doing 4 external loops with 15 internal loops each?". As you can see, nothing allows us to say that one is faster than the other.
Conclusion: there is no performance gain in changing the chained attr
methods for a single function that sets all attributes at once.
Upvotes: 3
Reputation: 614
Does the .call()
method of the d3 selection do what you're after? Documentation at https://github.com/d3/d3-selection/blob/v1.4.1/README.md#selection_call
I have sometimes used this method to define a more 'modular' feeling update function, and even pass different functions into the call()
to do different things as required.
In your example I think we can do:
function updateFunction(selection){
selection
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 2);
}
let circles = svg.selectAll("circle")
.data(data)
.call(updateFunction);
Upvotes: -1