【发布时间】:2018-10-12 22:59:20
【问题描述】:
我是 d3.js 的新手。我发现有两种方法可以绘制对象 - SVG 和 Canvas。 我的用例大约是 。我已经尝试了几个使用画布的例子,它看起来很棒。
我看到the difference between SVG and Canvas 周围有一个 SO 帖子。
两者似乎都适合我使用,但是,我倾向于使用画布(因为我已经很少有示例工作)。请纠正我如果我在 d3.js 上下文中遗漏任何内容?
【问题讨论】:
我是 d3.js 的新手。我发现有两种方法可以绘制对象 - SVG 和 Canvas。 我的用例大约是 。我已经尝试了几个使用画布的例子,它看起来很棒。
我看到the difference between SVG and Canvas 周围有一个 SO 帖子。
两者似乎都适合我使用,但是,我倾向于使用画布(因为我已经很少有示例工作)。请纠正我如果我在 d3.js 上下文中遗漏任何内容?
【问题讨论】:
链接的问题/答案中列出的差异说明了 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++);
}
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
总结
使用画布,数据绑定需要一组虚拟元素,但是一旦绑定,您就可以轻松使用转换和更新/进入/退出循环。但是,渲染与更新/进入/退出和转换是分离的——由您决定如何以及何时重绘可视化。此绘图发生在更新/进入/退出和转换方法之外。
使用 svg,进入/更新/退出循环和转换更新可视化中的元素,一步链接渲染和数据。
在对人造元素进行数据绑定的画布中,可视化表示人造节点。在 svg 中,可视化是节点。
数据绑定是一个根本的区别,惯用的 D3 在 SVG 中需要它,但让我们可以选择在使用 Canvas 时是否要使用它。 然而,Canvas 和 SVG 与 D3 相关的其他差异如下所述:
也许使用 Canvas 最重要的问题是它是无状态的,只是像素而不是元素的集合。这使得鼠标事件在与特定渲染形状交互时变得困难。虽然鼠标可以与画布交互,但会触发标准事件以与特定像素进行交互。
因此,使用 SVG 时,我们可以为强制布局中的每个节点分配一个点击监听器(例如),使用 Canvas,我们为整个画布设置一个点击监听器,然后必须根据位置确定应该考虑哪个节点“点击”。
上面提到的 D3-force 画布example 使用强制布局的.find 方法并使用该方法找到最接近鼠标单击的节点,然后将拖动对象设置为该节点。
我们可以通过几种方法来确定正在与之交互的渲染形状:
可见画布中的每个形状都是在不可见画布上绘制的,但在不可见画布上它具有唯一的颜色。在可见画布上获取鼠标事件的 xy,我们可以使用它来获取不可见画布上相同 xy 处的像素颜色。由于颜色是 HTML 中的数字,我们可以将该颜色转换为数据的索引。
为热图/网格数据反转比例(缩放 xy 位置到未缩放的输入值) (example)
使用未渲染的 Voronoi 图的 .find 方法查找离事件最近的节点(对于点、圆)
.find 方法查找离事件最近的节点(对于点、圆,主要在强制布局的上下文中)第一个可能是最常见的,当然也是最灵活的,但其他的可能更可取,具体取决于上下文。
我将很快谈到性能。在问题的链接帖子“What's the difference between SVG and Canvas”中,那里的答案可能不够大胆,但通常 canvas 和 svg 在处理数千个节点时的渲染时间不同,尤其是在渲染数千个正在动画的节点时。
随着更多节点被渲染以及节点执行更多操作(过渡、移动等),Canvas 的性能越来越高。
下面是 Canvas(在人造节点上具有数据绑定)和 SVG 以及 19 200 个同时转换的快速比较:
Canvas 应该是两者中更平滑的。
最后我会谈到 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 路径数据。
【讨论】: