将 React、D3 及其生态系统结合在一起
已发表: 2022-03-10自 2011 年创建以来,D3.js 已成为在 Web 上构建复杂数据可视化的事实标准。 React 也迅速成熟,成为创建基于组件的用户界面的首选库。
React 和 D3 都是两个优秀的工具,其设计目标有时会发生冲突。 两者都控制用户界面元素,并且以不同的方式进行。 我们如何让它们协同工作,同时根据您当前的项目优化它们的独特优势?
在这篇文章中,我们将了解如何构建需要 D3 强大图表优势的 React 项目。 我们将在您的主要工作和副项目中发现不同的技术以及如何选择最适合您需求的库。
D3 和 DOM
D3.js 中的 D3 代表数据驱动文档。 D3.js 是一个低级库,它提供了创建交互式可视化所需的构建块。 它使用 SVG、HTML、canvas 和 CSS 等 Web 标准来组装一个前端工具箱,该工具箱具有大量 API,并且几乎可以无限控制可视化的外观和行为。 它还提供了几个数学函数来帮助用户计算复杂的 SVG 路径。
它是如何工作的?
简而言之,D3.js 加载数据并将其附加到 DOM。 然后,它将数据绑定到 DOM 元素并转换这些元素,必要时在状态之间转换。
D3.js 选择类似于 jQuery 对象,因为它们帮助我们处理 SVG 复杂性。 这样做的方式类似于 jQuery 处理 HTML DOM 元素的方式。 这两个库还共享一个类似的基于链的 API,并使用 DOM 作为数据存储。
数据连接
正如 Mike Bostocks 的文章“Thinking with Joins”中所解释的,数据连接是 D3 通过使用选择将数据链接到 DOM 元素的过程。
数据连接帮助我们将我们提供的数据与已创建的元素相匹配,添加缺少的项目并删除不再需要的元素。 他们使用 D3.js 选择,当与数据结合时,将所选元素分成三个不同的组:需要创建的元素(输入组)、需要更新的元素(更新组)和需要的元素被删除(退出组)。

实际上,具有两个数组的 JavaScript 对象表示数据连接。 我们可以通过调用 selection 的 enter 和 exit 方法来触发对 enter 和 exit 组的操作,而在最新版本的 D3.js 中我们可以直接对 update 组进行操作。
正如 Bostock 所描述的,通过数据连接,“您可以可视化实时数据,允许交互式探索,并在数据集之间平滑过渡。” 它们实际上是一种差异算法,类似于 React 管理子元素渲染的方式,我们将在以下部分中看到。
D3 库
D3 社区还没有找到从 D3 代码创建组件的标准方法,这是一种常见的需求,因为 D3.js 非常低级。 我们可以说封装模式几乎与基于 D3 的库一样多,尽管我将通过它们的 API 将它们分为四组:面向对象、声明式、函数式和链式(或类似 D3)。
我对 D3.js 生态系统进行了一些研究,并选择了一个小的、高质量的子集。 它们是具有 D3.js 版本 4 和良好测试覆盖率的最新库。 它们在 API 的类型和抽象的粒度上有所不同。
可绘图
Plottable 是一个流行的面向对象的图表库,具有低粒度的特点; 所以,我们需要手动设置坐标轴、刻度和绘图来组成图表。 你可以在这里看到一个例子。

广告牌
Billboard 是著名的 C3.js 库的一个分支,更新了与 D3.js 版本 4 的兼容性,旨在为这个经典库提供连续性。 它是使用 ECMAScript 6 和新的现代工具(如 Webpack)编写的。 它的 API 基于传递给图表的配置对象,所以我们可以说它是一个声明式 API。

织女星
Vega 将声明式路径走得更远,将配置从 JavaScript 对象演变为纯 JSON 文件。 它旨在实现一个可视化语法,灵感来自 Leland Wilkinson 的一本书The Grammar of Graphics ,它形式化了数据可视化的构建块,这也是 D3.js 的灵感来源。 您可以使用它的编辑器,选择其中一个示例作为起点。

D3FC
D3FC 利用 D3.js 和自定义构建块来帮助您在 SVG 和画布中创建强大的交互式图表。 它具有功能性、低粒度的界面和大量的 D3.js 代码,因此,虽然功能强大,但可能需要一些学习。 看看它的例子。

