Главная » Статьи » Методы расширенной визуализации данных с реакцией

Методы расширенной визуализации данных с реакцией

Методы расширенной визуализации данных с реакцией

От автора: использование Voronois, однопроходного рендеринга и компонентов canvas для потрясающего UX. Итак, вы начали делать визуализацию данных с помощью react-vis, вы создали свои собственные диаграммы, возможно, вы прочитали замечательное введение Shyianovska Nataliia Data Visualization с react-vis или, возможно, даже построили панель управления. Пришло время изучить способы обработки больших объемов данных и обработки более сложных взаимодействий пользовательского интерфейса.

В ходе этой статьи мы увидим некоторые методы визуализации данных: как использовать однопроходный рендеринг, компоненты холста, debounced state обновления и Voronois. Пристегнитесь, это будет дикая поездка! Чтобы начать работу, вы можете создать новое приложение с помощью приложения create-react-app, а затем запустить в терминале:

npm install react-vis --save
# we will also be using a few additional libraries # so install these too
npm install --save d3-fetch d3-scale d3-scale-chromatic debounce simplify-js

Тем не менее, я использую немного другую конфигурацию для своих приложений, которую вы можете проверить здесь (вместе с кодом для всей этой статьи).

Получение и подготовка данных

Традиционная техника обучения чему-либо — подражать мастерам, и эта статья не будет исключением. Мы будем изучать визуализацию New York Times в 2016 году «3-х точечная запись Стефана Карри в контексте».

В этой визуализации многое происходит! Каждая строка показывает количество трех указателей, сделанных определенным игроком в течение определенного сезона. Эта информация становится доступной с помощью динамической мыши, которая одновременно выделяет этот конкретный игровой год и предоставляет всплывающую подсказку, которая точно описывает, в какой строке находится пользователь.

Нашим первым шагом будет получение данных. Приятно, что NYT служит для этой статьи в CSV, поэтому мы можем довольно легко получить это, посмотрев вкладку сети в хром, как показано ниже. После того, как вы загрузили файл данных, поместите его где-нибудь в свое приложение, я помещаю его под папку с именем data и назвал его «nyt-rip.csv».

Формат этого csv немного неудобен, он имеет столбцы для идентификатора игрока, имя игрока, год, а затем количество трех бросков для каждой игры сезона. Давайте очистим это в формате, с которым немного легче работать:

