React中的D3.js:数据绑定、SVG以及useEffect的所属规则
了解D3的真正含义、如何实现绑定元素的添加/更新/移除、为何SVG适合用于D3、以及如何划分DOM控制权,以便在折线图中让D3与React协同工作。
来源: Harnessing the Power of Data Visualization: Integrating D3.js with React
首先需要了解的三点
- D3是一个工具包,而非图表库。那些期望它具备类似Chart.js功能的人往往会遇到困难。D3是用于将数据与DOM元素关联的低级工具,这既解释了它的强大功能,也说明了为何学习曲线较为陡峭。
- React和D3会争夺相同的DOM节点。解决方法很简单:让React负责渲染SVG容器,而D3则通过
useEffect来管理容器内的所有内容。如果不遵守这一规则,就会花费数小时解决各种奇怪的冲突问题。 - enter、update和exit模式起初会让人觉得奇怪。经过几次实践后就会明白其原理,此时其他数据驱动图表的实现方式就会显得笨拙不堪。
D3的真正含义
其名称意为“数据驱动文档”。需要注意的是“文档”这个词:指的是外观由数据决定的DOM元素。D3将数组与元素绑定,允许你根据数组中的值来定义元素的外观。图表只是可能的结果之一,该库本身并不区分不同的图表类型。
这既是它的优势,也是它的难点。D3不会替你做任何决定。每一个视觉属性、每一根坐标轴刻度、每一种颜色以及所有的交互效果都需要你自行指定。这意味着你拥有完全的创作自由,但也需要输入大量代码。
数据绑定与DOM
这正是大多数初学者会放弃的地方,因此值得慢慢了解。
在普通的 JavaScript 中,你需要创建一个元素,为其填充内容,然后将其插入页面中。而使用 D3 时,你可以选择一组元素(包括那些尚不存在的元素),将数组绑定到该选择集上,随后会得到三组元素:
- 已经存在但需要更新数据的元素(更新选择集),
- 尚未对应任何元素的数据点(新增选择集),
- 数据已消失的元素(移除选择集)。
奇怪的是,对一个为空的选择集调用方法。你在一个不包含任何元素的 SVG 中选择所有的 rect 元素,接着调用 .data(myArray),然后是 .enter(),最后是 .append('rect'),这样矩形就会出现。实际上,.enter() 会为每个没有对应元素的数据项生成一个占位符,而 .append() 则会将每个占位符转换成真实的节点。可以理解为“对于每个没有对应元素的数据点,创建一个节点。”
在理解这一点之前,D3 代码看起来就像魔法一样;理解之后就变得很自然了。
根据数据设置视觉属性
一旦数据被绑定,接下来的步骤就更容易理解了。任何属性或样式都可以从数据项中计算得出:
selection.attr('height', d => yScale(d.value))
这里的 d 是绑定在该特定元素上的数据,而 yScale 是一个将数值范围转换为像素值的函数。柱状图的高度直接由其数据决定。更改数据后重新运行代码,高度便会随之变化。D3 可视化中的几乎所有内容都基于这一理念:将某个视觉属性设置为边界值的函数。
为何选择 SVG
D3 在 SVG 上进行绘图,其背后的原因远不止历史因素。
SVG 是基于向量的,因此图形是通过数学方式定义的,而非像素网格。在笔记本电脑上清晰可见的柱状图,在 4K 显示器上同样能保持清晰,且无需额外处理;导出的图像在非标准分辨率下也不会变得模糊。
对于交互性而言,SVG图形属于普通的DOM元素,能够像按钮一样参与浏览器的事件系统。悬停、点击和触摸操作均无需额外的碰撞检测,只需为这些元素添加监听器即可。
但当元素数量增多时问题就会显现。成千上万的SVG节点会拖慢页面加载速度,因此将50,000个独立点作为单独元素来绘制会带来不良影响。此时Canvas渲染更为合适。对于控制面板、业务图表以及大多数分析工具而言,SVG依然适用,其事件集成功能也物有所值。
交互性与灵活性
在那些需要用户从不同角度对数据进行切片、筛选和查看的探索型界面中,D3的表现远超静态图表工具。你可以完全掌控每一个事件和过渡效果。
大多数图表库会提供可直接配置的图表类型,如柱状图、折线图、饼图,并带有颜色、标签和图例等选项。而D3则提供的是构建这些图表的基石:比例尺、坐标轴、路径生成器以及布局算法。图表类型不过是这些组件的组合,因此并不存在所谓的支持列表。如果某种可视化效果目前还不存在,你可以自行创建,其限制仅来自SVG技术和数学原理。
这需要额外的工作量。是否值得则取决于具体项目。对于常规的商业图表,使用Recharts或Chart.js就足够了,无需再做更多处理。而对于那些对展示效果有极高要求的自定义交互式图形,D3才是更好的选择。
你会频繁使用的函数
这并非完整的参考手册,仅列出日常工作中会用到的函数调用:
- d3.select() 用于获取与选择器匹配的第一个元素。在 React 中,
d3.select(svgRef.current)是获取容器的常用方式。 - d3.selectAll() 会返回所有匹配的元素。由于通常涉及多个元素,数据绑定一般从这里开始。
- selection.data() 会将数组中的每个项与对应的元素关联起来,这样在后续调用中就可以使用
d。其他所有操作都依赖于这一步。 - selection.enter() 为没有节点的数据提供占位选择,随后通过
.append()创建实际的节点。 - selection.attr() 用于设置属性,比如上面所示的高度线,这类操作可能需要编写数百次。
.attr() 或 .style() 之前,数值会实现平滑过渡,从而将突变转化为动画。d3.max(data, d => d.value) 这样的用法。在 React 中使用 D3
直到遇到问题才会有人提及的麻烦:React 会维护一个虚拟 DOM 并将自身的更改应用到真实 DOM 上,而 D3 则直接编辑真实 DOM。如果两者都在同一个元素内操作,React 的重新渲染会覆盖 D3 做过的修改,或者 D3 会插入 React 未知的节点,从而导致下一次渲染出现混乱。
可行的解决方法是划分职责。React 负责渲染 SVG 容器,而不触碰其内部内容。D3 通过 ref 访问该容器,并在 useEffect 中完成所有操作。当数据发生变化时,状态更新会触发该效应,D3 便会清除旧内容并重新绘制。这样两个库就不会互相干扰。
1. 安装相关包
使用包管理器将 D3 与 React 和 ReactDOM 一起添加。
npm install d3 react react-dom
Yarn 用户也可以使用其自身的 add 命令来完成相同操作。
yarn add d3 react react-dom
2. 编写组件
React 的职责仅有一项:渲染 SVG 元素,并将对应的 ref 传递给 D3。可视化界面中的其他内容均不属于 React 的处理范围。
3. 在 useEffect 中绘制
在 effect 函数中,可以像在 React 之外一样使用 D3,通过 ref 找到容器。两个比例尺负责处理坐标计算:一个将类别标签映射到水平位置,另一个将数据值映射到垂直位置。每根柱状图的高度等于图表底部边缘坐标减去经过比例缩放后的数值。这部分内容常常让人困惑,因为 SVG 的 y 轴是向下的,所以柱子越高,对应的 y 值反而越小。
4. 渲染组件
将该组件放入您的应用中,并传入一个数据数组。修改数组后,图表会重新绘制:React会检测到属性的变化,触发相应的效应函数,D3则会清除旧数据并重新绘制图表。应用状态与可视化内容之间的关联是自动建立的。
5. 启动应用
使用npm运行开发服务器。
npm start
或者使用 Yarn。
yarn start
在浏览器中你会看到一个条形图。从那之后,所有功能都遵循相同的模式:悬停时显示工具提示、颜色刻度、动画更新、缩放功能以及更多图表类型。这一切都基于该效果实现,并使用上述功能。
总结
一旦结构合理,D3与React就能很好地配合。React负责容器和组件生命周期,而D3则负责其中的绘图工作。保持这种界限清晰,让每个库发挥其设计初衷的作用。
这样做的好处是能够创建出任何图表库都无法提供的可视化效果,因为你是直接操作原始数据而非配置选项。当图形本身构成产品价值的一部分时,这一点尤为重要。如果仪表板只需要折线图,那就直接使用Recharts,无需额外折腾。