Britecharts
Britecharts——一个由 Eventbrite 创建的库,我是其中的核心贡献者——利用了可重用图表 API,这是一种封装模式,由 Mike Bostock 在他的文章“走向可重用图表”中推广并在 NVD3 等其他库中使用。 Britecharts 创建了一个高级抽象,使得创建图表变得容易,同时保持内部的低复杂性,允许 D3 开发人员自定义 Britecharts 以供他们使用。 我们花了很多时间来构建精美的 UI 和许多平易近人的演示。

通过 API 总结这些库,我们可以这样表示它们:

React 和 DOM
React 是一个 JavaScript 库,它通过组合组件帮助我们构建用户界面。 这些组件跟踪它们的状态并传递属性以有效地重新渲染自己,从而优化应用程序的性能。
它是如何工作的?
虚拟 DOM 是 DOM 的当前状态的表示,是支持 React 重新渲染优化的技术。 该库使用复杂的差异算法来了解应用程序的哪些部分在条件发生变化时需要重新渲染。 这种差异算法称为“对账算法”。
动态子组件
在渲染包含项目列表的组件时,开发人员需要使用附加到子组件的唯一“键”属性。 此值有助于 diff 算法确定当新数据(或在 React 世界中称为状态)传递给组件时是否需要重新渲染项目。 协调算法检查键的值以查看是否需要添加或删除项目。 了解了 D3.js 的数据连接之后,是不是感觉很熟悉?
从 0.14 版本开始,React 还将渲染器保存在一个单独的模块中。 这样,我们可以使用相同的组件在不同的介质中进行渲染,例如原生应用程序 (React Native)、虚拟现实 (React VR) 和 DOM (react-dom)。 这种灵活性类似于 D3.js 代码可以在不同的上下文中呈现的方式,例如 SVG 和画布。
反应和 D3.js
React 和 D3 的共同目标是帮助我们以高度优化的方式处理 DOM 及其复杂性。 他们还共享对纯函数的偏好——对于给定的输入,代码总是返回相同的输出而不会产生副作用——和无状态组件。
然而,对 DOM 的共同关注使得这两个固执己见的库在确定渲染和动画用户界面元素时发生冲突。 我们将看到解决这一争端的不同方法,而且没有简单的答案。 不过,我们可以建立一个硬性规则:它们永远不应该共享 DOM 控制权。 那将是灾难的根源。
方法
在集成 React 和 D3.js 时,我们可以在不同的层面上这样做,更多地倾向于 D3.js 方面或 React 方面。 让我们看看我们的四个主要选择。
React 中的 D3.js
我们可以遵循的第一种方法是为我们的 D3 代码提供尽可能多的 DOM 控制权。 它使用一个 React 组件来渲染一个空的 SVG 元素,该元素作为我们数据可视化的根元素。 然后它使用componentDidUpdate
生命周期方法,使用该根元素,使用我们将在 vanilla JavaScript 场景中使用的 D3.js 代码创建图表。 我们还可以通过让shouldComponentUpdate
方法始终返回false
来阻止任何进一步的组件更新。
class Line extends React.Component { static propTypes = {...} componentDidMount() { // D3 Code to create the chart // using this._rootNode as container } shouldComponentUpdate() { // Prevents component re-rendering return false; } _setRef(componentNode) { this._rootNode = componentNode; } render() { <div className="line-container" ref={this._setRef.bind(this)} /> } }
在评估这种方法时,我们认识到它提供了一些优点和缺点。 在好处中,这是一个简单的解决方案,在大多数情况下都可以正常工作。 当您将现有代码移植到 React 中时,或者当您使用已经在其他地方工作的 D3.js 图表时,它也是最自然的解决方案。
不利的一面是,在一个 React 组件中混合 React 代码和 D3.js 代码可能会被视为有点粗俗,包含太多依赖项并使该文件太长而不能被视为高质量代码。 此外,这个实现根本感觉不到 React 惯用的。 最后,由于 React 渲染服务器不调用componentDidUpdate
方法,我们无法在初始 HTML 中提供图表的渲染版本。
反应人造 DOM
由 Oliver Caldwell 实现的 React Faux DOM “是一种使用现有 D3 工具的方式,但在 React 精神中通过 React 渲染它。” 它使用虚假的 DOM 实现来欺骗 D3.js,使其认为它正在处理真实的 DOM。 这样,我们在使用 D3.js 的同时保留了 React DOM 树——几乎——它的所有潜力。
import {withFauxDOM} from 'react-faux-dom' class Line extends React.Component { static propTypes = {...} componentDidMount() { const faux = this.props.connectFauxDOM('div', 'chart'); // D3 Code to create the chart // using faux as container d3.select(faux) .append('svg') {...} } render() { <div className="line-container"> {this.props.chart} </div> } } export default withFauxDOM(Line);
这种方法的一个优点是它允许您使用大多数 D3.js API,从而可以轻松地与已构建的 D3.js 代码集成。 它还允许服务器端渲染。 这种策略的一个缺点是性能较低,因为我们在 React 的虚拟 DOM 之前放置了另一个假 DOM 实现,对 DOM 进行了两次虚拟化。 这个问题限制了它对中小型数据可视化的使用。
生命周期方法包装
这种方法首先由 Nicolas Hery 提出,它利用了基于类的 React 组件中存在的生命周期方法。 它优雅地包装了 D3.js 图表的创建、更新和删除,在 React 和 D3.js 代码之间建立了清晰的界限。
import D3Line from './D3Line' class Line extends React.Component { static propTypes = {...} componentDidMount() { // D3 Code to create the chart this._chart = D3Line.create( this._rootNode, this.props.data, this.props.config ); } componentDidUpdate() { // D3 Code to update the chart D3Line.update( this._rootNode, this.props.data, this.props.config, this._chart ); } componentWillUnmount() { D3Line.destroy(this._rootNode); } _setRef(componentNode) { this._rootNode = componentNode; } render() { <div className="line-container" ref={this._setRef.bind(this)} /> } }
D3Line 是这样的:
const D3Line = {}; D3Line.create = (el, data, configuration) => { // D3 Code to create the chart }; D3Line.update = (el, data, configuration, chart) => { // D3 Code to update the chart }; D3Line.destroy = () => { // Cleaning code here }; export default D3Line;
以这种方式编码会产生一个轻量级的 React 组件,它通过一个简单的 API(创建、更新和删除)与基于 D3.js 的图表实例进行通信,向下推送我们想要监听的任何回调方法。
这种策略促进了关注点的清晰分离,使用外观来隐藏图表的实现细节。 它可以封装任何图形,生成的界面简单。 另一个好处是它很容易与任何已经编写的 D3.js 代码集成,它允许我们使用 D3.js 的出色转换。 这种方法的主要缺点是服务器端渲染是不可能的。
React 用于 DOM,D3 用于数学
在此策略中,我们将 D3.js 的使用限制在最低限度。 这意味着对 SVG 路径、比例、布局和任何获取用户数据并将其转换为我们可以使用 React 绘制的东西的转换执行计算。
由于大量与 DOM 无关的 D3.js 子模块,仅将 D3.js 用于数学计算是可能的。 这条路径对 React 最友好,让 Facebook 库完全控制 DOM,它做得非常好。
让我们看一个简化的例子:
class Line extends React.Component { static propTypes = {...} drawLine() { let xScale = d3.scaleTime() .domain(d3.extent(this.props.data, ({date}) => date)); .rangeRound([0, this.props.width]); let yScale = d3.scaleLinear() .domain(d3.extent(this.props.data, ({value}) => value)) .rangeRound([this.props.height, 0]); let line = d3.line() .x((d) => xScale(d.date)) .y((d) => yScale(d.value)); return ( <path className="line" d={line(this.props.data)} /> ); } render() { <svg className="line-container" width={this.props.width} height={this.props.height} > {this.drawLine()} </svg> } }
这种技术是经验丰富的 React 开发人员的最爱,因为它与 React 方式一致。 此外,一旦它到位,用它的代码构建图表感觉很棒。 另一个好处是在服务器端渲染,可能还有 React Native 或 React VR。

矛盾的是,这种方法需要更多关于 D3.js 工作原理的知识,因为我们需要在低级别与其子模块集成。 我们必须重新实现一些 D3.js 功能——更常见的轴和形状; 画笔、缩放和拖动可能是最难的——这意味着大量的前期工作。
我们还需要实现所有的动画。 我们在 React 生态系统中有很棒的工具可以让我们管理动画——参见 react-transition-group、react-motion 和 react-move——尽管它们都不能让我们创建复杂的 SVG 路径插值。 一个悬而未决的问题是如何利用这种方法来使用 HTML5 的 canvas 元素呈现图表。
在下图中,我们可以根据与 React 和 D3.js 的集成级别看到所有描述的方法:

React-D3.js 库
我对 D3.js-React 库进行了一些研究,希望在您面临选择使用、贡献或分叉的库的决定时对您有所帮助。 它包含一些主观指标,因此请持保留态度。
这项研究表明,尽管有许多库,但维护的却不多。 作为一名维护者,我可以理解要跟上一个主要库的变化是多么困难,以及必须照顾两个库将是一项艰巨的任务。
此外,生产就绪库的数量(从 1.0.0 及更高版本开始)仍然非常少。 这可能与发布这种类型的库所需的工作量有关。
让我们看看我最喜欢的一些。
胜利
Victory 是咨询公司 Formidable Labs 的一个项目,它是一个图表元素的低级组件库。 由于这种低级特性,Victory 组件可以与不同的配置组合在一起,以创建复杂的数据可视化。 在底层,它重新实现了 D3.js 功能,例如画笔和缩放,尽管它使用 d3-interpolate 来制作动画。

将其用于折线图将如下所示:
class LineChart extends React.Component { render() { return ( <VictoryChart height={400} width={400} containerComponent={<VictoryVoronoiContainer/>} > <VictoryGroup labels={(d) => `y: ${dy}`} labelComponent={ <VictoryTooltip style={{ fontSize: 10 }} /> } data={data} > <VictoryLine/> <VictoryScatter size={(d, a) => {return a ? 8 : 3;}} /> </VictoryGroup> </VictoryChart> ); } }
这会产生一个像这样的折线图:

开始使用 Victory 很容易,而且它有一些不错的好处,比如缩放和用于工具提示的 Voronoi 容器。 它是一个时髦的库,尽管它仍处于预发布状态并且有大量的错误待处理。 Victory 是目前唯一可以与 React Native 一起使用的库。
图表
Recharts 美观、令人愉快的用户体验、流畅的动画和漂亮的工具提示,是我最喜欢的 React-D3.js 库之一。 Recharts 仅使用 d3-scale、d3-interpolate 和 d3-shape。 它提供了比 Victory 更高级别的粒度,限制了我们可以编写的数据可视化的数量。

使用 Recharts 看起来像这样:
class LineChart extends React.Component { render () { return ( <LineChart width={600} height={300} data={data} margin={{top: 5, right: 30, left: 20, bottom: 5}} > <XAxis dataKey="name"/> <YAxis/> <CartesianGrid strokeDasharray="3 3"/> <Tooltip/> <Legend /> <Line type="monotone" dataKey="pv" stroke="#8884d8" activeDot={{r: 8}}/> <Line type="monotone" dataKey="uv" stroke="#82ca9d" /> </LineChart> ); } }

这个库也经过了很好的测试,虽然仍处于测试阶段,但它具有一些常用的图表、雷达图、树状图甚至画笔。 您可以查看其示例以了解更多信息。 为这个项目做出贡献的开发人员投入了大量精力,通过他们的动画项目 react-smooth 实现了流畅的动画。
尼沃
Nivo 是一个高级 React-D3.js 图表库。 它提供了许多渲染选项:SVG、画布,甚至是基于 API 的 HTML 版本的图表,非常适合服务器端渲染。 它使用 React Motion 制作动画。

它的 API 有点不同,因为它只为每个图表显示一个可配置的组件。 让我们看一个例子:
class LineChart extends React.Component { render () { return ( <ResponsiveLine data={data} margin={{ "top": 50, "right": 110, "bottom": 50, "left": 60 }} minY="auto" stacked={true} axisBottom={{ "orient": "bottom", "tickSize": 5, "tickPadding": 5, "tickRotation": 0, "legend": "country code", "legendOffset": 36, "legendPosition": "center" }} axisLeft={{ "orient": "left", "tickSize": 5, "tickPadding": 5, "tickRotation": 0, "legend": "count", "legendOffset": -40, "legendPosition": "center" }} dotSize={10} dotColor="inherit:darker(0.3)" dotBorderWidth={2} dotBorderColor="#ffffff" enableDotLabel={true} dotLabel="y" dotLabelYOffset={-12} animate={true} motionStiffness={90} motionDamping={15} legends={[ { "anchor": "bottom-right", "direction": "column", "translateX": 100, "itemWidth": 80, "itemHeight": 20, "symbolSize": 12, "symbolShape": "circle" } ]} /> ); } }

拉斐尔·贝尼特 (Raphael Benitte) 与 Nivo 的合作令人震惊。 文档很可爱,它的演示是可配置的。 由于该库的抽象级别较高,使用起来非常简单,可以说它提供的可视化创建潜力较小。 Nivo 的一个不错的功能是可以使用 SVG 模式和渐变来填充图表。
VX
VX 是用于创建可视化的低级可视化组件的集合。 它是无主见的,应该用于生成其他图表库或按原样使用。

让我们看一些代码:
class VXLineChart extends React.Component { render () { let {width, height, margin} = this.props; // bounds const xMax = width - margin.left - margin.right; const yMax = height - margin.top - margin.bottom; // scales const xScale = scaleTime({ range: [0, xMax], domain: extent(data, x), }); const yScale = scaleLinear({ range: [yMax, 0], domain: [0, max(data, y)], nice: true, }); return ( <svg width={width} height={height} > <rect x={0} y={0} width={width} height={height} fill="white" rx={14} /> <Group top={margin.top}> <LinePath data={data} xScale={xScale} yScale={yScale} x={x} y={y} stroke='#32deaa' strokeWidth={2} /> </Group> </svg> ); } };

鉴于这种低级粒度,我将 VX 视为 React 世界的 D3.js。 它与用户想要使用的动画库无关。 目前,它仍处于早期测试阶段,尽管 Airbnb 正在生产中使用它。 目前它的不足之处是缺乏对刷子和缩放等交互的支持。
Britecharts 反应
Britecharts React 仍处于测试阶段,它是这些库中唯一一个使用生命周期方法包装方法的库。 它旨在通过创建一个易于使用的代码包装器来允许在 React 中使用 Britecharts 可视化。

这是折线图的简单代码:
class LineChart extends React.Component { render () { const margin = { top: 60, right: 30, bottom: 60, left: 70, }; return ( <TooltipComponent data={lineData.oneSet()} topicLabel="topics" title="Tooltip Title" render={(props) => ( <LineComponent margin={margin} lineCurve="basis" {...props} /> )} /> ); } }

这个我不能客观。 Britecharts React 可以用作脚手架,将您的 D3.js 图表呈现为 React 组件。 它不支持服务器端渲染,尽管我们已经包含了加载状态来以某种方式克服这个问题。
随意查看在线演示并使用代码。
选择方法或库
我将使用图表构建应用程序的注意事项分为四类:质量、时间、范围和成本。 它们太多了,所以我们应该简化。
假设我们修复了quality 。 我们的目标是拥有一个经过良好测试的代码库,与 D3.js 版本 4 保持同步,并提供全面的文档。
如果我们考虑时间,问自己一个有用的问题是,“这是一项长期投资吗?” 如果响应是“是”,那么我会建议你创建一个基于 D3.js 的库,并使用生命周期方法方法将其包装在 React 中。 这种方法通过技术将我们的代码分开,并且更耐时间。
相反,如果项目的期限很紧,并且团队不需要长时间维护它,我建议获取最接近规范的 React-D3.js 或 D3.js 库,分叉并使用它,努力在此过程中做出贡献。
当我们处理范围时,我们应该考虑我们需要的是少量的基本图表,一次性的复杂可视化还是几个高度定制的图形。 在第一种情况下,我会再次选择最接近规范的库并将其分叉。 对于包含大量动画或交互的定制数据可视化,使用常规 D3.js 构建是最佳选择。 最后,如果您计划使用具有特定规格的不同图表(可能在 UX 人员和设计师的支持下),那么从头开始创建 D3 库或分叉和自定义现有库将是最好的选择。
最后,决策的成本方面与团队的预算和培训有关。 你的团队有哪些技能? 如果您有 D3.js 开发人员,他们更喜欢 D3.js 和 React 之间的明确分离,因此使用生命周期方法包装的方法可能会很好用。 但是,如果您的团队主要是 React 开发人员,他们会喜欢扩展任何当前的 React-D3.js 库。 此外,将生命周期方法与 D3.js 示例一起使用也可以。 我很少推荐的是滚动你自己的 React-D3.js 库。 前期所需的工作量令人生畏,而且两个库的更新速度使维护成本变得不小。
概括
React 和 D3.js 是帮助我们应对 DOM 及其挑战的好工具。 他们当然可以一起工作,我们有权选择在哪里划清界限。 存在一个健康的库生态系统来帮助我们使用 D3.js。 React-D3.js 也有很多令人兴奋的选择,而且这两个库都在不断发展,因此结合两者的项目将很难跟上。
选择将取决于许多变量,而这些变量无法在一篇文章中全部说明。 但是,我们涵盖了大部分主要考虑因素,希望能够让您做出自己的明智决定。
有了这个基础,我鼓励你好奇,检查提到的库,并为你的项目添加一些图表。
你用过这些项目吗? 如果你有,你的经历是什么? 在评论中与我们分享一些话。