d3.js中SVG和Canvas的区别

37

我对d3.js还不熟悉,我知道有两种绘制对象的方法——SVG和Canvas。 我的使用场景是涉及<100个节点和边缘。我已经尝试过使用Canvas的几个例子,效果很好。

我看到SO上有一个关于SVG和Canvas之间区别的帖子。

对我来说,这两者都可以,但是我更倾向于使用Canvas(因为我已经有了一些可行的例子)。如果我在d3.js上下文中漏掉了什么,请指出来。

1个回答

85
链接问题/答案中列出的差异涉及svg和canvas之间的一般差异(矢量/光栅等)。然而,对于d3来说,这些差异具有额外的含义,特别是考虑到d3的核心部分是数据绑定。
数据绑定
也许d3最核心的功能是数据绑定。Mike Bostock表示,他需要创建d3是因为他将数据与元素结合起来:
“定义性时刻是当我第一次让数据绑定工作起来时。那是魔术。我甚至不确定我理解它的方式,但使用它很棒。我意识到可以有一个实用的可视化工具,它不会毫无意义地限制你可以制作的可视化类型。link
对于SVG,数据绑定很容易-我们可以将数据分配给单个svg元素,然后使用该数据设置其属性/更新它等。这是建立在svg的状态性基础上的-我们可以重新选择圆并修改它或访问其属性。
对于Canvas,画布是无状态的,因此我们不能将数据绑定到画布内的形状,因为画布仅由像素组成。因此,我们不能选择和更新画布内的元素,因为画布没有任何要选择的元素。
基于以上,我们可以看到,在典型的D3中,SVG需要输入/更新/退出循环(或基本的附加语句):我们需要输入元素才能看到它们,而且我们经常根据它们的数据样式进行设置。对于画布,我们不需要输入任何东西,也不需要退出/更新。没有必要添加任何元素以便查看,因此,如果需要的话,我们可以在不使用进入/更新/退出或附加/插入方法的D3 SVG可视化的情况下绘制可视化。

不带数据绑定的画布

我将使用您上一个问题中的示例block here。因为我们根本不需要附加元素(或将数据附加到它们上面),所以我们使用forEach循环来绘制每个特征(这与SVG的典型D3相反)。由于没有元素可供更新,我们必须重新绘制每个特征每个时刻 - 重新绘制整个框架(请注意每个时刻清除画布)。关于拖动,d3.drag和d3.force具有预期与画布一起使用的某些功能,并且可以通过拖动事件直接修改数据数组 - 绕过任何需要节点元素与鼠标直接交互的DOM(d3.force也在直接修改数据数组 - 但它在 svg example 中也这样做)。

没有数据绑定,我们根据数据直接绘制元素:
data.forEach(function(d) {
    // drawing instructions:
    context.beginPath()....
})

如果数据发生变化,我们可能需要重新绘制数据。
带有数据绑定的画布
话虽如此,您可以使用画布实现数据绑定,但需要使用虚拟元素采用不同的方法。我们按照常规的更新/退出/进入循环进行操作,但由于我们正在使用虚拟元素,因此不会呈现任何内容。我们在需要时重新渲染画布(如果我们使用转换,则可能是连续的),并基于虚拟元素绘制图形。
要创建一个虚拟父容器,我们可以使用:
// container for dummy elements:
var faux = d3.select(document.createElement("custom"));

然后我们可以根据需要进行选择,使用enter/exit/update/append/remove/transition等方法:

// treat as any other DOM elements:
var bars = faux.selectAll(".bar").data(data).enter()....

但是由于这些选择中的元素没有被渲染,我们需要指定如何以及何时绘制它们。没有数据绑定和画布,我们根据数据直接绘制元素,有了数据绑定和画布,我们基于虚拟DOM中的选择/元素进行绘制:

