D3 — одна из самых популярных библиотек JavaScript для создания динамических и интерактивных визуализаций данных.
Сегодня он используется сотни тысяч сайтов и веб-приложений .
В интернете огромное количество Примеры — от банальных линейных графиков до динамически обновляемых диаграмм Вороного — создано с помощью этой библиотеки.
Кажется, что для любой самой навороченной визуализации можно найти готовый код и лишь слегка доработать его «под себя».
Однако интеграция D3 в веб-приложение, созданное на 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;
Подход №1. React для структуры, D3 для рендеринга
В этом случае 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.
- Никаких дополнительных усилий для интеграции ранее созданных на D3 компонентов не требуется.
- Не использует возможности оптимизации обновлений React.
- Усложняет тестирование и поддержку кода компонента.
Подход №2. React для манипуляций с DOM, D3 для вычислений
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 разработчиков.
- Ограниченное использование функций D3.js
- Требуются дополнительные усилия для переноса кода, написанного на чистом D3.js.
- Полученный код сильно привязан к структуре компонента React и его жизненному циклу.
Визуализацию, созданную таким способом, будет сложно повторно использовать в проектах, созданных с использованием других фреймворков или библиотек (Angualar, VueJS).
Подход №3. React для создания/удаления элементов визуализации, D3 для обновления.
В этом случае контроль над всей внутренней структурой 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;
плюсы
- Более широкое использование функций D3, включая анимированные переходы между различными состояниями объекта.
- Улучшена производительность за счет использования React для управления структурой визуализации (добавление/удаление ее элементов).
- Невозможно использовать транзакции d3 для удаленных элементов.
- Это решение также довольно сильно привязано к структуре React-компонента (хотя и в меньшей степени, чем вариант 2).
Подход №4. Используйте поддельный DOM для D3
Этот подход предполагает создание DOM-подобного объекта, который будет использоваться для работы с D3. Это позволит, с одной стороны, использовать все API D3.js, а с другой — даст React полный контроль над реальным DOM. Оливер Колдуэлл, предложивший это идея , созданный реакция-подделка , который автоматически сохраняет созданные/измененные элементы D3 в состоянии компонента.А затем 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);
плюсы
- Компонент D3 компонента будет аналогичен версии, написанной на чистом D3. Это означает, что вы можете легко интегрировать код из обширной базы примеров и нет привязки к конкретному фреймворку.
- Поддерживаются все API D3, включая анимацию и обработку событий.
- Для ускорения рендеринга компонентов используются все стандартные методы и встроенные оптимизации React.
- Необходимость использовать другую стороннюю библиотеку или написать собственную реализацию поддельного DOM-объекта.
Заключение
Конечно, если ваше приложение не выходит за рамки создания стандартных гистограмм, линейных графиков или круговых диаграмм, то лучше использовать одну из множества графических библиотек, изначально созданных для React. Однако D3 может стать незаменимым помощником при создании сложных дашбордов, инфографики и интерактивных историй.Поэтому стоит тщательно взвесить все «за» и «против» при выборе того или иного подхода.
Замечу, что в реальных проектах чаще всего используются два последних подхода.
Также стоит обратить внимание на такие библиотеки, как Семиотический.
js И Перезарядка , построенный на основе D3 и React. Теги: #Визуализация данных #d3 #react #JavaScript #интерактивная визуализация #JavaScript #Визуализация данных #react.js
-
Psion Сделал «Хорошо» На «Нетбуке»
19 Oct, 24 -
Мостелеком Инвестирует В Тройную Игру
19 Oct, 24