НОВОСТИ Визуализация сложных данных с использованием D3 и React

NewsBot
Оффлайн

NewsBot

.
.
Регистрация
21.07.20
Сообщения
40.408
Реакции
1
Репутация
0
Существует много возможный вариантов реализации сложных графиков в ваших проектах. Я за несколько лет попробовал все возможные варианты. Сначала это были готовые библиотеки типа . AmCharts сразу же оказался большим и неповоротливым. После этого были более гибкие и дружелюбные библиотеки, такие как . Recharts был поначалу очень хорош, но со временем сложные фичи создавались такими костылями, которые даже показывать стыдно, а какие-то фичи и вовсе были невозможны в реализации. Таким образом, я пришел к и решаю на нем любые задачи, связанные с графиками. Иногда это занимает немного больше времени по сравнению с готовыми инструментами. Но остается одно неоспоримое преимущество – мы всегда знаем, что никогда не упремся в рамки и ваш код не захочется отправить в помойку через пару месяцев.


Какая цель этой статьи? Я хочу рассказать вам про крутой инструмент и о том, как его максимально эффективно использовать в связке с React. Мы последовательно разберем универсальный рецепт для построения компонентов любой сложности.


78htfcgddh1cilpxtx8f7_i9shy.jpeg



Оглавление











Посмотреть на результат (спойлер)

Сложные данные



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


Пару примеров эффективного восприятия информации:

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

Что из себя представляет D3



D3.js – это javaScript библиотека для обработки и визуализации данных. Она включает в себя функции для масштабирования, утилиты для манипуляции с данными и DOM-узлами.


При этом, скажу сразу, что большая часть этой библиотеки уже устарела, и ее не стоит использовать. Именно ту часть, где идут манипуляции с DOM узлами, эту задачу мы будем максимально перекладывать на React.

1. Абстрагирование от физических размеров



Я не просто так начинаю с этого пункта. Самое первое, что необходимо сделать, когда приступаем к разработке графика – это абстрагироваться от физических размеров. Ну скажем, чтобы мы могли получать координаты точек и сразу же записывать их в атрибуты. Соответственно, нам нужны какие-то методы, которые получают на вход значение или название категории, а на выходе отдают координаты в пикселях.


getY(`значение`); \\ возвращает координату по оси y в пикселях
getX(`название категории`); \\ возвращает координату по оси x в пикселях


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


К счастью в D3 это сделать очень просто.

Получение координат по оси Y (ось значения)



На изображении показано положение точек из массива [4, 15, 28, 35, 40] в контейнере выстой 300px:


rbqy_cpww0vupodm2bxswnwp7vo.png



Теперь посмотрите как с помощью D3 создать функцию для получения физических координат для отрисовки этих точек:


const getY = d3.scaleLinear()
.domain([0, 40])
.range([300, 0]);


Мы создаем функцию getY с помощью D3 функции scaleLinear(). В метод domain передаем область данных, а в range передаем физические размеры от 300px до 0px. Так как в svg отчет начинается с левого верхнего угла, то нужно именно в таком порядке передавать аргументы в range – сначала 300, потом 0.


Мы только один раз работаем с физическими размерами, когда создаем эту функцию и передаем в нее высоту графика. После этого мы работаем только с реальными данными и сразу же выводим полученные размеры в svg атрибуты.


Пример применения функции getY:


getY(4); // 270
getY(15); // 187.5
getY(28); // 90
getY(35); // 37.5
getY(40); // 0


В качестве аргумента мы передаем значение, а на выходе получаем координату по оси y. Обратите внимание, что это отступ сверху контейнера.

Получение координат по оси X (ось категории)



Аналогичная ситуация по оси X. Мы хотим один раз подвязаться к категориям, а дальше передавать название категории и получать ее координаты.


На изображении мы видим контейнер шириной 600px и 5 месяцев. Месяца будут служить подписями по оси X:


16ahf9aop0vlep1u5jpfrm5g7bg.png



Создадим такую функцию:


const getX = d3.scaleBand()
.domain(['Jan', 'Feb', 'Mar', 'Apr', 'May'])
.range([0, 600]);


Мы используем функцию scaleBand из D3. В domain мы передаем все возможные категории в нужном порядке, а в range область, выделенную под график.




Смотрим пример применения нашей функции getX:


