將 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 也有很多令人興奮的選擇,而且這兩個庫都在不斷發展,因此結合兩者的項目將很難跟上。
選擇將取決於許多變量,而這些變量無法在一篇文章中全部說明。 但是,我們涵蓋了大部分主要考慮因素,希望能夠讓您做出自己的明智決定。
有了這個基礎,我鼓勵你好奇,檢查提到的庫,並為你的項目添加一些圖表。
你用過這些項目嗎? 如果你有,你的經歷是什麼? 在評論中與我們分享一些話。