📜 ⬆️ ⬇️

How to make friends React and D3

D3 is one of the most popular javascript libraries for creating dynamic and interactive data visualizations. Today it is used by hundreds of thousands of websites and web applications .


There are a huge number of examples on the Internet - from banal line graphs to dynamically updated Voronoi diagrams - created using this library. It seems that you can find ready-made code for any of the most bizarre visualization and only slightly modify it "for yourself."


However, integrating D3 into a web-application built on React, in practice, is not the easiest task.



The problem is that D3 and React both want to control the DOM. Does it mean that sharing these libraries is impossible? Of course no.


In this article, using the example of creating a histogram, I propose to consider 4 different approaches to solving this problem, as well as discuss their pros and cons.


Below is the code for creating a histogram on pure D3, which we want to turn into a full-fledged component of React.


Histogram code
import * as d3 from "d3"; const animDuration = 600; class BarChartVanilla { constructor(selector, size) { this.size = size; this.conatiner = d3.select(selector) .append("svg") .attr("width", size.width) .attr("height", size.height); this.scaleColor = d3.scaleSequential(d3.interpolateViridis); this.scaleHeight = d3.scaleLinear().range([0, size.height - 20]); this.scaleWidth = d3.scaleBand().range([0, size.width]).padding(0.1); } draw(data) { this.scaleColor.domain([0, data.length]); this.scaleWidth.domain(data.map((d) => (d.item))); this.scaleHeight.domain(d3.extent(data, (d) => (d.count))); const bars = this.conatiner .selectAll(".bar") .data(data, function key(d) { return d.item }); bars.exit() .transition().duration(animDuration) .attr("y", this.size.height) .attr("height", 0) .style("fill-opacity", 0) .remove(); bars.enter() .append("rect") .attr("class", "bar") .attr("y", this.size.height) .attr("x", this.size.width ) .attr("rx", 5 ).attr("ry", 5 ) .merge(bars) .transition().duration(animDuration) .attr("y", (d) => ( this.scaleHeight(d.count) )) .attr("height", (d) => (this.size.height - this.scaleHeight(d.count)) ) .attr("x", (d, i) => ( this.scaleWidth(d.item) ) ) .attr("width", this.scaleWidth.bandwidth() ) .style("fill", (d, i) => ( this.scaleColor(i) )); } } export default BarChartVanilla; 

Approach # 1. React for structure, D3 for visualization


In this case, React is used only for rendering the html-container (most often) visualization. All actual data manipulations and their representation inside the created container remain with D3.

Component code
 import React from "react"; import PropTypes from "prop-types"; import * as d3 from "d3"; class BarChartV1 extends React.Component { scaleColor = d3.scaleSequential(d3.interpolateViridis); scaleHeight = d3.scaleLinear(); scaleWidth = d3.scaleBand().padding(0.1); componentDidMount() { this.updateChart(); } componentDidUpdate() { this.updateChart(); } updateChart() { this.updateScales(); const { data, width, height, animDuration } = this.props; const bars = d3.select(this.viz) .selectAll(".bar") .data(data, function key(d) { return d.item }); bars.exit() .transition().duration(animDuration) .attr("y", height) .attr("height", 0) .style("fill-opacity", 0) .remove(); bars.enter() .append("rect") .attr("class", "bar") .attr("y", height) .attr("rx", 5 ).attr("ry", 5 ) .merge(bars) .transition().duration(animDuration) .attr("y", (d) => ( this.scaleHeight(d.count) )) .attr("height", (d) => (height - this.scaleHeight(d.count)) ) .attr("x", (d) => ( this.scaleWidth(d.item) ) ) .attr("width", this.scaleWidth.bandwidth() ) .style("fill", (d) => ( this.scaleColor(d.item) )); } updateScales() { const { data, width, height } = this.props; this.scaleColor.domain([0, data.length]); this.scaleWidth .domain(data.map((d) => (d.item))) .range([0, width]); this.scaleHeight .domain(d3.extent(data, (d) => (d.count))) .range([height - 20, 0]); } render() { const { width, height } = this.props; return ( <svg ref={ viz => (this.viz = viz) } width={width} height={height} > </svg> ); } } BarChartV1.defaultProps = { animDuration: 600 }; BarChartV1.propTypes = { data: PropTypes.array.isRequired, width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, animDuration: PropTypes.number }; export default BarChartV1; 

pros



Minuses



Approach # 2. React to manipulate DOM, D3 for calculations


D3.js includes several dozen different modules, only a few of which are directly related to the DOM. Numerous helpers for preliminary data preparation, working with colors and geographic objects, implemented algorithms for calculating curves, building graphs and interpolating data can make life easier for developers, even in the case when rendering and updating all svg elements is implemented within the life cycle of the React component.


Component code
 import React from "react"; import PropTypes from "prop-types"; import * as d3 from "d3"; class BarChartV2 extends React.Component { scaleColor = d3.scaleSequential(d3.interpolateViridis); scaleHeight = d3.scaleLinear(); scaleWidth = d3.scaleBand().padding(0.1); render() { this.updateScales(); const { width, height, data } = this.props; const bars = data.map((d) => ( <rect key={d.item} width={this.scaleWidth.bandwidth()} height={height - this.scaleHeight(d.count)} x={ this.scaleWidth(d.item)} y={this.scaleHeight(d.count)} fill={this.scaleColor(d.item)} rx="5" ry="5" />)); return ( <svg width={width} height={height} > { bars } </svg> ); } updateScales() { const { data, width, height } = this.props; this.scaleColor.domain([0, data.length]); this.scaleWidth .domain(data.map((d) => (d.item))) .range([0, width]); this.scaleHeight .domain(d3.extent(data, (d) => (d.count))) .range([height - 20, 0]); } } BarChartV2.defaultProps = { animDuration: 600 }; BarChartV2.propTypes = { data: PropTypes.array.isRequired, width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, animDuration: PropTypes.number }; export default BarChartV2; 