getX('Jan'); // 0
getX('Feb'); // 120
getX('Mar'); // 240
getX('Apr'); // 360
getX('May'); // 480


В качестве аргумента мы передаем название категории, а на выходе получаем координату по оси X (отступ слева).

2. Отрисовка простых фигур



С использованием наших функций для получения координат мы уже можем рисовать простые фигуры на координатной плоскости. К простым фигурам в текущем контексте я отношу:

  • rect — прямоугольник;
  • circle — круг;

  • line — линия;
  • text — обычный блок текста.


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

Точки



Для примера попробуем нарисовать точки с использованием svg-фигуры circle:



const data = [
{ name: 'Jan', value: 40 },
{ name: 'Feb', value: 35 },
{ name: 'Mar', value: 4 },
{ name: 'Apr', value: 28 },
{ name: 'May', value: 15 },
];

return (

{data.map((item, index) => {
return (
2}
cy={getY(item.value)}
r={4}
fill="#7cb5ec"
/>
);
})}

);


Фигура circle абсолютно примитивна. В данном случае она принимает координаты центра – cx, cy, радиус r и цвет заливки fill.


Здесь мы использовали новый метод bandwidth:


getX.bandwidth()


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


getX(item.name) + getX.bandwidth() / 2


Вот, что у нас получится в результате:


yjg6lh4kpnj_nif2k76fmfq2kae.png


Подписи



Для создания текстовых узлов в svg используется фигура text. Она также принимает координаты и содержит свои личные атрибуты для стилизации.


Подпишем значения на наших точках:


return (

{data.map((item, index) => {
return (


2}
y={getY(item.value) - 10}
textAnchor="middle"
>
{item.value}


);
})}

);


Что здесь нового? Мы обернули наш круг и текст элементом g. Элемент g один из самых распространенных в svg, обычно он просто группирует элементы и двигает их вместе при необходимости через свойство transform.


Вот как выглядят наши подписи к точкам:



b_wfqtcb_syz19bfjpdu65woisg.png


3. Оси



Для осей существуют готовые элементы в D3.


const getYAxis = ref => {
const yAxis = d3.axisLeft(getY);
d3.select(ref).call(yAxis);
};

const getXAxis = ref => {
const xAxis = d3.axisBottom(getX);
d3.select(ref).call(xAxis);
};

return (


/ нужно сдвинуть ось в самый низ svg
/>
...

);


Вот что получается, если ничего не менять и не настраивать:



twlttycws0eh6-bmhht645zitbm.png





Попробуем добавить немного красоты и переопределим изначальные стили:




const getYAxis = ref => {
const yAxis = d3.axisLeft(getY)
.tickSize(-600) // ширина горизонтальных линий на графике
.tickPadding(7); // отступ значений от самого графика
d3.select(ref).call(yAxis);
};

const getXAxis = ref => {
const xAxis = d3.axisBottom(getX);
d3.select(ref).call(xAxis);
};

return (




...

);


И немного стилей:


.axis {
color: #ccd6eb;
& text {
color: #666;
}
& .domain {
display: none;
}
}

.xAxis {
& line {
display: none;
}
}


Посмотрим как сейчас выглядит наш пример:


73hturpehfi2ifrchpzpieq6g5q.png


4. Отрисовка сложных фигур



У svg нет каких-то встроенных простых методов для построения кривых по точкам, секций круга и так далее. Это достаточно сложный процесс на низком уровне. D3 предоставляет методы для построения таких сложных фигур.

Кривые линии



Начнем с обычной кривой линии, для которой мы уже построили точки:


const linePath = d3
.line()
.x(d => getX(d.name) + getX.bandwidth() / 2)
.y(d => getY(d.value))
.curve(d3.curveMonotoneX)(data);

// M60,0C100,6.25,140,12.5,180,37.5C220,62.5,260,270,300,270C340,270,380,90,420,90C460,90,500,138.75,540,187.5


В качестве аргумента line() мы передаем наш массив с данными data, а D3 уже под капотом проходится по этому массиву и вызывает функции для поиска координат, которые мы передали в методы x и y. В curve мы передаем тип линии, в данном случае это curveNatural (таких типов достаточно много).