bars.each(function() {
  var selection = d3.select(this);
  context.beginPath();
  context.fillRect(selection.attr("x"), selection.attr("y")...
  ...
})

我们可以在退出/进入/更新等情况下重新绘制元素,这可能具有一些优势。这还允许D3过渡,通过在虚拟元素上连续重绘而在过渡属性时进行转换。

以下示例具有完整的输入/输出/更新循环和过渡,演示了带有数据绑定的画布:

var canvas = d3.select("body")
  .append("canvas")
  .attr("width", 600)
  .attr("height", 200);
  
var context = canvas.node().getContext("2d");

var data = [1,2,3,4,5];

// container for dummy elements:
var faux = d3.select(document.createElement("custom"));

// normal update exit selection with dummy elements:
function update() {
  // modify data:
  manipulateData();
  
  
  var selection = faux.selectAll("circle")
    .data(data, function(d) { return d;});
    
  var exiting = selection.exit().size();
  var exit = selection.exit()
    .transition()
    .attr("r",0)
   .attr("cy", 70)
   .attr("fill","white")
    .duration(1200)
   .remove();
    
  var enter = selection.enter()
    .append("circle")
    .attr("cx", function(d,i) { 
       return (i + exiting) * 20 + 20; 
    })
    .attr("cy", 50)
    .attr("r", 0)
 .attr("fill",function(d) { return ["orange","steelblue","crimson","violet","yellow"][d%5]; });
 
 enter.transition()
    .attr("r", 8)
 .attr("cx", function(d,i) { 
       return i * 20 + 20; 
    })
    .duration(1200);
    
  selection
    .transition()
    .attr("cx", function(d,i) {
      return i * 20 + 20;
    })
    .duration(1200);
 
}


// update every 1.3 seconds
setInterval(update,1300);


// rendering function, called repeatedly:
function render() {
  context.clearRect(0, 0, 600, 200);
  faux.selectAll("circle").each(function() {
    var sel = d3.select(this);
    context.beginPath();
    context.arc(sel.attr("cx"),sel.attr("cy"),sel.attr("r"),0,2*Math.PI);
 context.fillStyle = sel.attr("fill");
    context.fill();
 context.stroke();
  })
  window.requestAnimationFrame(render) 
}

window.requestAnimationFrame(render)

// to manipulate data:
var index = 6; // to keep track of elements.
function manipulateData() {
  data.forEach(function(d,i) {
    var r = Math.random();
    if (r < 0.5 && data.length > 1) {
      data.splice(i,1);
    }
    else {
      data.push(index++);
    }
  })
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

块版本

摘要

使用canvas,数据绑定需要一组虚拟元素,但是一旦绑定,您可以轻松使用过渡和更新/输入/退出循环。但是,渲染与更新/输入/退出和转换分离-由您决定何时以及如何重新绘制可视化。这个绘图发生在更新/输入/退出和转换方法之外。

使用svg,输入/更新/退出循环和转换会更新可视化中的元素,将渲染和数据链接在一步中。

在canvas中,使用伪元素进行数据绑定,可视化表示伪节点。在svg中,可视化是节点。

数据绑定是一个基本差异,惯用的D3在SVG中需要它,但是在使用Canvas时可以选择是否使用它。但是下面提到了Canvas和SVG之间的其他差异:

交互性

也许使用Canvas时最重要的问题是它是无状态的,只是像素的集合而不是元素。这使得当与特定渲染形状交互时,鼠标事件变得困难。虽然鼠标可以与Canvas交互,但标准事件仅对特定像素的交互触发。

因此,虽然在SVG中,我们可以为力导向图中的每个节点分配一个点击监听器(例如),但是在Canvas中,我们为整个画布设置一个点击监听器,然后根据位置确定应该认为哪个节点被“点击”。

上述提到的 D3-force 画布 示例 使用了力导向图的 .find 方法,用它来查找最接近鼠标点击的节点,然后将拖动主题设置为该节点。
有几种方法可以确定正在交互的渲染形状:
  1. 创建一个隐藏的画布,为渲染形状提供参考映射

可见画布中的每个形状都绘制在不可见画布上,但在不可见画布上,它具有唯一的颜色。在可见画布上获取鼠标事件的 xy 坐标,我们可以使用相同的 xy 在不可见画布上获取像素颜色。由于 HTML 中颜色是数字,因此我们可以将该颜色转换为数据的索引。

  1. 反转比例尺(缩放的 xy 位置到未缩放的输入值)用于热图/网格化数据(示例

  2. 使用未渲染的 Voronoi 图的 .find 方法查找最靠近事件的节点(对于点、圆)

  3. 使用力导向图的 .find 方法查找最靠近事件的节点(对于点、圆,主要在力导向图的上下文中使用)
  4. 使用直接数学、四叉树或其他方法
第一种方法可能是最常见的,也是最灵活的,但根据上下文情况,其他方法可能更可取。

性能

我将快速介绍性能问题。在与问题“SVG和Canvas的区别”相关的帖子中,答案可能不够突出,但通常情况下,当处理数千个节点并且这些节点正在进行动画时,Canvas和SVG在渲染时间上有所不同。
随着更多节点的呈现以及节点执行更多操作(转换、移动等),Canvas的性能会越来越好。
以下是Canvas(使用伪节点上的数据绑定)和SVG以及19,200个同时过渡的快速比较: Canvas应该是两者中更流畅的。

D3模块

最后,我会谈到D3的模块。其中大部分根本不涉及DOM,并且可以轻松用于SVG或Canvas。例如,d3-quadtree或d3-time-format并非特定于SVG或Canvas,因为它们根本不涉及DOM或渲染。像d3-hierarchy这样的模块实际上也没有呈现任何内容,但提供了在Canvas或SVG中呈现所需的信息。
{{大多数}}提供SVG路径数据的模块和方法也可以用于生成canvas路径方法调用,因此可以相对容易地用于SVG和Canvas。
我将在这里特别提到几个模块:
D3-selection
显然,此模块需要选择,选择需要元素。因此,要将其与Canvas一起使用,例如进入/更新/退出循环或选择.append/remove/lower/raise,我们要使用带有Canvas的伪元素。
使用Canvas时,使用selection.on()分配的事件侦听器可以使用或不使用数据绑定,鼠标交互的挑战如上所述。
D3-transition
此模块过渡元素的属性,因此只有在使用伪元素进行数据绑定时才会与Canvas一起使用。
D3-axis
此模块严格限于SVG,除非愿意做大量工作来将其塞入Canvas使用中。在使用SVG时,此模块非常有用,特别是在转换轴时。
D3-path
这将采用Canvas路径命令并将其转换为SVG路径数据。有助于采用Canvas代码的SVG情况。主要在D3内部使用以生成SVG路径数据。

3
值得一提的是,D3 的某些功能/模块(例如 d3.axis)仅在 SVG 中运行,因为其源代码是编写用于创建 SVG 元素的。 - Gerardo Furtado
1
啊,是的,轴是d3的一个非常有用的功能,在使用画布时会变得更加复杂。 - Andrew Reid
@AndrewReid 我有一个关于你的分组条形图切换系列的问题,希望你能帮助我。谢谢。 - Joliet
@Joliet,如果你能详细说明一下,我可能可以帮忙,我会尽力而为。 - Andrew Reid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接