Кастомные аннотации через renderAnnotation

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

Аннотации в Nivo представляют собой слой поверх графика, который не влияет на данные, но добавляет смысловые визуальные акценты. Они применяются для:

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

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

Сигнатура renderAnnotation

Во многих компонентах Nivo, таких как ResponsiveLine, ResponsiveBar и других, renderAnnotation передается как функция:

renderAnnotation: (annotation) => ReactNode

Объект annotation содержит данные, необходимые для построения элемента:

  • x, y или координаты в масштабе графика;
  • size или геометрические параметры;
  • datum — исходная точка данных;
  • serieId — идентификатор серии;
  • borderColor, color — стилистические параметры;
  • note — текстовое описание (если задано);
  • дополнительные кастомные поля, переданные пользователем.

Базовая структура кастомной аннотации

Кастомная аннотация строится как React-компонент, возвращаемый функцией:

const MyAnnotation = ({ x, y, note }) => {
    return (
        <g transform={`translate(${x}, ${y})`}>
            <circle r={6} fill="#ff5500" />
            <text
                x={10}
                y={-10}
                fontSize={12}
                fill="#333"
            >
                {note}
            </text>
        </g>
    );
};

Использование в графике:

<ResponsiveLine
    data={data}
    margin={{ top: 50, right: 50, bottom: 50, left: 60 }}
    annotations={[
        {
            type: 'point',
            x: 10,
            y: 100,
            note: 'Пиковое значение'
        }
    ]}
    renderAnnotation={(annotation) => <MyAnnotation {...annotation} />}
/>

Геометрия и система координат

Nivo использует внутреннюю систему координат SVG, где аннотации позиционируются относительно уже отрендеренного графика. Это означает:

  • координаты x и y уже преобразованы через шкалы (scales);
  • аннотация не работает с “сырыми” данными напрямую;
  • позиционирование всегда происходит в пикселях SVG-контейнера.

Для сложных случаев важно учитывать смещения margin, так как они влияют на итоговую позицию.

<g transform={`translate(${x + offsetX}, ${y + offsetY})`}>

Расширенные аннотации с интерактивностью

Аннотации могут включать интерактивные элементы: hover-эффекты, кликабельные зоны, динамическое изменение состояния.

const InteractiveAnnotation = ({ x, y, datum }) => {
    const [active, setActive] = useState(false);

    return (
        <g
            transform={`translate(${x}, ${y})`}
            onMouseEn ter={() => setActive(true)}
            onMouseLe ave={() => setActive(false)}
        >
            <circle r={active ? 10 : 6} fill={active ? '#1f77b4' : '#999'} />
            {active && (
                <text x={12} y={-12} fontSize={11}>
                    {datum.label}
                </text>
            )}
        </g>
    );
};

Такой подход позволяет превращать аннотации в полноценные элементы интерфейса, а не только визуальные маркеры.

Использование сложной графики в аннотациях

renderAnnotation не ограничивается базовыми SVG-элементами. Внутри можно использовать:

  • foreignObject для встраивания HTML;
  • иконки из библиотек (например, lucide или material icons);
  • мини-графики;
  • индикаторы состояния.

Пример с HTML-вставкой:

const HtmlAnnotation = ({ x, y, note }) => {
    return (
        <foreignObject x={x} y={y} width={120} height={60}>
            <div style={{
                background: 'white',
                border: '1px solid #ddd',
                padding: '6px',
                borderRadius: '6px'
            }}>
                <strong>{note}</strong>
            </div>
        </foreignObject>
    );
};

Работа с несколькими аннотациями

При наличии массива аннотаций renderAnnotation вызывается для каждой из них. Это позволяет строить сложные системы меток:

annotations={[
    { x: 5, y: 20, note: 'Старт' },
    { x: 15, y: 80, note: 'Рост' },
    { x: 30, y: 40, note: 'Спад' }
]}

Каждая аннотация обрабатывается независимо, что позволяет комбинировать разные типы отображения.

Стилизация через динамические параметры

Аннотации могут адаптироваться к данным графика:

const DynamicAnnotation = ({ datum, x, y }) => {
    const color = datum.value > 100 ? 'red' : 'green';

    return (
        <g transform={`translate(${x}, ${y})`}>
            <circle r={5} fill={color} />
            <text x={8} y={4} fontSize={10} fill={color}>
                {datum.value}
            </text>
        </g>
    );
};

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

Связь renderAnnotation и кастомных слоев

В архитектуре Nivo аннотации являются частью слоя поверх основной отрисовки. renderAnnotation интегрируется в pipeline рендера следующим образом:

  1. строится основная сцена графика;
  2. вычисляются позиции аннотаций;
  3. вызывается renderAnnotation для каждой сущности;
  4. результат накладывается поверх графика.

Это означает, что аннотации всегда находятся в верхнем слое SVG и не перекрываются данными графика.

Оптимизация производительности

При большом количестве аннотаций важно учитывать нагрузку:

  • избегается создание тяжелых React-деревьев;
  • минимизируется использование state внутри аннотаций;
  • предпочтение отдается чистым функциональным компонентам;
  • используется мемоизация через React.memo.
const MemoAnnotation = React.memo(({ x, y, note }) => {
    return (
        <g transform={`translate(${x}, ${y})`}>
            <text>{note}</text>
        </g>
    );
});

Особенности взаимодействия с масштабами

При использовании логарифмических или временных шкал аннотации автоматически получают преобразованные координаты. Это означает, что:

  • x может представлять timestamp в преобразованном виде;
  • y зависит от домена шкалы;
  • ручное вычисление координат не требуется.

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

Композиция аннотаций и линий связи

Часто аннотации соединяются с точками графика линиями:

const ConnectedAnnotation = ({ x, y, x1, y1 }) => {
    return (
        <g>
            <line x1={x} y1={y} x2={x1} y2={y1} stroke="#999" />
            <circle cx={x1} cy={y1} r={4} />
        </g>
    );
};

Это используется для создания callout-маркеров, поясняющих конкретные точки данных.

Использование renderAnnotation в сложных визуализациях

В комплексных дашбордах аннотации становятся частью аналитического слоя. Они могут:

  • реагировать на внешние фильтры;
  • отображать агрегированные показатели;
  • синхронизироваться между несколькими графиками;
  • изменять форму в зависимости от контекста данных.

Такая гибкость делает renderAnnotation ключевым инструментом кастомизации визуализаций в Nivo, позволяя выходить за пределы стандартной декларативной модели и внедрять прикладную логику прямо в слой графического отображения.