Теперь немного разберем полученную строку. Команда M используется в строки для указания точки, откуда нужно начать рисовать. Команда С — это кубическая кривая Безье, которая принимает три набора координат, по которым строит кривую. Подробнее можно почитать здесь — .


Теперь просто вставляем полученную строку в качестве атрибута d для элемента path:


return (






);


Path – одна из самых распространенных фигур в svg из которой можно сделать практически что угодно. Мы еще будем использовать эту фигуру дальше.


Смотрим на результат:





1gofjcz1u9ra7fdqezm6zmrzwze.png


Замкнутые области



Теперь мы попробуем построить замкнутую области с одной кривой стороной. Она будет использоваться в качестве заливки для графика.


В построении области с кривой стороной похожая ситуация, как и с кривой линией. Здесь используется функция area, а методов становится больше, потому что нужно передать отдельно функцию для поиска нижней линии. Если нам нужна прямая нижняя линия, то просто передаем нулевое значение по низу.


const areaPath = d3.area()
.x(d => getX(d.name) + getX.bandwidth() / 2)
.y0(d => getY(d.value))
.y1(() => getY(0))
.curve(d3.curveMonotoneX)(data);

// M60,300C100,300,140,300,180,300C220,300,260,300,300,300C340,300,380,300,420,300C460,300,500,300,540,300L540,187.5C500,138.75,460,90,420,90C380,90,340,270,300,270C260,270,220,62.5,180,37.5C140,12.5,100,6.25,60,0Z


На выходе также получаем путь, который нужно передать в фигуру path. Здесь в конце пути появляется новая команда Z, которая замыкает контур, рисуя прямую линию от текущего положения обратно к первой точке пути. А также в середине строки есть команда L, которая рисует прямую линию от текущей точки.


Добавляем полученную строку в path:


return (






);


Смотрим на нашу красоту:




p8o7p039pwmrxbw2w9v0sacfn4u.png


5. События



Мы игнорируем все методы для навешивания событий из D3. Эту задачу мы также перекладываем на React и вешаем все события прям в разметке JSX. А для хранения состояний используем знакомый всем хук .

Эффект наведения



Подробнее рассмотрим эффект наведения, остальные события делаются аналогично.


Наша задача сделать эффект увеличения точки при наведении на всю область категории. Так как у нас нет определенного прямоугольника в DOM, на которое можно повесить событие напрямую, то мы будем вешать событие на всю svg, а затем вычислять позицию.


Но для начало заведем состояние активной категории:




// null – если ничего не активно (по умолчанию)
const [activeIndex, setActiveIndex] = useState(null);


После этого пишем наш обработчик:


const handleMouseMove = (e) => {
const x = e.nativeEvent.offsetX; // количество пикселей от левого края svg
const index = Math.floor(x / getX.step()); // делим количество пикселей на ширину одной колонки и получаем индекс
setActiveIndex(index); // обновляем наше состояние
};

return (



)


И добавим событие, которое будет сбрасывать активный индекс, когда мы убираем мышку с svg:


const handleMouseMove = (e) => { … };

const handleMouseLeave = () => {
setActiveIndex(null);
};

return (



)


Рабочее состояние есть, теперь просто говорим, что нужно рисовать если индекс активный, а что, если нет:


data.map((item, index) => {
return (

2}
cy={getY(item.value)}
r={index === activeIndex ? 6 : 4} // при наведении просто немного увеличиваем круг
fill="#7cb5ec"
strokeWidth={index === activeIndex ? 2 : 0} // обводка появляется только при наведении
stroke="#fff" // добавили белый цвет для обводки
style={{ transition: `ease-out .1s` }}
/>


);
})



И теперь смотрим на результат:


wrxsrdvgl-gocilnkmam1uhgbhq.gif


Итог



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



D3 – это мощный инструмент, но существенная часть библиотеки устарела, поэтому нужно выбирать те вещи, которые действительно нам облегчат жизнь. Соответственно, мы берем из D3 только функции для масштабирования и методы для создания сложных фигур.


Мы выкидываем из D3 все устаревшие методы для прямой манипуляции элементами DOMа и делам это как знали и умели до этого.


В интернете будет много примеров, которые будут сбивать вас с толку и заставлять писать в стиле jQuery, будьте внимательны. Надеюсь эта статья вам поможет сделать всё красиво!
 
Сверху Снизу