Loading...
G 的 API 和 DOM API 尽可能一致,因此 Web 生态中一些面向 DOM API 的库都可以以非常低的成本接入,例如 使用 Hammer.js 手势库、使用 Interact.js 拖拽库。对于它们来说,G 的事件 API 和 DOM Events API 完全一致:
import Hammer from 'hammerjs';// 直接把 G 的 Circle 当成 DOM 元素交给 Hammer.jsconst hammer = new Hammer(circle);hammer.on('press', (e) => {console.log("You're pressing me!");console.log(e.target); // circle});
同样的,对于 D3 来说,我们完全可以在保留它数据驱动能力的同时,接管它内部默认的 SVG(它也是 DOM API 的一部分)渲染,使用 Canvas 或 WebGL 完成。
在以下示例中,我们使用 Fullstack D3 的几个教学例子,在保留绝大部分 D3 风格代码的同时,通过“一行”代码的修改完成渲染 API 的切换,实现 D3 数据处理 + G 渲染 的效果。你可以在运行时切换 Canvas、WebGL 和 SVG 的渲染效果:
还可以直接使用一些风格化渲染插件,例如通过 g-plugin-rough-canvas-renderer 对上面的柱形图进行手绘风格改造:
详见:https://observablehq.com/@xiaoiver/d3-rough-barchart
值得一提的是,最早我是从 Sprite.js 中看到这一思路,不过当时它对于 DOM API 的实现完成度还不太高,导致部分 D3 API(例如 join)无法正常使用。
理论上这也能解决其他基于 SVG 的绘图库的渲染性能问题,当然这种方案也存在一些“限制”。
示例柱状图来自 Fullstack D3
“一行代码”确实有些标题党,毕竟创建 G 画布和渲染器的步骤不能少:
const canvasRenderer = new CanvasRenderer();const canvas = new Canvas({container: 'container',width: 600,height: 500,renderer: canvasRenderer,});
接下来就是“一行代码”的部分了,现在无需 D3 创建 <svg>
了,我们只需要把 G 场景图的根节点交给 D3,画布的尺寸在创建时就已经指定好了:
// 改动前:D3 使用 DOM API 创建 `<svg>`const wrapper = d3.select('#wrapper').append('svg').attr('width', dimensions.width).attr('height', dimensions.height);// 改动后:把 G 场景图的根节点交给 D3const wrapper = d3.select(canvas.document.documentElement);
以上就是全部的修改内容了,后续就可以完全使用 D3 语法了。例如创建一个 <g>
并设置样式,G 会让 D3 认为仍然在操作 DOM API:
const bounds = wrapper.append('g').style('transform',`translate(${dimensions.margin.left}px, ${dimensions.margin.top}px)`,);
或者使用 D3 的事件机制增加一些事件交互,例如响应鼠标事件修改柱子颜色:
binGroups.on('mouseenter', function (e) {d3.select(e.target).attr('fill', 'red');}).on('mouseleave', function (e) {d3.select(e.target).attr('fill', 'cornflowerblue');});
在使用 D3 以及第三方扩展时,经常需要使用 CSS 选择器,例如 d3-annotation 会使用如下语法:
var group = selection.select('g.annotations');
为了能让 g.annotations
这样的 CSS 选择器正常工作,需要使用 g-plugin-css-selector 插件,注册方式如下:
import { Plugin } from '@antv/g-plugin-css-select';renderer.registerPlugin(new Plugin());
另外在 D3 项目中经常使用 CSS 样式表,例如该 示例 中使用了 d3-annotation
,设置了描边颜色:
.annotation path {stroke: var(--accent-color);}
我们可以使用 G 类似 DOM API 的元素查询方法 querySelectorAll:
const paths = canvas.document.querySelectorAll('.annotation path');paths.forEach(() => {});
或者继续使用 D3 的选择器语法:
svg.select('.annotation path').style('stroke', 'purple');
在说明该方案的限制之前,有必要先了解下背后的原理。我们将从以下方面展开:
首先并不是所有基于 DOM API 的类库都能像 Hammer.js、interact.js、D3 这样“无缝接入” G。或者说,这些适合接入的类库都有一个共同特点,它们并不假定自己处于真实的浏览器 DOM 环境中。
我们以 D3 为例,d3-selection 在创建 DOM 元素时并不会直接使用 window.document
,而是从元素的 ownerDocument 属性上获取,因此只要 G 的图形上也有该同名属性就可以正常运行,而不会调用到浏览器真实的 DOM API:
// @see https://github.com/d3/d3-selection/blob/main/src/creator.js#L6// 获取 documentvar document = this.ownerDocument;// 使用 document 创建元素document.createElement(name);
这些类库这么做还有一个好处,那就是适合在 node 端配合 jsdom 运行测试用例(D3 的做法)。
因此,如果 X6 未来也想以这种方式接入 G,也需要确保没有类似 window.document
这样的用法。
最后 G 在自身内部实现中,也需要避免“浏览器真实 DOM 环境”这样的假定,例如在创建画布时,这样才能运行在 WebWorker 甚至是小程序环境中。
有了适合接入的类库,能否正常运行就要看 G 对于 DOM API 实现的程度了。还是以 D3 为例,在插入元素到文档前会使用 compareDocumentPosition 比较位置,如果 G 没有实现这个 API,运行时就会报错。
可见 G 的核心 API 其实就是轻量版的 jsdom。为什么说是“轻量版”呢,因为很多功能例如 HTML 解析、非内联的 CSS 样式我们都省略了。
目前 G 实现的 DOM API 如下:
wrapper.append('g')
这样的 D3 代码实际上创建了 G 的 el.style('font-size', '1em')
这样包含相对单位的代码才能运行D3 的生态是非常庞大的,无缝接入意味着很多能力是开箱即用的。
在该示例中,我们使用 d3-shape 和 d3-transition 实现了形变动画:
这不禁让我们思考另一个问题,G 的动画能力是否也应该是可插拔的。在上面这个例子中,G 只需要提供渲染能力,对 path
的 d
属性动画完全交给 D3。
我们选择性实现了绝大部分 DOM API,这也意味着放弃了例如 innerHTML
这样的 API:
el.innerHTML = '<div></div>';
因此 D3 中的 selection.html() 暂时是无法正常工作的。
如果要实现这个特性,G 需要思考“混合”渲染的问题。目前同一时间只能选择一个渲染器渲染全部图形,而混合渲染要求 HTML 与使用 Canvas / WebGL 渲染的图形共存。考虑到这些混合内容间的渲染次序和交互,并不是一件容易的事。
值得一提的是新版 Google Docs,从官方提供的示例文档来看,第一页中包含了两个 SVG 和一个 Canvas。其中主体部分(主要是文字)使用 Canvas / WebGL 绘制,并支持文本选中效果,而图片(右上角、文档内)使用 SVG:
目前 G 也不支持外部样式表,因此 D3 应用中的外部样式表无法生效。
但内联用法是有效的,例如示例中的如下用法:
const barText = binGroups.filter(yAccessor).append('text').attr('x', (d) => xScale(d.x0) + (xScale(d.x1) - xScale(d.x0)) / 2).attr('y', (d) => yScale(yAccessor(d)) - 5).text(yAccessor).attr('fill', 'darkgrey').style('text-anchor', 'middle').style('font-size', '12px').style('font-family', 'sans-serif');
SVG 中的 foreignObject 允许嵌入 HTML,和 innerHTML 一样,暂时无法支持。