Nivo предоставляет уровень кастомизации, при котором стандартные
визуальные примитивы могут быть полностью заменены пользовательскими
компонентами. В контексте столбчатых диаграмм ключевую роль играет
свойство barComponent из пакета @nivo/bar, позволяющее переопределить
отрисовку каждого отдельного столбца.
В стандартной реализации столбчатой диаграммы каждый бар формируется
внутренним компонентом библиотеки. Он получает вычисленные координаты,
размеры, цвет, а также метаданные исходного набора данных. При
использовании barComponent стандартный рендер полностью
заменяется пользовательской функцией или React-компонентом.
Ключевой момент заключается в том, что библиотека не перестаёт рассчитывать геометрию. Меняется только визуальное представление.
Каждый бар получает набор свойств, включающий:
x, y — позиция элементаwidth, height — геометрия столбцаcolor — вычисленный цвет из схемы или кастомной
палитрыdata — исходный объект серииvalue — числовое значениеborderRadius — скругления, если включеныstyle — итоговые SVG-стилиonClick, onMouseEnter,
onMouseLeave — обработчики событийДополнительно передаются внутренние метаданные, используемые системой анимаций и легенд.
Простейшая замена стандартного прямоугольника позволяет модифицировать внешний вид без изменения логики диаграммы.
import { ResponsiveBar } from '@nivo/bar'
const CustomBar = ({ x, y, width, height, color }) => {
return (
<rect
x={x}
y={y}
width={width}
height={height}
fill={color}
rx={6}
ry={6}
/>
)
}
export default function Chart() {
return (
<ResponsiveBar
data={[
{ country: 'A', value: 40 },
{ country: 'B', value: 70 },
]}
keys={['value']}
indexBy="country"
margin={{ top: 20, right: 20, bottom: 40, left: 40 }}
barComponent={CustomBar}
/>
)
}
В данном случае заменяется только SVG-элемент, вся логика масштабирования остаётся внутри библиотеки.
Часто требуется доступ к исходному объекту данных. Он передаётся
через data или вложенный datum.
const CustomBar = ({ x, y, width, height, color, data }) => {
const isHigh = data.value > 50
return (
<g>
<rect
x={x}
y={y}
width={width}
height={height}
fill={isHigh ? '#e74c3c' : color}
/>
<text
x={x + width / 2}
y={y - 6}
textAnchor="middle"
fontSize={12}
fill="#333"
>
{data.value}
</text>
</g>
)
}
В этом варианте добавляется логика условного форматирования и подписи значений непосредственно внутри баров.
При кастомизации важно учитывать, что библиотека управляет состояниями hover, active и focus. Эти состояния передаются через дополнительные свойства.
Пример использования hover-эффекта:
const CustomBar = ({ x, y, width, height, color, isInteractive, onMouseEnter, onMouseLeave }) => {
return (
<rect
x={x}
y={y}
width={width}
height={height}
fill={color}
opacity={isInteractive ? 1 : 0.7}
onMouseEn ter={onMouseEnter}
onMouseLe ave={onMouseLeave}
/>
)
}
Такой подход сохраняет встроенную интерактивность без потери производительности.
barComponent не ограничен одним элементом. Допускается возврат группы
(<g>), что позволяет добавлять декоративные
элементы.
const CustomBar = ({ x, y, width, height, color }) => {
return (
<g>
<rect x={x} y={y} width={width} height={height} fill={color} />
<circle
cx={x + width / 2}
cy={y}
r={4}
fill="#000"
/>
</g>
)
}
Подобный подход используется для визуальных маркеров, индикаторов тренда или сегментации.
В Nivo анимации управляются через внутренний motion layer. При
кастомном barComponent анимации не исчезают, если правильно
прокинуты props animatedProps или используется
motion-обёртка.
Пример с Framer Motion:
import { motion } from 'framer-motion'
const MotionRect = motion.rect
const CustomBar = ({ x, y, width, height, color }) => {
return (
<MotionRect
x={x}
y={y}
width={width}
height={height}
fill={color}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
/>
)
}
При этом важно избегать конфликтов между внутренними анимациями библиотеки и внешними motion-переходами.
barComponent позволяет реализовать сложные визуальные конструкции, включая сегментированные бары.
const CustomBar = ({ x, y, width, height, color, data }) => {
const segments = data.segments || []
return (
<g>
{segments.map((s, i) => (
<rect
key={i}
x={x}
y={y + i * (height / segments.length)}
width={width}
height={height / segments.length}
fill={s.color}
/>
))}
</g>
)
}
Такой подход используется для отображения распределённых значений внутри одного столбца.
События передаются библиотекой и могут быть использованы без дополнительной логики.
const CustomBar = ({ x, y, width, height, color, onClick }) => {
return (
<rect
x={x}
y={y}
width={width}
height={height}
fill={color}
onCl ick={() => onClick && onClick()}
style={{ cursor: 'pointer' }}
/>
)
}
Интерактивность сохраняет контекст выбранного элемента, включая индекс и серию.
import { ResponsiveBar } from '@nivo/bar'
const CustomBar = ({
x,
y,
width,
height,
color,
data,
onClick,
onMouseEnter,
onMouseLeave
}) => {
const isCritical = data.value > 80
return (
<g
onCl ick={onClick}
onMouseEn ter={onMouseEnter}
onMouseLe ave={onMouseLeave}
>
<rect
x={x}
y={y}
width={width}
height={height}
fill={isCritical ? '#c0392b' : color}
rx={4}
/>
<text
x={x + width / 2}
y={y + 14}
textAnchor="middle"
fontSize={11}
fill="#fff"
>
{data.value}
</text>
{isCritical && (
<circle
cx={x + width - 6}
cy={y + 6}
r={3}
fill="#fff"
/>
)}
</g>
)
}
export default function Chart() {
return (
<ResponsiveBar
data={[
{ country: 'A', value: 20 },
{ country: 'B', value: 90 },
{ country: 'C', value: 55 },
]}
keys={['value']}
indexBy="country"
barComponent={CustomBar}
margin={{ top: 20, right: 20, bottom: 40, left: 40 }}
/>
)
}
x, y,
width, height, что приводит к нарушению сетки
диаграммы