Привязка аннотаций к данным

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

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

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

const annotations = [
  {
    type: 'circle',
    match: {
      serieId: 'temperature',
      dataIndex: 10
    },
    noteX: 20,
    noteY: -20,
    noteTextOffsetY: -12,
    text: 'Пиковое значение'
  }
]

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

Привязка через datum и индекс данных

Наиболее частый способ привязки — использование индекса точки или её значения внутри серии.

match: {
  serieId: 'sales',
  dataIndex: 5
}

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

match: {
  serieId: 'sales',
  value: 120
}

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

Использование селекторов для сложных условий

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

match: (datum) => datum.y > 100

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

Пример выделения всех значений выше порога:

const annotations = [
  {
    type: 'rect',
    match: (d) => d.y > 200,
    noteTextOffsetY: -10,
    text: 'Зона аномальных значений'
  }
]

Геометрические типы аннотаций

В Nivo доступны несколько базовых типов аннотаций, каждый из которых по-разному взаимодействует с данными.

Точечные аннотации

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

{
  type: 'circle',
  match: { serieId: 'profit', dataIndex: 3 },
  radius: 10,
  borderWidth: 2,
  borderColor: '#ff0000'
}

Такая аннотация пересчитывает позицию при изменении масштаба осей, оставаясь «приклеенной» к точке данных.

Линейные аннотации

Линии часто используются для отображения порогов или трендов.

{
  type: 'line',
  match: (d) => d.y === 150,
  borderColor: '#00aaff',
  borderWidth: 2,
  strokeDasharray: '6 4'
}

Линия может быть привязана как к конкретному значению оси, так и к вычисляемому набору точек.

Прямоугольные аннотации

Используются для выделения диапазонов значений.

{
  type: 'rect',
  match: (d) => d.x >= 10 && d.x <= 20,
  backgroundColor: 'rgba(255, 200, 0, 0.15)'
}

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

Привязка аннотаций в ResponsiveLine

Наиболее распространённый сценарий — работа с линейными графиками. В компоненте ResponsiveLine аннотации передаются через свойство annotations.

import { ResponsiveLine } from '@nivo/line'

const data = [
  {
    id: 'revenue',
    data: [
      { x: 1, y: 30 },
      { x: 2, y: 80 },
      { x: 3, y: 120 }
    ]
  }
]

const annotations = [
  {
    type: 'circle',
    match: {
      serieId: 'revenue',
      dataIndex: 2
    },
    text: 'Резкий рост'
  }
]

export default function Chart() {
  return (
    <ResponsiveLine
      data={data}
      margin={{ top: 50, right: 50, bottom: 50, left: 50 }}
      annotations={annotations}
    />
  )
}

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

Привязка через координаты осей

В ряде случаев используется привязка не к индексу, а к координатам осей. Это особенно актуально при работе с временными рядами.

{
  type: 'line',
  match: { x: 2024 },
  borderColor: '#ff6600'
}

Такой подход позволяет закреплять аннотации за конкретными значениями оси X независимо от плотности данных.

Динамическое формирование аннотаций из данных

Аннотации часто генерируются автоматически на основе анализа массива данных. Например, можно выделять максимум серии:

const maxPoint = data[0].data.reduce((acc, curr, index) => {
  if (curr.y > acc.value) {
    return { index, value: curr.y }
  }
  return acc
}, { index: 0, value: -Infinity })

const annotations = [
  {
    type: 'circle',
    match: {
      serieId: 'revenue',
      dataIndex: maxPoint.index
    },
    text: 'Максимум'
  }
]

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

Пользовательские аннотации

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

const CustomAnnotation = ({ x, y, datum }) => {
  return (
    <g transform={`translate(${x}, ${y})`}>
      <rect width={80} height={30} fill="#222" />
      <text fill="#fff" x={10} y={20}>
        {datum.y}
      </text>
    </g>
  )
}

Использование:

{
  type: 'circle',
  match: { serieId: 'sales', dataIndex: 4 },
  render: CustomAnnotation
}

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

Связь аннотаций с масштабированием

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

  • размеров контейнера
  • диапазонов осей
  • фильтрации набора данных

позиционирование пересчитывается без дополнительной логики.

Это особенно важно при использовании ResponsiveLine, ResponsiveBar и других responsive-компонентов.

Аннотации в столбчатых диаграммах

В ResponsiveBar аннотации работают аналогично, но привязка осуществляется к категориям.

{
  type: 'rect',
  match: {
    key: 'product A'
  },
  backgroundColor: 'rgba(0,0,255,0.1)'
}

В этом случае ключ категории заменяет индекс данных, что упрощает работу с категориальными наборами.

Сложные условия комбинированной привязки

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

{
  type: 'circle',
  match: (d) => d.x > 10 && d.y < 50 && d.serieId === 'profit'
}

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

Производительность при большом количестве аннотаций

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

Практика оптимизации включает:

  • мемоизацию массива аннотаций
  • предварительный расчёт совпадений
  • ограничение числа активных аннотаций
  • избегание тяжёлых вычислений внутри match

Пример мемоизации:

const annotations = useMemo(() => {
  return data[0].data
    .filter(d => d.y > 100)
    .map((d, index) => ({
      type: 'circle',
      match: { serieId: 'revenue', dataIndex: index }
    }))
}, [data])

Сопоставление аннотаций с несколькими сериями

В многосерийных графиках аннотации могут применяться сразу к нескольким линиям.

{
  type: 'line',
  match: (d) => ['revenue', 'profit'].includes(d.serieId) && d.x === 5
}

Такой механизм позволяет синхронно отмечать события, влияющие на разные метрики.

Стилизация аннотаций через тему

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

theme={{
  annotations: {
    text: {
      fontSize: 12,
      fill: '#333'
    },
    outlineWidth: 1
  }
}}

Это обеспечивает согласованность с остальными элементами визуализации без ручной настройки каждого объекта.

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

Аннотации часто используются не только для сырых данных, но и для производных показателей: скользящих средних, отклонений, трендов.

const movingAverage = data.map((d, i, arr) => {
  const slice = arr.slice(Math.max(0, i - 2), i + 1)
  const avg = slice.reduce((s, v) => s + v.y, 0) / slice.length
  return { ...d, avg }
})

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

{
  type: 'circle',
  match: (d) => d.y > d.avg,
  text: 'Выше среднего'
}

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