首页 > 解决方案 > 更模块化的 D3.js 编码

问题描述

考虑代码片段

let circles = svg.selectAll("circle")
                 .data(data)
                 .attr("cx", d => d.x)
                 .attr("cy", d => d.y)
                 .attr("r", 2);

三行attr-cx, attr-cy, 和attr-r使用以下伪代码在内部进行操作:

foreach d in update-selection:
    d.cx = (expression)
foreach d in update-selection:
    d.cy = (expression)
foreach d in update-selection:
    d.r = (constant)

现在假设我们想做不同的事情。我们想改为运行:

foreach d in update-selection:
    d.cx = (expression)
    d.cy = (expression)
    d.r = (constant)

通过写

let circles = svg.selectAll("circle")
                 .data(data)
                 .myfunction(d => d);

或者

let circles = svg.selectAll("circle")
                 .data(data)
                 .myfunction(d);

我们可能想要这样做,因为:

  1. 不管迭代控制有多快,如果我们迭代一次而不是三次,它仍然更快。
  2. attr-cxattr-cy和的序列attr-r不仅仅是三个语句,而是数十或数百个语句的序列(操纵属性,以及其他更改),我们希望将它们隔离到一个单独的块中以提高可读性和可测试性。
  3. 作为一个练习,以更好地理解在 D3 中编码时可用的选项。

您如何attr通过单个函数调用隔离三个语句?

更新

Towards Reusable Charts是 Mike Bostock 发表的一篇罕见的文章,它提出了一种通过将大部分代码分离到一个单独的模块中来组织可视化的方法。你知道其余的:模块化促进重用,通过针对 API 进行编程来增强团队合作,支持测试等。其他 D3.js 示例在很大程度上依赖于更适合可丢弃的一次性可视化的单体编程。你知道模块化 D3.js 代码的其他努力吗?

标签: javascriptperformanced3.jssvg

解决方案


TL; DRattr :更改一次设置所有属性的单个函数的链接方法没有性能提升。


我们可以同意,典型的 D3 代码是相当重复的,有时会attr链接十几个方法。作为一名 D3 程序员,我现在已经习惯了,但我明白很多程序员都将其作为他们对 D3 的主要抱怨。

在这个答案中,我不会讨论这是好是坏,丑还是美,好还是不愉快。那将只是一种意见,而且毫无价值。在这个答案中,我将只关注性能

首先,让我们考虑一些假设的解决方案:

  1. 使用d3-selection-multi: 这似乎是完美的解决方案,但实际上它没有任何改变:在其源代码中,d3-selection-multi只需获取传递的对象并selection.attr多次调用,就像您的第一个片段一样。

    但是,如果性能(您的#1)不是问题,并且您唯一关心的是可读性和可测试性(如您的#2),我会选择d3-selection-multi.

  2. 使用selection.each:我相信大多数 D3 程序员会立即考虑将链式封装attr在一个each方法中。但实际上这并没有改变:

    selection.each((d, i, n)=>{
        d3.select(n[i])
            .attr("foo", foo)
            .attr("bar", bar)
            //etc...
    });
    

    正如你所看到的,被锁链attr的仍然存在。更糟糕的是,不是我们有额外的each(内部attr使用selection.each

  3. 使用selection.call或任何其他替代方法并将相同的链接attr方法传递给选择。

在性能方面,这些都不是足够的选择。所以,让我们尝试另一种提高性能的方法。

检查源代码attr我们可以看到,在内部,它使用Element.setAttributeor Element.setAttributeNS。有了这些信息,让我们尝试使用仅循环一次选择的方法重新创建伪代码。为此,我们将使用selection.each,如下所示:

selection.each((d, i, n) => {
    n[i].setAttribute("cx", d.x);
    n[i].setAttribute("cy", d.y);
    n[i].setAttribute("r", 2);
})

最后,让我们测试一下。对于这个基准测试,我编写了一个非常简单的代码,设置了一些圆圈的cxcyr属性。这是默认方法:

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>

这是setAttribute在单个循环中使用的方法:

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>

最后,最重要的时刻:让我们对其进行基准测试。我通常使用 jsPerf,但它不适合我,所以我正在使用另一个在线工具。这里是:

https://measurethat.net/Benchmarks/Show/6750/0/multiple-attributes

结果令人失望,几乎没有区别:

在此处输入图像描述

有一些波动,有时一个代码更快,但大多数时候它们是相当的。

然而,情况变得更糟:正如另一个用户在他们的评论中正确指出的那样,正确和动态的方法将涉及在您的第二个伪代码中再次循环。这会使性能更差:

在此处输入图像描述

因此,问题在于您的主张(“无论迭代控制多快,如果我们迭代一次而不是三次,它仍然更快”)不一定是正确的。这样想:如果您选择了 15 个元素和 4 个属性,那么问题将是“执行 15 个外部循环和每个 4 个内部循环还是执行 4 个外部循环和每个 15 个内部循环更快?” . 如您所见,没有什么可以让我们说一个比另一个快。

结论attr:更改一次设置所有属性的单个函数的链接方法没有性能提升。


推荐阅读