- PVSM.RU - https://www.pvsm.ru -
D3 одна из наиболее популярных javascript-библиотек для создания динамических и интерактивных визуализаций данных. Сегодня ее используют сотни тысяч сайтов и web-приложений [1].
В интернете огромное количество примеров [2] – от банальных линейных графиков до динамически обновляющихся диаграмм Вороного – созданных с помощью этой библиотеки. Кажется, что можно найти готовый код для любой самой причудливой визуализации и лишь немного модифицировать его «под себя».
Однако, интеграция D3 в web-приложение, построенное на React, на практике оказывается не самой простой задачей.
Проблема в том, что D3 и React оба хотят контролировать DOM. Значит ли, что совместное использование этих библиотек невозможно? Разумеется, нет.
В данной статье на примере создания гистограммы я предлагаю рассмотреть 4 различных подхода к решению данной проблемы, а также обсудить их плюсы и минусы.
Ниже представлен код создания гистограммы на чистом D3, который мы хотим превратить в полноценный компонент React.
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;
В данном случае React используется только для рендеринга html-контейнера (чаще всего ) визуализации. Все фактические манипуляции с данными и их представлением внутри созданного контейнера остаются за D3.
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;
Плюсы
Минусы
D3.js включает в себя несколько десятков различных модулей, лишь немногие из которых непосредственно связаны с DOM. Многочисленные хелперы для предварительной подготовки данных, работы с цветами и географическими объектами, реализованные алгоритмы для расчета кривых, построения графов и интерполяции данных могут заметно облегчить жизнь разработчику, даже в случае когда рендеринг и обновление всех svg-элементов реализуется в рамках жизненного цикла компонента React.
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;
Плюсы
Минусы
В этом случае контроль над всей внутренней структурой узла DOM остается за React. D3.js лишь модифицирует атрибуты, уже созданных элементов.
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;
Плюсы
Минусы
Данный подход предполагает создание объекта, похожего на DOM, который будет использован для работы с D3. Это позволит с одной стороны использовать все API D3.js, а с другой – отдать React полный контроль над реальным DOM.
Оливер Колдвел (Oliver Caldwell), предложивший данную идею [3], создал react-faux-dom [4], который автоматически сохраняет созданные/измененные D3 элементы в state компонента. А затем уже React определяет нужно ли обновлять и в каком объеме настоящий DOM.
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);
Плюсы
Минусы
Разумеется, если ваше приложение не выходит за рамки построения стандартных гистограмм, линейных графиков или круговых диаграмм, то лучше воспользоваться одной из множества библиотек для построения графиков, изначально созданных под React.
Однако, D3 может оказаться незаменимым помощником при создании сложных дашбордов, инфографик и интерактивных историй. Поэтому стоит внимательно взвесить все «за» и «против» при выборе того или иного подхода. Замечу, что реальных проектах чаще всего используются два последних подхода.
Стоит также обратить внимание на такие библиотеки как Semiotic.js [5] и Recharts [6], построенные на основе D3 и React.
Автор: maryzam
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/279405
Ссылки в тексте:
[1] сотни тысяч сайтов и web-приложений: https://www.wappalyzer.com/technologies/d3
[2] примеров: http://bl.ocks.org/
[3] идею: https://oli.me.uk/2015/09/09/d3-within-react-the-right-way/
[4] react-faux-dom: https://github.com/Olical/react-faux-dom
[5] Semiotic.js: https://github.com/emeeks/semiotic/
[6] Recharts: http://recharts.org/
[7] Источник: https://habr.com/post/354806/?utm_source=habrahabr&utm_medium=rss&utm_campaign=354806
Нажмите здесь для печати.