Кастомный компонент подсказки

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

Кастомный компонент подсказки в Nivo представляет собой React-компонент, который получает данные точки графика и возвращает произвольную разметку. Механизм основан на передаче функции или компонента в свойство tooltip соответствующего графика.


Базовая модель данных tooltip

Во всех основных типах графиков Nivo (line, bar, area, scatter) tooltip получает структуру данных, описывающую активную точку или группу точек. Типичный объект содержит:

  • id — идентификатор серии
  • value — числовое значение
  • color — цвет серии
  • indexValue — значение оси X
  • data — исходный объект точки

Пример структуры:

{
    id: 'series A',
    value: 124,
    color: '#ff4d4f',
    indexValue: '2024-01',
    data: {
        x: '2024-01',
        y: 124
    }
}

Подключение кастомного tooltip в линейном графике

В компоненте Line из @nivo/line tooltip задаётся через проп tooltip.

import { ResponsiveLine } from '@nivo/line'

const CustomTooltip = ({ point }) => {
    return (
        <div style={{
            background: '#1f1f1f',
            padding: '10px 12px',
            borderRadius: 6,
            color: '#fff',
            fontSize: 12
        }}>
            <div><strong>{point.seriesId}</strong></div>
            <div>Значение: {point.data.y}</div>
            <div>Категория: {point.data.x}</div>
        </div>
    )
}

const Chart = ({ data }) => (
    <ResponsiveLine
        data={data}
        margin={{ top: 40, right: 20, bottom: 40, left: 40 }}
        xScale={{ type: 'point' }}
        yScale={{ type: 'linear', min: 'auto', max: 'auto' }}
        axisBottom={{ legend: 'время' }}
        axisLeft={{ legend: 'значение' }}
        tooltip={CustomTooltip}
    />
)

Ключевой момент заключается в том, что tooltip получает уже нормализованные данные, поэтому не требуется ручная агрегация.


Форматирование чисел и локализация

В реальных интерфейсах числовые значения редко отображаются «как есть». Часто используется Intl.NumberFormat:

const formatValue = (value) =>
    new Intl.NumberFormat('ru-RU', {
        style: 'decimal',
        maximumFractionDigits: 2
    }).format(value)

Интеграция в tooltip:

const CustomTooltip = ({ point }) => {
    return (
        <div className="tooltip">
            <div>{point.seriesId}</div>
            <div>{formatValue(point.data.y)}</div>
        </div>
    )
}

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


Tooltip с несколькими сериями

В групповых графиках (stacked, grouped bar) tooltip может содержать массив точек. Например, в @nivo/bar компоненте данные часто приходят как набор значений по одному индексу.

const GroupedTooltip = ({ data }) => {
    return (
        <div style={{ background: '#222', padding: 12 }}>
            <div>Категория: {data.indexValue}</div>

            {data.data.map((item) => (
                <div key={item.id} style={{ color: item.color }}>
                    {item.id}: {item.value}
                </div>
            ))}
        </div>
    )
}

Здесь data.data содержит все серии для одного значения оси X, что позволяет строить сравнительные подсказки.


Использование HTML и визуальных маркеров

Кастомный tooltip в Nivo не ограничен текстом. Возможна интеграция цветовых индикаторов, иконок, полос прогресса и других элементов интерфейса.

const EnhancedTooltip = ({ point }) => {
    return (
        <div style={{
            background: '#fff',
            border: `1px solid ${point.color}`,
            padding: 10,
            borderRadius: 8,
            minWidth: 160
        }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                <span style={{
                    width: 10,
                    height: 10,
                    background: point.color,
                    display: 'inline-block'
                }} />
                <strong>{point.seriesId}</strong>
            </div>

            <div style={{ marginTop: 6 }}>
                Значение: {point.data.y}
            </div>
        </div>
    )
}

Цвет серии используется как визуальный якорь, связывающий tooltip с линией или столбцом на графике.


Tooltip в scatter-диаграммах

В @nivo/scatterplot tooltip часто применяется для отображения многомерных данных.

const ScatterTooltip = ({ node }) => {
    return (
        <div style={{ background: '#111', color: '#fff', padding: 10 }}>
            <div>ID: {node.data.id}</div>
            <div>X: {node.x}</div>
            <div>Y: {node.y}</div>
            <div>Группа: {node.serieId}</div>
        </div>
    )
}

Особенность scatter-графиков заключается в том, что tooltip часто становится основным способом интерпретации точек, поскольку визуально они могут быть плотными и перекрывающимися.


Доступ к исходным данным внутри tooltip

В Nivo tooltip-компоненты могут получать не только отображаемые значения, но и дополнительные поля из исходного dataset.

const CustomTooltip = ({ point }) => {
    const meta = point.data.metadata

    return (
        <div>
            <div>{point.data.x}</div>
            <div>{point.data.y}</div>
            {meta && (
                <div>
                    Регион: {meta.region}
                </div>
            )}
        </div>
    )
}

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


Управление поведением tooltip

Tooltip в Nivo можно полностью контролировать:

  • включение/выключение через enableSlices
  • позиционирование
  • задержка отображения
  • кастомизация контейнера

Пример управления поведением:

<ResponsiveLine
    data={data}
    enableSlices="x"
    tooltip={CustomTooltip}
    sliceTooltip={GroupedTooltip}
/>

При использовании enableSlices="x" tooltip агрегирует все точки по оси X, что полезно для временных рядов.


Производительность кастомных tooltip

При работе с большими графиками tooltip становится частью высокочастотных ререндеров. Каждое движение курсора может инициировать обновление состояния.

Оптимизация достигается через:

  • мемоизацию компонентов (React.memo)
  • вынос форматирования за пределы render-функции
  • минимизацию вычислений внутри tooltip
const MemoTooltip = React.memo(({ point }) => {
    return (
        <div>
            {point.seriesId}: {point.data.y}
        </div>
    )
})

Дополнительная оптимизация — предварительное форматирование данных до передачи в график.


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

Вместо inline-стилей часто используется интеграция с CSS или CSS-in-JS:

import styled from 'styled-components'

const TooltipBox = styled.div`
    background: #0f172a;
    color: white;
    padding: 12px;
    border-radius: 8px;
    font-size: 12px;
`

const CustomTooltip = ({ point }) => (
    <TooltipBox>
        <div>{point.seriesId}</div>
        <div>{point.data.y}</div>
    </TooltipBox>
)

Такой подход обеспечивает консистентность интерфейса при масштабировании визуализаций.


Разделение tooltip по типам графиков

Разные графики требуют разных стратегий отображения:

  • линейные графики — акцент на временные ряды
  • столбчатые графики — сравнение категорий
  • scatter — многомерная интерпретация точек
  • area — накопленные значения

Унификация tooltip-компонентов достигается через создание базового слоя:

const BaseTooltip = ({ title, items }) => (
    <div className="tooltip">
        <div>{title}</div>
        {items.map((i) => (
            <div key={i.label}>
                {i.label}: {i.value}
            </div>
        ))}
    </div>
)

Обработка edge cases

В реальных данных tooltip должен учитывать:

  • null и undefined значения
  • отсутствие серии
  • некорректные типы данных
  • пустые наборы точек при slicing
const SafeTooltip = ({ point }) => {
    if (!point || !point.data) return null

    return (
        <div>
            {point.seriesId ?? 'unknown'}
        </div>
    )
}

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