Кастомный рендер столбца через barComponent

Nivo предоставляет уровень кастомизации, при котором стандартные визуальные примитивы могут быть полностью заменены пользовательскими компонентами. В контексте столбчатых диаграмм ключевую роль играет свойство barComponent из пакета @nivo/bar, позволяющее переопределить отрисовку каждого отдельного столбца.

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

Ключевой момент заключается в том, что библиотека не перестаёт рассчитывать геометрию. Меняется только визуальное представление.

Структура данных, передаваемых в barComponent

Каждый бар получает набор свойств, включающий:

  • x, y — позиция элемента
  • width, height — геометрия столбца
  • color — вычисленный цвет из схемы или кастомной палитры
  • data — исходный объект серии
  • value — числовое значение
  • borderRadius — скругления, если включены
  • style — итоговые SVG-стили
  • onClick, onMouseEnter, onMouseLeave — обработчики событий

Дополнительно передаются внутренние метаданные, используемые системой анимаций и легенд.

Базовый пример кастомного barComponent

Простейшая замена стандартного прямоугольника позволяет модифицировать внешний вид без изменения логики диаграммы.

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-элемент, вся логика масштабирования остаётся внутри библиотеки.

Расширенная работа с datum и метаданными

Часто требуется доступ к исходному объекту данных. Он передаётся через 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}
        />
    )
}

Такой подход сохраняет встроенную интерактивность без потери производительности.

Использование группировки SVG-элементов

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 }}
        />
    )
}

Типичные ошибки при использовании barComponent

  • Игнорирование входных координат x, y, width, height, что приводит к нарушению сетки диаграммы
  • Полное отключение событий, из-за чего теряется интерактивность
  • Использование тяжёлой логики внутри рендера каждого бара, что снижает FPS
  • Конфликт SVG и HTML-слоёв при попытке внедрения DOM-элементов вместо SVG
  • Перекрытие внутренней анимации библиотеки внешними эффектами без синхронизации состояния