pros



Minuses



Approach # 3. React to create / delete visualization elements, D3 to update


In this case, control over the entire internal structure of the DOM node remains with React. D3.js only modifies the attributes of already created elements.


Component code
 import React from "react"; import PropTypes from "prop-types"; import * as d3 from "d3"; class BarChartV3 extends React.Component { scaleColor = d3.scaleSequential(d3.interpolateViridis); scaleHeight = d3.scaleLinear(); scaleWidth = d3.scaleBand().padding(0.1); componentDidMount() { this.updateChart(); } componentDidUpdate() { this.updateChart(); } updateChart() { this.updateScales(); const { data, height, animDuration } = this.props; const bars = d3.select(this.viz) .selectAll(".bar") .data(data, function(d) { return d ? d.item : d3.select(this).attr("item"); }); bars .transition().duration(animDuration) .attr("y", (d) => ( this.scaleHeight(d.count) )) .attr("height", (d) => (height - this.scaleHeight(d.count)) ) .attr("x", (d) => ( this.scaleWidth(d.item) ) ) .attr("width", this.scaleWidth.bandwidth() ) .style("fill", (d) => ( this.scaleColor(d.item) )); } updateScales() { const { data, width, height } = this.props; this.scaleColor.domain([0, data.length]); this.scaleWidth .domain(data.map((d) => (d.item))) .range([0, width]); this.scaleHeight .domain(d3.extent(data, (d) => (d.count))) .range([height - 20, 0]); } render() { const { width, height, data } = this.props; const bars = data.map((d) => ( <rect key={d.item} item={d.item} className="bar" y={height} rx="5" ry="5" />)); return ( <svg ref={ viz => (this.viz = viz) } width={width} height={height} > { bars } </svg> ); } } BarChartV3.defaultProps = { animDuration: 600 }; BarChartV3.propTypes = { data: PropTypes.array.isRequired, width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, animDuration: PropTypes.number }; export default BarChartV3; 

pros



Minuses



Approach # 4. Use fake DOM for D3


This approach involves creating an object similar to DOM, which will be used to work with D3. This will allow, on the one hand, to use all the D3.js APIs, and on the other hand, to give React full control over the real DOM.


Oliver Caldwell, who proposed this idea , created react-faux-dom , which automatically saves the created / modified D3 elements in the state component. And then React determines whether it is necessary to update and to what extent a real DOM.


Component code
 import React from "react"; import PropTypes from "prop-types"; import * as d3 from "d3"; import { withFauxDOM } from 'react-faux-dom' class BarChartV4 extends React.Component { scaleColor = d3.scaleSequential(d3.interpolateViridis); scaleHeight = d3.scaleLinear(); scaleWidth = d3.scaleBand().padding(0.1); componentDidMount() { this.updateChart(); } componentDidUpdate (prevProps, prevState) { if (this.props.data !== prevProps.data) { this.updateChart(); } } updateChart() { this.updateScales(); const { data, width, height, animDuration } = this.props; const faux = this.props.connectFauxDOM("g", "chart"); const bars = d3.select(faux) .selectAll(".bar") .data(data, function key(d) { return d.item }); bars.exit() .transition().duration(animDuration) .attr("y", height) .attr("height", 0) .style("fill-opacity", 0) .remove(); bars.enter() .append("rect") .attr("class", "bar") .attr("y", height) .attr("x", width ) .attr("width", 0) .attr("height", 0) .attr("rx", 5 ).attr("ry", 5 ) .merge(bars) .transition().duration(animDuration) .attr("y", (d) => ( this.scaleHeight(d.count) )) .attr("height", (d) => (height - this.scaleHeight(d.count)) ) .attr("x", (d, i) => ( this.scaleWidth(d.item) ) ) .attr("width", this.scaleWidth.bandwidth() ) .style("fill", (d, i) => ( this.scaleColor(i) )); this.props.animateFauxDOM(800); } updateScales() { const { data, width, height } = this.props; this.scaleColor.domain([0, data.length]); this.scaleWidth .domain(data.map((d) => (d.item))) .range([0, width]); this.scaleHeight .domain(d3.extent(data, (d) => (d.count))) .range([height - 20, 0]); } render() { const { width, height } = this.props; return ( <svg width={width} height={height} > { this.props.chart } </svg> ); } } BarChartV4.defaultProps = { animDuration: 600 }; BarChartV4.propTypes = { data: PropTypes.array.isRequired, width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, animDuration: PropTypes.number }; export default withFauxDOM(BarChartV4); 

pros



Minuses



Conclusion


Of course, if your application does not go beyond the construction of standard histograms, line graphs or pie charts, it is better to use one of the many libraries for plotting graphs originally created under React.


However, D3 can be an indispensable tool in creating complex dashboards, infographics and interactive stories. Therefore, it is worth carefully weighing all the pros and cons when choosing a particular approach. I note that real projects most often use the last two approaches.


It is also worth paying attention to such libraries as Semiotic.js and Recharts , built on the basis of D3 and React.


')

Source: https://habr.com/ru/post/354806/


All Articles