Визуализация в Nivo строится вокруг строго определённых структур данных, которые описывают точки, серии и дополнительные метаданные графика. В основе лежит идея обобщённого (generic) описания данных, где библиотека не фиксирует конкретный тип значений, а предоставляет механизмы для их строгого определения на уровне приложения.
Типовая проблема при работе с графиками заключается в том, что данные
могут иметь разную семантику: числовые временные ряды, категориальные
оси, вложенные структуры с дополнительными атрибутами. Без дженериков
такие данные быстро приводят к потере типобезопасности и появлению
any в коде.
В Nivo применяется обобщённый подход:
<RawDatum><Datum>Это позволяет сохранять строгую типизацию на всём пути от источника данных до отрисовки графика.
Основной концепт строится вокруг двух уровней:
Простейшая модель для линейного графика:
type Point = {
x: string | number | Date
y: number
}
Использование дженерика в компоненте:
import { ResponsiveLine } from '@nivo/line'
type MyPoint = {
x: number
y: number
}
const data: Array<{
id: string
data: MyPoint[]
}> = [
{
id: 'series-1',
data: [
{ x: 1, y: 10 },
{ x: 2, y: 20 }
]
}
]
<ResponsiveLine<MyPoint>
data={data}
/>
Здесь MyPoint выступает параметром дженерика, фиксируя
структуру точек.
В большинстве компонентов Nivo используется модель серии:
type Serie<Datum> = {
id: string | number
data: Datum[]
}
Такой подход позволяет параметризовать любые графики независимо от структуры точки.
Пример расширенной модели:
type SalesDatum = {
month: string
value: number
region: string
}
type SalesSerie = {
id: string
data: SalesDatum[]
color?: string
}
Передача в компонент:
<ResponsiveLine<SalesDatum>
data={salesSeries}
/>
Важно, что дженерик применяется не к серии, а к типу точки, поскольку серия является контейнером.
TypeScript способен выводить типы автоматически, если структура данных не нарушает ожидаемый формат.
const data = [
{
id: 'A',
data: [
{ x: 1, y: 2 },
{ x: 2, y: 3 }
]
}
]
В этом случае тип может быть выведен как:
{
id: string
data: { x: number; y: number }[]
}[]
Однако при добавлении дополнительных полей:
{ x: 1, y: 2, label: 'first point' }
вывод типов часто деградирует до расширенных union-структур, что снижает строгость проверки. Поэтому в Nivo-проектах обычно фиксируется явный тип дженерика.
Практическая особенность Nivo заключается в том, что точка графика может содержать произвольные поля, используемые в тултипах, кастомных слоях и логике форматирования.
type MetricDatum = {
x: number
y: number
timestamp: number
status: 'ok' | 'warning'
}
Использование в визуализации:
<ResponsiveLine<MetricDatum>
data={data}
tooltip={({ point }) => {
return (
<div>
<div>{point.data.x}</div>
<div>{point.data.y}</div>
<div>{point.data.status}</div>
</div>
)
}}
/>
Тип point.data автоматически наследует
MetricDatum, что исключает необходимость кастов.
Типизация не ограничивается точками. Оси также зависят от структуры данных.
type AxisDatum = {
x: Date
y: number
}
Для временных шкал часто используется:
type TimeSerie = {
x: Date
y: number
}
При этом масштабирование требует согласованности типов:
xScale ожидает тип оси XyScale работает с числовыми значениямиНесоответствие типов приводит к логическим ошибкам даже при
компиляции без ошибок, поэтому строгая фиксация Datum
критична.
Линейные графики используют точечную модель, тогда как другие типы визуализаций требуют иной структуры.
Bar chart:
type BarDatum = {
category: string
valueA: number
valueB: number
}
Pie chart:
type PieDatum = {
id: string
label: string
value: number
}
Использование:
<ResponsivePie<PieDatum>
data={pieData}
/>
Здесь дженерик фиксирует тип объекта сектора.
В реальных системах данные часто содержат вложенные поля:
type ComplexDatum = {
x: number
y: number
meta: {
source: string
confidence: number
}
}
Nivo позволяет безопасно использовать такие структуры в кастомных компонентах:
<ResponsiveLine<ComplexDatum>
data={data}
layers={[
'grid',
'axes',
'lines',
'points',
({ points }) => {
return points.map(p => {
const meta = p.data.meta
return (
<g key={p.id}>
<text>
{meta.source}
</text>
</g>
)
})
}
]}
/>
Тип meta сохраняется на всём пути обработки данных.
Дженерики в Nivo не являются полностью свободными: они ограничены внутренними интерфейсами библиотеки.
Базовый контракт можно представить так:
interface BaseDatum {
x: PropertyKey
y: number
}
При попытке передать несовместимый тип:
type InvalidDatum = {
x: { a: number }
y: string
}
возникает нарушение ожиданий визуализации:
Типизация помогает выявить такие ошибки до выполнения.
Типизация активно используется в форматтерах:
type TooltipDatum = {
x: number
y: number
label: string
}
const tooltip = (point: { data: TooltipDatum }) => {
return `${point.data.label}: ${point.data.y}`
}
Типизация гарантирует, что форматтер всегда получает согласованную структуру.
В TypeScript возможно расширять типы Nivo через декларации:
declare module '@nivo/line' {
interface Point {
extra?: string
}
}
Это позволяет добавлять поля без изменения сигнатур компонентов.
Распространённые проблемы:
any[]data и дженерика
<Datum>id, приводящее к неоднозначности
сериализацииТиповой анти-паттерн:
const data: any = [...]
Он полностью отключает механизм проверки и делает дженерики бессмысленными.
Типизация в Nivo распространяется на несколько уровней:
Пример события:
type ClickDatum = {
id: string
x: number
y: number
}
<ResponsiveLine<ClickDatum>
onCl ick={(point) => {
const d = point.data
console.log(d.x, d.y)
}}
/>
Согласованность типов между слоями обеспечивает предсказуемость поведения всей визуализации.