Диаграммы рассеяния и пузырьковые диаграммы в библиотеке Nivo предназначены для отображения взаимосвязей между числовыми значениями. Визуализация становится особенно полезной при добавлении интерактивности: всплывающих подсказок, реакций на наведение, выделения точек, масштабирования и пользовательских обработчиков событий.
Компонент ScatterPlot в Nivo построен поверх SVG или Canvas и поддерживает множество механизмов взаимодействия без необходимости вручную работать с DOM-событиями.
Базовый интерактивный график:
import { ResponsiveScatterPlot } from '@nivo/scatterplot'
const data = [
{
id: 'dataset',
data: [
{ x: 10, y: 20 },
{ x: 15, y: 35 },
{ x: 25, y: 10 },
],
},
]
export default function Chart() {
return (
<div style={{ height: 500 }}>
<ResponsiveScatterPlot
data={data}
margin={{ top: 40, right: 40, bottom: 70, left: 90 }}
xScale={{ type: 'linear', min: 0, max: 30 }}
yScale={{ type: 'linear', min: 0, max: 40 }}
axisBottom={{
legend: 'X',
legendPosition: 'middle',
legendOffset: 46,
}}
axisLeft={{
legend: 'Y',
legendPosition: 'middle',
legendOffset: -60,
}}
colors={{ scheme: 'category10' }}
nodeSize={10}
useMesh={true}
/>
</div>
)
}
useMeshОдной из важнейших возможностей интерактивности является свойство
useMesh.
useMesh={true}
При включении mesh-слоя библиотека создаёт невидимую интерактивную сетку поверх графика. Это значительно повышает точность наведения курсора на точки.
Без mesh пользователь должен попасть точно в SVG-элемент точки. При большом количестве данных или маленьком размере узлов взаимодействие становится неудобным.
Преимущества mesh:
ScatterPlot поддерживает стандартные обработчики событий React.
<ResponsiveScatterPlot
data={data}
onCl ick={(node, event) => {
console.log(node)
}}
/>
Объект node содержит:
{
id,
serieId,
data,
x,
y,
color
}
Пример открытия карточки объекта:
onCl ick={(node) => {
setSelectedUser(node.data.user)
}}
onMouseEn ter={(node) => {
console.log('hover', node)
}}
onMouseLe ave={(node) => {
console.log('leave', node)
}}
Часто используется для:
onContextM enu={(node, event) => {
event.preventDefault()
console.log('context menu')
}}
Подобный подход применяется в аналитических системах и BI-панелях.
Tooltip — основной механизм отображения информации о точке.
Стандартный tooltip:
enableTooltip={true}
Nivo автоматически показывает:
Наиболее распространённый сценарий — создание собственного компонента.
const CustomTooltip = ({ node }) => {
return (
<div
style={{
background: 'white',
padding: 12,
border: '1px solid #ccc',
}}
>
<strong>{node.serieId}</strong>
<div>X: {node.data.x}</div>
<div>Y: {node.data.y}</div>
</div>
)
}
Подключение:
<ResponsiveScatterPlot
tooltip={CustomTooltip}
/>
Каждая точка может содержать произвольные поля.
const data = [
{
id: 'users',
data: [
{
x: 10,
y: 20,
name: 'Alex',
country: 'Germany',
sales: 1400,
},
],
},
]
Использование в tooltip:
const CustomTooltip = ({ node }) => (
<div>
<div>{node.data.name}</div>
<div>{node.data.country}</div>
<div>{node.data.sales}</div>
</div>
)
Bubble-диаграммы используют размер точки как дополнительное измерение данных.
nodeSize={(node) => node.data.sales / 100}
Полный пример:
<ResponsiveScatterPlot
data={data}
nodeSize={(node) => node.data.population / 1000000}
/>
В результате:
Размер пузыря можно менять при наведении.
nodeSize={(node) =>
hoveredId === node.id ? 30 : 12
}
Пример с состоянием:
const [hoveredId, setHoveredId] = useState(null)
<ResponsiveScatterPlot
data={data}
nodeSize={(node) =>
hoveredId === node.id ? 25 : 10
}
onMouseEn ter={(node) => {
setHoveredId(node.id)
}}
onMouseLe ave={() => {
setHoveredId(null)
}}
/>
Подобный эффект часто применяется в:
Цвет точки также может меняться динамически.
nodeColor={(node) =>
selectedId === node.id
? '#ff0000'
: node.color
}
Пример выбора точки:
const [selectedId, setSelectedId] = useState(null)
<ResponsiveScatterPlot
data={data}
onCl ick={(node) => {
setSelectedId(node.id)
}}
nodeColor={(node) =>
selectedId === node.id
? '#ff5722'
: '#468df3'
}
/>
layersNivo позволяет полностью переопределять визуальные слои.
Стандартные слои:
layers={[
'grid',
'axes',
'nodes',
'markers',
'mesh',
'legends',
]}
Можно добавить собственный слой:
const CustomLayer = ({ nodes }) => {
return (
<g>
{nodes.map(node => (
<text
key={node.id}
x={node.x}
y={node.y - 10}
textAnchor="middle"
>
{node.data.label}
</text>
))}
</g>
)
}
Подключение:
layers={[
'grid',
'axes',
'nodes',
CustomLayer,
'mesh',
]}
Подписи можно показывать только при наведении.
const LabelsLayer = ({ nodes }) => {
return (
<g>
{nodes.map(node => {
if (hoveredId !== node.id) {
return null
}
return (
<text
key={node.id}
x={node.x}
y={node.y - 15}
>
{node.data.name}
</text>
)
})}
</g>
)
}
Nivo использует библиотеку React Spring.
Включение анимации:
animate={true}
motionConfig="gentle"
Доступные пресеты:
defaultgentlewobblystiffslowmolassesПример:
<ResponsiveScatterPlot
data={data}
animate={true}
motionConfig="wobbly"
/>
Bubble-график обычно использует:
Пример:
const data = [
{
id: 'countries',
data: [
{
x: 82,
y: 91,
population: 145000000,
gdp: 18000,
},
],
},
]
Размер:
nodeSize={(node) =>
Math.sqrt(node.data.population) / 500
}
Цвет:
nodeColor={(node) =>
node.data.gdp > 20000
? '#2e7d32'
: '#c62828'
}
Иногда требуется подсветить целую серию.
nodeOpacity={(node) =>
activeSerie === node.serieId ? 1 : 0.2
}
Наведение на легенду:
legends={[
{
anchor: 'bottom-right',
direction: 'column',
itemDirection: 'left-to-right',
translateX: 130,
itemWidth: 100,
itemHeight: 12,
effects: [
{
on: 'hover',
style: {
itemOpacity: 1,
},
},
],
},
]}
Легенды могут выступать элементом управления графиком.
Пример фильтрации:
const [hiddenSeries, setHiddenSeries] = useState([])
const filteredData = data.filter(
item => !hiddenSeries.includes(item.id)
)
Переключение:
onCl ick={(serie) => {
setHiddenSeries(prev =>
prev.includes(serie.id)
? prev.filter(id => id !== serie.id)
: [...prev, serie.id]
)
}}
Для больших объёмов данных рекомендуется Canvas-компонент.
import {
ResponsiveScatterPlotCanvas
} from '@nivo/scatterplot'
Canvas особенно эффективен при:
Пример:
<ResponsiveScatterPlotCanvas
data={data}
pixelRatio={window.devicePixelRatio}
useMesh={true}
/>
Canvas быстрее SVG, но имеет ограничения:
| Возможность | SVG | Canvas |
|---|---|---|
| CSS-стилизация | Да | Нет |
| DOM-доступ | Да | Нет |
| SVG-анимации | Да | Нет |
| Производительность | Средняя | Высокая |
Однако Nivo реализует систему виртуального hit detection, поэтому tooltip и hover продолжают работать.
renderNodeПолный контроль над отображением точки.
const CustomNode = ({
node,
x,
y,
size,
color
}) => {
return (
<g transform={`translate(${x}, ${y})`}>
<circle
r={size / 2}
fill={color}
/>
<text
y={-20}
textAnchor="middle"
>
{node.data.label}
</text>
</g>
)
}
Подключение:
renderNode={CustomNode}
Пример свечения:
const CustomNode = ({
node,
x,
y,
size,
}) => {
const isHovered = hoveredId === node.id
return (
<g transform={`translate(${x}, ${y})`}>
<circle
r={size / 2}
fill="#1976d2"
filter={
isHovered
? 'url(#shadow)'
: undefined
}
/>
</g>
)
}
Добавление glow-эффекта:
<defs>
<filter id="shadow">
<feDropShadow
dx="0"
dy="0"
stdDeviation="4"
floodColor="#2196f3"
/>
</filter>
</defs>
ScatterPlot можно синхронизировать с другими визуализациями.
Пример:
const [selectedPoint, setSelectedPoint] =
useState(null)
Обработка:
onMouseEn ter={(node) => {
setSelectedPoint(node.id)
}}
Передача в другой график:
<BarChart
activeId={selectedPoint}
/>
Такой подход применяется в:
Nivo не предоставляет встроенный zoom для ScatterPlot, но интеграция возможна через D3 Zoom.
Подключение:
npm install d3-zoom
Базовая интеграция:
import { zoom } from 'd3-zoom'
import { sel ect } from 'd3-selection'
Инициализация:
useEffect(() => {
const svg = sel ect(svgRef.current)
svg.call(
zoom().on('zoom', (event) => {
console.log(event.transform)
})
)
}, [])
Brush позволяет выделять диапазон точек.
Чаще всего используется библиотека:
npm install d3-brush
Пример:
import { brush } fr om 'd3-brush'
Создание brush:
brush().on('end', (event) => {
console.log(event.selection)
})
При работе с большими наборами данных важно:
nodeSize.Пример оптимизации:
const memoizedData = useMemo(() => {
return transformData(rawData)
}, [rawData])
Мемоизация tooltip:
const CustomTooltip = React.memo(({ node }) => {
return (
<div>
{node.data.label}
</div>
)
})
Nivo поддерживает accessibility-атрибуты.
isInteractive={true}
ARIA-подписи:
ariaLabel="Sales scatter plot"
Описание точек:
nodeAriaLabel={(node) =>
`${node.serieId}: x ${node.data.x}, y ${node.data.y}`
}
Это особенно важно для:
Изменение курсора:
theme={{
tooltip: {
container: {
cursor: 'pointer',
},
},
}}
Через custom node:
<circle
style={{
cursor: 'pointer',
}}
/>
Большинство интерактивных сценариев строится вокруг состояния.
Пример:
const [hoveredNode, setHoveredNode] =
useState(null)
const [selectedNode, setSelectedNode] =
useState(null)
Далее состояние используется для:
import { ResponsiveScatterPlot } fr om '@nivo/scatterplot'
import { useState } from 'react'
export default function BubbleChart() {
const [selectedId, setSelectedId] =
useState(null)
const data = [
{
id: 'countries',
data: [
{
x: 75,
y: 90,
population: 145,
label: 'Country A',
id: 'a',
},
{
x: 55,
y: 45,
population: 80,
label: 'Country B',
id: 'b',
},
],
},
]
return (
<div style={{ height: 600 }}>
<ResponsiveScatterPlot
data={data}
margin={{
top: 60,
right: 140,
bottom: 70,
left: 90,
}}
xScale={{
type: 'linear',
min: 0,
max: 100,
}}
yScale={{
type: 'linear',
min: 0,
max: 100,
}}
nodeSize={(node) =>
node.data.population
}
nodeColor={(node) =>
selectedId === node.id
? '#ff5722'
: '#1976d2'
}
tooltip={({ node }) => (
<div
style={{
background: '#fff',
padding: 12,
border: '1px solid #ccc',
}}
>
<strong>
{node.data.label}
</strong>
<div>
Population:
{node.data.population}
</div>
</div>
)}
onCl ick={(node) => {
setSelectedId(node.id)
}}
useMesh={true}
animate={true}
/>
</div>
)
}