【问题标题】:Difference between svg and canvas in d3.jsd3.js 中 svg 和 canvas 的区别
【发布时间】:2018-10-12 22:59:20
【问题描述】:

我是 d3.js 的新手。我发现有两种方法可以绘制对象 - SVG 和 Canvas。 我的用例大约是 。我已经尝试了几个使用画布的例子,它看起来很棒。

我看到the difference between SVG and Canvas 周围有一个 SO 帖子。

两者似乎都适合我使用,但是,我倾向于使用画布(因为我已经很少有示例工作)。请纠正我如果我在 d3.js 上下文中遗漏任何内容?

【问题讨论】:

    标签: d3.js svg canvas


    【解决方案1】:

    链接的问题/答案中列出的差异说明了 svg 和画布(矢量/光栅等)之间的一般差异。但是,对于 d3,这些差异具有额外的含义,尤其是考虑到 d3 的核心部分是数据绑定。

    数据绑定

    也许 d3 最核心的特性是数据绑定。 Mike Bostock 表示,一旦将数据加入元素,他就需要创建 d3:

    决定性的时刻是我第一次使用数据连接 时间。这太神奇了。我什至不确定我是否理解它是如何工作的,但是 使用起来非常棒。我意识到可以有一个实用的工具 没有不必要地限制类型的可视化 你可以做的可视化。 link

    使用 SVG,数据绑定很容易 - 我们可以为单个 svg 元素分配一个数据,然后使用该数据来设置其属性/更新它/等等。这是建立在 svg 的状态性之上的——我们可以重新选择一个圆并修改它或访问它的属性。

    使用 Canvas,画布是无状态的,因此我们无法将数据绑定到画布内的形状,因为画布仅包含像素。因此我们不能选择和更新画布中的元素,因为画布没有任何元素可供选择。

    基于上述,我们可以看到在惯用的 D3 中,svg 需要进入/更新/退出循环(或基本的附加语句):我们需要输入元素才能看到它们,并且我们经常根据它们的数据设置它们的样式.使用画布,我们不需要 输入任何内容,退出/更新也是如此。没有为了查看而附加的元素,因此我们可以在没有 d3 svg 可视化中使用的 enter/update/exit 或 append/insert 方法的情况下绘制可视化,如果我们愿意

    没有数据绑定的画布

    我将在您的最后一个问题here 中使用示例 bl.ock。因为我们根本不需要附加元素(或向它们附加数据),所以我们使用一个 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/etc:

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

    但是由于这些选择中的元素没有被渲染,我们需要指定如何以及何时绘制它们。在没有数据绑定和 Canvas 的情况下,我们直接根据数据绘制元素,使用数据绑定和 Canvas,我们根据人造 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++);
        }
      })
    }
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"&gt;&lt;/script&gt;

    Block version.

    总结

    使用画布,数据绑定需要一组虚拟元素,但是一旦绑定,您就可以轻松使用转换和更新/进入/退出循环。但是,渲染与更新/进入/退出和转换是分离的——由您决定如何以及何时重绘可视化。此绘图发生在更新/进入/退出和转换方法之外。

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

    在对人造元素进行数据绑定的画布中,可视化表示人造节点。在 svg 中,可视化是节点。

    数据绑定是一个根本的区别,惯用的 D3 在 SVG 中需要它,但让我们可以选择在使用 Canvas 时是否要使用它。 然而,Canvas 和 SVG 与 D3 相关的其他差异如下所述:

    交互性

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

    因此,使用 SVG 时,我们可以为强制布局中的每个节点分配一个点击监听器(例如),使用 Canvas,我们为整个画布设置一个点击监听器,然后必须根据位置确定应该考虑哪个节点“点击”。

    上面提到的 D3-force 画布example 使用强制布局的.find 方法并使用该方法找到最接近鼠标单击的节点,然后将拖动对象设置为该节点。

    我们可以通过几种方法来确定正在与之交互的渲染形状:

    1. 创建一个隐藏的画布,为渲染的形状提供参考图

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

    1. 为热图/网格数据反转比例(缩放 xy 位置到未缩放的输入值) (example)

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

    3. 使用强制布局的 .find 方法查找离事件最近的节点(对于点、圆,主要在强制布局的上下文中)
    4. 使用直接数学、四叉树或其他方法

    第一个可能是最常见的,当然也是最灵活的,但其他的可能更可取,具体取决于上下文。

    性能

    我将很快谈到性能。在问题的链接帖子“What's the difference between SVG and 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 路径数据的模块和方法也可用于生成画布路径方法调用,因此可以相对轻松地用于 SVG 和 Canvas。

    我将在这里特别提到几个模块:

    D3-选择

    显然这个模块需要选择,选择需要元素。因此,要将其与 Canvas 一起用于进入/更新/退出周期或选择 .append/remove/lower/raise 之类的事情,我们希望在 Canvas 中使用人造元素。

    使用 Canvas,分配有 selection.on() 的事件侦听器可以在有或没有数据绑定的情况下工作,上面提到了鼠标交互的挑战。

    D3 过渡

    这个模块转换元素的属性,所以它通常只在我们使用带有人造元素的数据绑定时才会与 Canvas 一起使用。

    D3 轴

    这个模块是严格的 SVG 除非愿意做大量的工作来硬塞到 Canvas 中使用。此模块在使用 SVG 时非常有用,尤其是在转换轴时。

    D3 路径

    这需要 Canvas 路径命令并将它们转换为 SVG 路径数据。对于将画布代码应用于 SVG 情况很有用。主要在 D3 内部用于生成 SVG 路径数据。

    【讨论】:

    • 值得一提的是,一些 D3 功能/模块,例如 d3.axis,在 SVG 中工作,因为编写源代码是为了创建 SVG 元素。跨度>
    • 啊,是的,轴是 d3 的一个非常有用的功能,它与画布变得更加复杂。
    • @AndrewReid 我对您的分组条形图切换系列有疑问,希望您能帮助我。谢谢。
    • @Joliet,我也许可以,如果你能详细说明一下,我会看看我能做什么。
    猜你喜欢
    • 1970-01-01
    • 2011-06-27
    • 2019-04-04
    • 2014-10-15
    • 1970-01-01
    • 1970-01-01
    • 2015-10-26
    • 2018-09-03
    • 1970-01-01
    相关资源
    最近更新 更多