Аннотации в Nivo используются для добавления смысловых слоёв поверх визуализаций: выделения точек, интервалов, событийных отметок, комментариев к значениям. В основе лежит привязка аннотаций к координатам данных, что позволяет сохранять синхронизацию между графиком и его интерпретацией при любых изменениях масштаба, фильтрации или ресайза контейнера.
В библиотеке Nivo аннотации описываются декларативно через массив объектов, где каждый объект содержит информацию о типе аннотации, её положении и визуальных параметрах. Ключевой принцип — аннотация не задаёт пиксельные координаты напрямую, а ссылается на данные графика.
Базовая структура аннотации:
const annotations = [
{
type: 'circle',
match: {
serieId: 'temperature',
dataIndex: 10
},
noteX: 20,
noteY: -20,
noteTextOffsetY: -12,
text: 'Пиковое значение'
}
]
Привязка через match обеспечивает устойчивую связь с
данными, а не с отрисованной геометрией. При изменении масштаба или
размера контейнера Nivo пересчитывает позицию автоматически.
Наиболее частый способ привязки — использование индекса точки или её значения внутри серии.
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 аннотации передаются через
свойство 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: 'Выше среднего'
}
Такой подход расширяет график до уровня аналитического инструмента, где визуальные метки отражают вычисленные зависимости, а не только исходные данные.