Во многих компонентах библиотеки Nivo точки данных
рендерятся стандартным SVG-элементом <circle>. Такой
подход удобен для базовой визуализации, однако быстро становится
ограничением при создании сложных интерфейсов:
Свойство nodeComponent позволяет полностью
переопределить рендер узла графика и внедрить собственный
React-компонент вместо стандартной точки.
Чаще всего nodeComponent используется в:
ResponsiveLineResponsiveScatterPlotResponsiveNetworkResponsiveSwarmPlotНаиболее востребован сценарий в ResponsiveLine.
nodeComponentСтандартный пример:
import { ResponsiveLine } from '@nivo/line'
const CustomPoint = ({ x, y, size, color }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<circle
r={size / 2}
fill={color}
stroke="#000"
strokeWidth={2}
/>
</g>
)
}
export const Chart = ({ data }) => {
return (
<ResponsiveLine
data={data}
nodeComponent={CustomPoint}
/>
)
}
Во время рендера графика Nivo вызывает пользовательский компонент для каждой точки данных.
В компонент передаётся большой набор параметров:
const CustomPoint = (props) => {
console.log(props)
return null
}
Типичный набор свойств:
{
x,
y,
size,
color,
borderWidth,
borderColor,
data,
animatedProps,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick
}
Свойства:
x
y
представляют итоговые координаты точки внутри SVG-контейнера.
Пример:
const CustomPoint = ({ x, y }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<circle r={6} fill="red" />
</g>
)
}
Группа <g> используется практически всегда,
поскольку позволяет удобно позиционировать содержимое через
transform.
Свойство data содержит исходные данные элемента.
Пример структуры:
{
x: '2025-01',
y: 120,
xFormatted: '2025-01',
yFormatted: '120'
}
Это позволяет строить условный рендеринг.
const CustomPoint = ({ x, y, data }) => {
const isCritical = data.y > 100
return (
<g transform={`translate(${x}, ${y})`}>
<circle
r={isCritical ? 12 : 6}
fill={isCritical ? '#ff0000' : '#3b82f6'}
/>
</g>
)
}
SVG поддерживает текстовые элементы.
const CustomPoint = ({ x, y, data }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<circle r={14} fill="#2563eb" />
<text
textAnchor="middle"
dominantBaseline="central"
fill="#fff"
fontSize={10}
>
{data.y}
</text>
</g>
)
}
const DiamondPoint = ({ x, y, color }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<rect
x={-8}
y={-8}
width={16}
height={16}
fill={color}
transform="rotate(45)"
/>
</g>
)
}
nodeComponent не ограничивается одной фигурой.
Полноценный составной узел:
const ComplexPoint = ({ x, y, color }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<circle
r={16}
fill="rgba(59,130,246,0.15)"
/>
<circle
r={8}
fill={color}
/>
<circle
r={3}
fill="#fff"
/>
</g>
)
}
Подобный подход позволяет создавать:
Nivo активно использует react-spring.
В nodeComponent доступны анимированные параметры.
Пример:
const AnimatedPoint = ({
animatedProps,
color
}) => {
return (
<animated.g
transform={animatedProps.transform}
>
<animated.circle
r={animatedProps.size}
fill={color}
/>
</animated.g>
)
}
import { useState } from 'react'
const HoverPoint = ({ x, y, color }) => {
const [hovered, setHovered] = useState(false)
return (
<g
transform={`translate(${x}, ${y})`}
onMouseEn ter={() => setHovered(true)}
onMouseLe ave={() => setHovered(false)}
>
<circle
r={hovered ? 12 : 6}
fill={color}
style={{
transition: 'all .2s ease'
}}
/>
</g>
)
}
Компонент получает стандартные обработчики событий.
const InteractivePoint = ({
x,
y,
data,
onClick
}) => {
return (
<g
transform={`translate(${x}, ${y})`}
onCl ick={(event) => {
onClick(data, event)
}}
>
<circle
r={8}
fill="#10b981"
/>
</g>
)
}
const CustomPoint = ({
x,
y,
data,
onMouseEnter,
onMouseLeave
}) => {
return (
<g
transform={`translate(${x}, ${y})`}
onMouseEn ter={(event) => {
onMouseEnter(data, event)
}}
onMouseLe ave={onMouseLeave}
>
<circle
r={8}
fill="#6366f1"
/>
</g>
)
}
SVG не умеет напрямую рендерить HTML, однако можно применять
foreignObject.
const HtmlPoint = ({ x, y, data }) => {
return (
<foreignObject
x={x - 20}
y={y - 20}
width={40}
height={40}
>
<div
style={{
width: 40,
height: 40,
borderRadius: '50%',
background: '#2563eb',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12
}}
>
{data.y}
</div>
</foreignObject>
)
}
При большом количестве точек кастомный рендер способен серьёзно нагружать React.
Особенно опасны:
React.memoimport React from 'react'
const CustomPoint = React.memo(
({ x, y, color }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<circle
r={6}
fill={color}
/>
</g>
)
}
)
Плохой вариант:
<g>
<circle />
<circle />
<circle />
<circle />
<circle />
</g>
Хороший вариант:
<circle />
На графиках с тысячами точек разница становится критичной.
Распространённая ошибка:
<circle
cx={x}
cy={y}
/>
вместо:
<g transform={`translate(${x}, ${y})`}>
<circle />
</g>
Использование <g> делает компонент более
масштабируемым и упрощает композицию.
Свойство:
size
обычно представляет диаметр точки.
Правильный радиус:
r={size / 2}
const AvatarPoint = ({ x, y, data }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<clipPath id={`clip-${data.x}`}>
<circle r={14} />
</clipPath>
<image
href={data.avatar}
x={-14}
y={-14}
width={28}
height={28}
clipPath={`url(#clip-${data.x})`}
/>
</g>
)
}
SVG поддерживает фильтры.
const ShadowPoint = ({ x, y }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<defs>
<filter id="shadow">
<feDropShadow
dx="0"
dy="2"
stdDeviation="3"
floodOpacity="0.35"
/>
</filter>
</defs>
<circle
r={10}
fill="#3b82f6"
filter="url(#shadow)"
/>
</g>
)
}
const GradientPoint = ({ x, y }) => {
return (
<g transform={`translate(${x}, ${y})`}>
<defs>
<radialGradient id="pointGradient">
<stop
offset="0%"
stopColor="#fff"
/>
<stop
offset="100%"
stopColor="#2563eb"
/>
</radialGradient>
</defs>
<circle
r={10}
fill="url(#pointGradient)"
/>
</g>
)
}
import React, { memo, useState } from 'react'
import { ResponsiveLine } from '@nivo/line'
const CustomNode = memo((props) => {
const {
x,
y,
color,
size,
data
} = props
const [hovered, setHovered] = useState(false)
const radius = hovered
? size
: size / 2
const isDanger = data.y > 80
return (
<g
transform={`translate(${x}, ${y})`}
onMouseEn ter={() => setHovered(true)}
onMouseLe ave={() => setHovered(false)}
style={{
cursor: 'pointer'
}}
>
<circle
r={radius + 6}
fill={
isDanger
? 'rgba(239,68,68,.2)'
: 'rgba(59,130,246,.2)'
}
/>
<circle
r={radius}
fill={
isDanger
? '#ef4444'
: color
}
stroke="#fff"
strokeWidth={2}
style={{
transition: 'all .2s ease'
}}
/>
<text
y={-18}
textAnchor="middle"
fontSize={11}
fill="#111827"
>
{data.y}
</text>
</g>
)
})
export const Chart = ({ data }) => {
return (
<ResponsiveLine
data={data}
enablePoints={true}
pointSize={12}
nodeComponent={CustomNode}
margin={{
top: 50,
right: 50,
bottom: 50,
left: 60
}}
/>
)
}
nodeComponent особенно полезенПозволяет отображать:
Подходит для:
Используется для:
Позволяет рендерить:
nodeComponentНесмотря на гибкость, кастомный рендер имеет ограничения.
SVG плохо масштабируется на десятках тысяч элементов.
При огромных объёмах данных лучше использовать:
Каждая точка становится отдельным React-компонентом.
1000 точек = 1000 React-компонентов.
Некоторые CSS-возможности работают иначе:
z-index;| Возможность | Стандартные точки | nodeComponent |
|---|---|---|
| Цвет | Да | Да |
| Размер | Да | Да |
| SVG-фигуры | Ограниченно | Полностью |
| HTML-вставки | Нет | Да |
| Анимации | Базовые | Полностью |
| React-логика | Нет | Да |
| Состояния | Нет | Да |
| События | Частично | Полностью |
| Аватары | Нет | Да |
| Сложные композиции | Нет | Да |
На production-проектах nodeComponent обычно применяется
для:
Именно nodeComponent превращает Nivo из библиотеки
типовых графиков в полноценный инструмент кастомной визуализации на
React.