const nonNumericRows = { player: true, pname: true, year: true
}; // reformats input csv so that each row // has details about the corresponding player
// and an array of points describing that players record
export function cleanData(data) { return data.map(row => { // extract all of the columns for // this row that update that players score const gameData = Object.keys(row) .filter(key => !nonNumericRows[key] && row[key] !== 'NA') .map((key, x) => ({x, y: Number(row[key])})); // return a formatted object to manipulate return { player: row.player, pname: row.pname, year: row.year, height: gameData[gameData.length - 1], gameData }; });
}

Отлично, теперь у нас есть механизм для форматирования наших данных удобным способом. Пока мы пишем служебные функции, давайте также напишем один для захвата домена данных:

export function getDataDomain(data) { const {min, max} = data.reduce((acc, player) => { return player.gameData.reduce((mem, row) => { return { min: Math.min(mem.min, row.y), max: Math.max(mem.max, row.y) }; }, acc);
}, {min: Infinity, max: -Infinity}); return [min, max];
}

Поместите обе эти функции в файл utils.js, и мы отправимся на гонки.

Первый наивный подход

Чтобы заложить основу для наших последующих оптимизаций, давайте представим наивный подход. В нашем наборе данных есть 750 игровых лет, поэтому давайте просто иметь 750 LineSeries. Когда мы нависаем над ними, давайте перерисуем строки и выделим выбранную. Довольно разумно! Вот полный проход при наивной реализации, после чего я расскажу о каждой части:

import React from 'react';
import {csv} from 'd3-fetch';
import {scaleLinear} from 'd3-scale';
import {interpolateWarm} from 'd3-scale-chromatic';
import {XYPlot, LineSeries, LabelSeries, XAxis} from 'react-vis';
import {desiredLabels, layout} from '../constants';
import {cleanData, getDataDomain} from '../utils'; // create a object descrbing which players to list along the y-axis
const labelMap = desiredLabels.reduce((acc, row) => { acc[`${row.pname}-${row.year}`] = true; return acc;
}, {}); export default class RootComponent extends React.Component { state = { data: null, loading: true, highlightSeries: null } componentWillMount() { csv('data/nyt-rip.csv') .then(data => this.setState({ data: cleanData(data), loading: false })); } render() { const {loading, highlightSeries, data} = this.state; if (loading) { return <div><h1>LOADING</h1></div>; } const dataDomain = getDataDomain(data); const domainScale = scaleLinear() .domain(dataDomain).range([1, 0]); const colorScale = val => interpolateWarm(domainScale(val)); return ( <XYPlot {...layout}> {data.map((d, idx) => { const id = `${d.pname}-${d.year}`; const y = d.gameData[d.gameData.length - 1].y; return ( <LineSeries key={idx} strokeWidth={1} data={d.gameData} onSeriesMouseOver={() => this.setState({highlightSeries: id})} onSeriesMouseOut={() => this.setState({highlightSeries: null})} stroke={ id === highlightSeries ? 'black' : colorScale(y) } />); })} <LabelSeries data={data .filter(row => labelMap[`${row.pname}-${row.year}`])} style={{fontSize: '10px', fontFamily: 'sans-serif'}} getY={d => d.gameData[d.gameData.length - 1].y} getX={d => 82} labelAnchorX="start" getLabel={d => `${d.pname} - ${d.gameData[d.gameData.length - 1].y}` }/> <XAxis style={{ ticks: {fontSize: '10px', fontFamily: 'sans-serif'} }} tickFormat={d => !d ? '1st game' : (!(d % 10) ? `${d}th` : '')} /> </XYPlot> ); }
}

Как много всего сразу! Но с этим легко справиться, если разбить код на куски. Первое, что делает наш компонент, когда он готовится к монтированию — это вызывает данные через функцию csv d3-fetch, затем мы очищаем данные и сохраняем их в состоянии для проглатывания в ближайшем будущем.

componentWillMount() { csv('data/nyt-rip.csv') .then(data => this.setState({ data: cleanData(data), loading: false })); }

Мы начинаем нашу функцию рендеринга, отказываясь пытаться отобразить наш компонент, если данные еще не загружены, вместо этого просто вернули сообщение загрузки (хотя вы могли бы легко показать показания счетчика или использовать что-то фантастическое, как placeloader ). Затем мы создаем цветную шкалу, основанную на d3-scale-chromatic, используя нашу функцию getDataDomain из предыдущего раздела. В этой статье мы в первую очередь заинтересованы в восстановлении ощущения (в отличие от точного внешнего вида) оригинальной визуализации NYT, поэтому здесь мы используем другую цветовую гамму и отказываемся от некоторых дополнительных графических украшений.

const {loading, highlightSeries, data} = this.state;
if (loading) { return <div><h1>LOADING</h1></div>;
}
const dataDomain = getDataDomain(data);
const domainScale = scaleLinear().domain(dataDomain).range([1, 0]);
const colorScale = val => interpolateWarm(domainScale(val));

Наконец, мы приходим к фактическому рендерингу нашей диаграммы. Мы начинаем с рассмотрения всех наших строк данных и создания LineSeries для каждого из них наряду с определением рудиментарного метода взаимодействия. Затем мы добавляем LabelSeries, чтобы выделить только определенные точки вдоль yAxis, а xAxis со специальным форматированием в соответствии с метками, указанными графикой NYT.

<XYPlot {...layout}> {data.map((player, idx) => { const playerHighlightString = `${player.pname}-${player.year}`; return ( <LineSeries key={idx} strokeWidth="4" curve="curveStepBefore" data={player.gameData} onSeriesMouseOver={() => this.setState({highlightSeries: playerHighlightString})} onSeriesMouseOut={() => this.setState({highlightSeries: null})} stroke={ playerHighlightString === highlightSeries ? 'black' : colorScale(player.gameData[player.gameData.length - 1].y) } />); })} <LabelSeries data={data.filter(row => labelMap[`${row.pname}-${row.year}`])} style={{fontSize: '10px', fontFamily: 'sans-serif'}} getY={d => d.gameData[d.gameData.length - 1].y} getX={d => 82} labelAnchorX="start" getLabel={d => `${d.pname} - ${d.gameData[d.gameData.length - 1].y}`}/> <XAxis style={{ticks: {fontSize: '10px', fontFamily: 'sans-serif'}}} tickFormat={d => !d ? '1st game' : (!(d % 10) ? `${d}th` : '')}/>
</XYPlot>

Проницательный читатель будет наблюдать, что мы использовали пару констант, импортированных из отдельного файла constants.js, которые вытаскиваются для логической уверенности:

export const desiredLabels = [ {pname: 'Brian Taylor', year: 1980}, {pname: 'Mike Bratz', year: 1981}, {pname: 'Don Buse', year: 1982}, {pname: 'Mike Dunleavy', year: 1983}, {pname: 'Larry Bird', year: 1986}, {pname: 'Danny Ainge', year: 1988}, {pname: 'Michael Adams', year: 1989}, {pname: 'Michael Adams', year: 1990}, {pname: 'Vernon Maxwell', year: 1991}, {pname: 'Vernon Maxwell', year: 1992}, {pname: 'Dan Majerle', year: 1994}, {pname: 'John Starks', year: 1995}, {pname: 'Dennis Scott', year: 1996}, {pname: 'Reggie Miller', year: 1997}, {pname: 'Dee Brown', year: 1999}, {pname: 'Gary Payton', year: 2000}, {pname: 'Antoine Walker', year: 2001}, {pname: 'Jason Richardson', year: 2008}, {pname: 'Stephen Curry', year: 2013}, {pname: 'Stephen Curry', year: 2014}, {pname: 'Stephen Curry', year: 2015}, {pname: 'Stephen Curry', year: 2016}
];
export const layout = { height: 1000, width: 800, margin: {left: 20, right: 200, bottom: 100, top: 20}
};
export const NUMBER_OF_GAMES = 82;
export const MAX_NUMBER_OF_THREE_POINTERS = 405;

Объединяя все это, это выглядит так:

Довольно круто! Тем не менее, отзывчивость не очень хороша, строки не выделяются, когда мы приближаемся к ним, и браузер заметно отстает. Эта стратегия также не позволяет нам добавлять всплывающую подсказку вдоль стороны (хотя браузер мог бы сильно дергаться, когда мы наводим курсор на различные элементы). Должен быть лучший способ!

Лучшая архитектура

До сих пор мы проводили нашу серию с использованием линий SVG. Хотя этот подход позволяет легко рассуждать о состоянии пользовательского интерфейса, ДЕЙСТВИТЕЛЬНО неэффективно каждый раз перерисовывать все наши строки. Это связано с тем, что каждая из этих строк смоделирована как очень подробный узел DOM, который довольно сильно взвешивается в браузере. Чтобы облегчить этот вес, мы можем использовать встроенную полосу холста in-line, LineSeriesCanvas. Холст имеет тенденцию быть намного быстрее, чем рендеринг, чем SVG, но не имеет такого же детального представления в DOM, что означает, что любые взаимодействия должны быть сварены вручную. Очевидно, что падение этой новой серии в наше наивное решение ускорит работу всей страницы, но мы потеряем динамическую интерактивность.

Чтобы решить эту проблему, мы разделим наш график на два компонента: один, который обрабатывает интерактивность, и тот, который обрабатывает все остальное. Это мотивируется идеей мыслью, что React выполняет только функцию рендеринга для обновленных компонентов.

Благодаря этой архитектуре у нас будет компонент, который отображает линии холста, и тот, который отобразит выделенную линию и выделит всплывающую подсказку. Таким образом, эффективно разделение элементов, которые будут быстро отображать из тех, которые будут дорогостоящими или трудоемкими для рендеринга. Некоторые псевдо-коды для макета:

 <div className = "relative"> <NonInteractiveParts /> <InteractiveParts /> </ DIV> 

Мы хотим, чтобы эти компоненты выглядели как одна красивая диаграмма, поэтому мы предоставляем свойства css для интерактивных частей

position: absolute;
top: 0;

Это позволяет интерактивным частям «сидеть поверх» неинтерактивных свойств, тем самым завершая внешний вид.

Статические детали

Мы уже чего-то добились. Обратите внимание, что статические части диаграммы очень похожи на то, что мы имели в наивном подходе; просто контейнер с некоторыми сериями в нем. В интересах краткости мы можем комбинировать компонент корня и холст, проиллюстрированный выше, в один компонент, так как каждый из них отображается только один раз.

import React from 'react';
import {csv} from 'd3-fetch';
import {scaleLinear} from 'd3-scale';
import {interpolateWarm} from 'd3-scale-chromatic';
import { XYPlot, LineSeriesCanvas
} from 'react-vis'; import { cleanData, getDataDomain, buildVoronoiPoints
} from '../utils';
import { layout, NUMBER_OF_GAMES, MAX_NUMBER_OF_THREE_POINTERS
} from '../constants';
import InteractiveComponents from './interactive-parts'; export default class RootComponent extends React.Component { state = { loading: true, data: [], allPoints: null, playerYearMap: null, playerMap: null } componentWillMount() { csv('data/nyt-rip.csv') .then(data => { const updatedData = cleanData(data); const playerYearMap = updatedData.reduce((acc, row) => { const {pname, year, gameData} = row; acc[`${pname}-${year}`] = gameData[gameData.length - 1].y; return acc; }, {}); const playerMap = updatedData.reduce((acc, row) => { acc[`${row.pname}-${row.year}`] = row; return acc; }, {}); this.setState({ data: updatedData, loading: false, allPoints: buildVoronoiPoints(updatedData), playerYearMap, playerMap }); }); } render() { const {loading, data, allPoints, playerYearMap, playerMap} = this.state; if (loading) { return <div><h1>LOADING</h1></div>; } const dataDomain = getDataDomain(data); const domainScale = scaleLinear().domain(dataDomain).range([1, 0]); const colorScale = val => interpolateWarm(domainScale(val)); return ( <div className="relative"> <div> <XYPlot xDomain={[0, NUMBER_OF_GAMES]} yDomain={[0, MAX_NUMBER_OF_THREE_POINTERS + 1]} {...layout}> {data.map((player, idx) => ( <LineSeriesCanvas key={idx} strokeWidth={1} onNearestX={d => this.setState({crossvalue: d})} data={player.gameData} stroke={colorScale(player.gameData[player.gameData.length - 1].y)} />))} </XYPlot> </div> { // interactive components } <InteractiveComponents allPoints={allPoints} playerYearMap={playerYearMap} playerMap={playerMap} /> </div> ); }
}

Этот новый компонент очень похож на наш первый. Наш шаг по монтажу делает небольшую дополнительную работу, чтобы облегчить предоставление компонента интерактивности — в большей степени об этом в одно мгновение — и призывает к скорейшему превращению в InteractiveComponents. Но кроме этого мало что изменилось!

Интерактивная часть

Здесь идет классный материал. Мы увидели выше, что мы подготовили наши данные немного на этапе componentWillMount, чтобы подготовить его к интерактивному компоненту, давайте посмотрим более детально:

componentWillMount() { csv('data/nyt-rip.csv') .then(data => { const updatedData = cleanData(data); const playerYearMap = updatedData.reduce((acc, row) => { const {pname, year, gameData} = row; acc[`${pname}-${year}`] = gameData[gameData.length - 1].y; return acc; }, {}); const playerMap = updatedData.reduce((acc, row) => { acc[`${row.pname}-${row.year}`] = row; return acc; }, {}); this.setState({ data: updatedData, loading: false, allPoints: buildVoronoiPoints(updatedData), playerYearMap, playerMap }); }); }

Наши очищенные данные такие же, как и раньше, скрипучие и простые в использовании. Затем мы вводим новую переменную playerYearMap, это объект с ключами, равными уникальным идентификаторам игрового года, и значениям, равным максимальному количеству очков из трех указателей, которые достигли каждый игрок. Это будет использоваться для упрощения позиционирования меток и всплывающей подсказки. Аналогичным образом мы вводим playerMap, который также имеет идентификаторы игрового года в качестве ключей, но на этот раз всю строку в качестве значений. Это позволит быстро / постоянный поиск строк, когда мы наводим на вещи.

Последняя новая переменная называется allPoints и генерируется функцией buildVoronoiPoints. Чем она может быть интересна? Ну вот функция (она живет в утилях):

export function buildVoronoiPoints(data) { return data.reduce((acc, {player, pname, year, gameData}) => { return acc.concat({ player, pname, year, x: 41, y: gameData[gameData.length - 1].y }); }, []);
}

Это создает единую точку для каждого игрока в центре домена на «высоте» этого игрока, максимальное количество трех указателей. Мы можем использовать это для создания Voronoi. Voronoi — это разбиение пространственной плоскости такое, что для набора точек, где каждая точка содержится внутри собственной ячейки. Нам гарантируется, что каждая точка будет изолирована друг от друга. Это свойство может быть использовано для превосходного эффекта мыши, так что пользователь может навести курсор только на одну точку за раз.

Мы подражаем настройке мыши, найденной в оригинальной графике NYT, где плоскость разделяется на полосы, так что, когда вы нажимаете вверх и вниз, выбранные в текущий момент изменения игрока, и по мере того, как вы нажимаете мышь влево и вправо, он остается неизменным. Мы можем восстановить это поведение, используя Voronoi с нашими специально созданными allPoints из ранее. После внедрения макета ячеек voronoi будет выглядеть так:

Довольно круто! Теперь мы готовы увидеть код для интерактивного компонента.

import React from 'react';
import {scaleLinear} from 'd3-scale';
import { XYPlot, LineSeries, LabelSeries, XAxis, Voronoi, Hint
} from 'react-vis';
import debounce from 'debounce'; import { desiredLabels, layout, NUMBER_OF_GAMES, MAX_NUMBER_OF_THREE_POINTERS
} from '../constants'; export default class InteractiveComponents extends React.Component { state = { highlightSeries: null, highlightTip: null } componentWillMount() { this.debouncedSetState = debounce(newState => this.setState(newState), 40); } render() { const {allPoints, playerYearMap, playerMap} = this.props; const {highlightSeries, highlightTip} = this.state; const {height, width, margin} = layout; const x = scaleLinear() .domain([0, NUMBER_OF_GAMES]) .range([margin.left, width - margin.right]); const y = scaleLinear() .domain([0, MAX_NUMBER_OF_THREE_POINTERS]) .range([height - margin.top - margin.bottom, 0]); return ( <div className="absolute full"> <XYPlot onMouseLeave={() => this.setState({highlightSeries: null, highlightTip: null})} xDomain={[0, NUMBER_OF_GAMES]} yDomain={[0, MAX_NUMBER_OF_THREE_POINTERS + 1]} {...layout}> <LabelSeries labelAnchorX="start" data={desiredLabels.map(row => ({ ...row, y: playerYearMap[`${row.pname}-${row.year}`], yOffset: -12 }))} style={{fontSize: '10px', fontFamily: 'sans-serif'}} getX={d => NUMBER_OF_GAMES} getY={d => d.y} getLabel={d => `${d.pname.toUpperCase()}, ${d.year}`}/> <XAxis tickFormat={d => !d ? '1st game' : (!(d % 10) ? `${d}th` : '')}/> {highlightSeries && <LineSeries animation curve="" data={highlightSeries} color="black"/>} { highlightTip && <Hint value={{y: highlightTip.y, x: NUMBER_OF_GAMES}} align={{horizontal: 'right'}}> {`${highlightTip.name} ${highlightTip.y}`} </Hint> } <Voronoi extent={[ [0, y(MAX_NUMBER_OF_THREE_POINTERS)], [width, height - margin.bottom] ]} nodes={allPoints} polygonStyle={{ // UNCOMMENT BELOW TO SEE VORNOI // stroke: 'rgba(0, 0, 0, .2)' }} onHover={row => { const player = playerMap[`${row.pname}-${row.year}`]; if (!player) { this.setState({ highlightSeries: null, highlightTip: null }); return; } this.debouncedSetState({ highlightSeries: player.gameData, highlightTip: { y: player.gameData[player.gameData.length - 1].y, name: row.pname } }); }} x={d => x(d.x)} y={d => y(d.y)} /> </XYPlot> </div> ); }
}

Как и в нашем наивном подходе, давайте рассмотрим интересные части по одному. Начнем со слона в комнате:

<Voronoi extent={[ [0, y(MAX_NUMBER_OF_THREE_POINTERS)], [width, height - margin.bottom] ]} nodes={allPoints} polygonStyle={{ // UNCOMMENT BELOW TO SEE VORNOI stroke: 'rgba(0, 0, 0, .2)' }} onHover={row => { const player = playerMap[`${row.pname}-${row.year}`]; if (!player) { this.setState({ highlightSeries: null, highlightTip: null }); return; } this.debouncedSetState({ highlightSeries: player.gameData, highlightTip: { y: player.gameData[player.gameData.length - 1].y, name: row.pname } }); }} x={d => x(d.x)} y={d => y(d.y)} />

Этот безобидный компонент принимает список точек и строит voronoi прямо в DOM. Подпись немного аномальна по сравнению с другими компонентами в react-vis (вам нужно обеспечить ее масштабы, так что будьте осторожны! В этом компоненте стоит отметить функцию debouncedSetState, которую, как вы заметили выше, мы должны определить через:

componentWillMount() { this.debouncedSetState = debounce(newState => this.setState(newState), 40); }

Эта функция использует функцию lodash, называемую debounce, которая предотвращает вызов функции более определенной частоты (в этом случае каждые 40 мс). Предотвращение такого типа сверхбыстрого изменения состояния выгодно для нас, поскольку мы не хотим, чтобы каждый раз небольшое движение, которое пользователь предпринимает, чтобы вызвать изменение состояния. Это вызовет дрожание и ненужные перерисовывания! Чтобы замаскировать эту небольшую задержку, добавим анимацию, включив анимацию в LineSeries.

Объединяя все это, мы получаем:

Пара стилистических изменений, вот и все! Он работает плавно и точно воспроизводит взаимодействие, обнаруженное в первоначальной визуализации. Круто!

Еще кое-что

Мы неплохо справились с эмуляцией функциональности оригинальной визуализации NYT, но теперь мы хотим знать: можем ли мы сделать лучше? Мышь над функциональностью, которую они представляют, является разумной, однако она немного неуклюжа и не позволяет вам работать над каждым игроком из-за совпадений. Мы бы хотели, чтобы мы могли оптимизировать это разумным и эффективным образом. Ответ на эту проблему, и, действительно, если мы честны, большинство проблем — сделать наш Voronoi более нюансированным.

Наивный коленный рывок должен был моделировать клетки Voronoi для каждой точки данных. Хотя это вполне возможно сделать, это создало бы много ячеек. Если в сезоне 82 игры для 750 игроков, это будет 61 500 различных ячеек! Общее эмпирическое правило говорит, что браузер может обрабатывать не более пары тысяч элементов SVG, поэтому нам нужно быть более умными.

Мощным решением является разработка упрощенной модели нашего набора данных путем упрощения каждой из наших линий. Приятно, что есть здоровое тело на тему упрощения линии, например, когда-либо отличная библиотека Владимира Агафонкина / mourner ‘s simplify-js. Большая часть такого рода работ возникла из-за того, что картографы заинтересованы в том, чтобы упростить форму, сохраняя упрощения к оборванным краям береговых линий и другим нерегулярным географическим телам. Но мы будем использовать его для другого.

Мы упростим линию каждого игрока за год, чтобы мы получили суть их линии, не имея слишком много деталей. Мы выполняем эту идею, добавляя simplify-js для работы с нашими данными с другим дополнением к utils-файлу:

const betterThanCurryLine = simplify(stephCurryRecord, 0.5) .map(({x, y}) => ({x, y: y + 20, pname: 'Stephen Curry', year: 2016}));
const SIMPLICATION = 3;
export function buildVoronoiPointsWithSimplification(data) { return data.reduce((acc, {player, pname, year, gameData}) => { return acc.concat( simplify(gameData, SIMPLICATION).map(({x, y}) => ({player, pname, year, x, y})) ); }, betterThanCurryLine);
}

Будучи визуализаторами, это наш первый импульс, чтобы попытаться понять, как выглядят эти упрощения, и вуаля:

Со всеми этими частями на месте мы, наконец, готовы увидеть финальную voronoi в действии. Вот:

Заключение

В ходе этой статьи мы увидели, как восстановить интерактивную графику данных качества публикации с помощью react-vis. Это связано с использованием широкого спектра интерактивных методов визуализации, включая рендеринг холста, рендеринг одиночного прохода, debouncing, Voronois и упрощение линий в качестве механизма для отслеживания линейных рядов. Не для каждой визуализации требуется много оптимизаций и передовых методов, однако мы знаем много о наших данных и целевом поведении, поэтому мы можем настроить его так, как мы хотели.

При построении нашей визуализации мы широко использовали ряд функций реагирования, однако идеи, обсуждаемые в этой статье, применимы к любой веб-системе визуализации. Вы можете легко реализовать эти методы в ванильном d3, семиотическом или любом из огромного множества других инструментов визуализации.

Если вы хотите увидеть код в контексте, посетите этот репозиторий. Для получения дополнительных примеров ознакомьтесь с разделом диаграмм документации по взаимодействию или с моей личной работой, например этой, или этой, или даже этой. Счастливой визуализации!

Автор: Andrew McNutt

Источник: https://towardsdatascience.com/

Редакция: Команда webformyself.