Типизация обработчиков событий

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

Типизация событий строится вокруг React-событий и обобщённых структур Nivo, где ключевую роль играют Datum, Serie, BarDatum, Point, ArcDatum. В TypeScript-окружении это позволяет не только повысить предсказуемость логики, но и исключить ошибки при доступе к полям данных.


Базовая структура обработчиков

Большинство компонентов Nivo используют унифицированный стиль описания событий:

onClick: (datum: Datum, event: React.MouseEvent) => void
onMouseEnter: (datum: Datum, event: React.MouseEvent) => void
onMouseLeave: (datum: Datum, event: React.MouseEvent) => void

Datum в данном контексте представляет собой расширенный объект данных, обогащённый служебными полями (индексы, цвета, идентификаторы серии). Тип конкретного Datum зависит от графика.

Например, для столбчатой диаграммы:

import { BarDatum } from '@nivo/bar'

Для линейного графика:

import { Point } from '@nivo/line'

Для круговых диаграмм:

import { ArcDatum } from '@nivo/pie'

Типизация событий в столбчатых диаграммах

Компонент ResponsiveBar предоставляет расширенный набор событий, связанных с отдельными столбцами.

import { ResponsiveBar, BarDatum } from '@nivo/bar'

type BarClickHandler = (bar: BarDatum, event: React.MouseEvent) => void

Объект BarDatum включает как исходные значения, так и вычисленные свойства:

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

Пример строгой типизации:

const handleBarClick: BarClickHandler = (bar, event) => {
  console.log(bar.id)
  console.log(bar.value)
  console.log(bar.data)
}

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


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

Линейные графики (ResponsiveLine) используют структуру точек:

import { Point } from '@nivo/line'

Сигнатуры событий:

type PointClickHandler = (point: Point, event: React.MouseEvent) => void

Тип Point содержит:

  • x и y — координаты данных
  • serieId — идентификатор серии
  • data — исходный объект данных
  • index — индекс точки в серии
  • color — цвет точки

Пример обработки:

const handlePointClick = (point: Point, event: React.MouseEvent) => {
  const series = point.serieId
  const value = point.data.y
}

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

onMouseEnter: (point: Point, event: React.MouseEvent) => void
onMouseMove: (point: Point, event: React.MouseEvent) => void

События круговых диаграмм

В ResponsivePie обработчики работают с дугами:

import { ArcDatum } from '@nivo/pie'

Типовая сигнатура:

type ArcClickHandler = (arc: ArcDatum, event: React.MouseEvent) => void

Структура ArcDatum включает:

  • id — идентификатор секции
  • value — значение
  • formattedValue — форматированное отображение
  • color — вычисленный цвет сектора
  • label — текстовая метка

Пример:

const handleArcClick = (arc: ArcDatum) => {
  const percentage = arc.value
  const label = arc.label
}

Важной особенностью является наличие предрассчитанных углов:

  • arc.arc.startAngle
  • arc.arc.endAngle

Эти данные позволяют реализовывать сложные визуальные эффекты, зависящие от геометрии сектора.


Обобщённые типы событий и дженерики

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

type MyData = {
  category: string
  amount: number
}

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

<ResponsiveBar<MyData>
  data={data}
  keys={['amount']}
  indexBy="category"
  onCl ick={(bar, event) => {
    bar.data.category
    bar.data.amount
  }}
/>

Здесь bar.data становится строго типизированным объектом MyData.


Типизация событий наведения и взаимодействия

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

onMouseEnter: (datum, event) => void
onMouseMove: (datum, event) => void
onMouseLeave: (datum, event) => void

Тип event остаётся стандартным React:

React.MouseEvent<SVGElement, MouseEvent>

Особенность заключается в том, что Nivo передаёт уже вычисленный datum, а не сырые координаты DOM-элемента.


Контекстные события и координаты

Некоторые компоненты предоставляют доступ к координатам курсора:

onMouseMove: (datum, event) => {
  const x = event.clientX
  const y = event.clientY
}

Однако важно учитывать, что координаты относятся к viewport, а не к SVG-системе координат. Для преобразования используются внутренние утилиты Nivo, которые не всегда экспонируются напрямую.


Типизация легенд и интерактивных элементов

Легенды также поддерживают события:

onClick: (item, event) => void

Тип элемента легенды:

type LegendItem = {
  id: string
  label: string
  color: string
  data: unknown
}

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


Перегрузка типов и расширение событий

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

type ExtendedBarDatum<T> = BarDatum & {
  data: T
}

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

const onCl ick = (bar: ExtendedBarDatum<MyData>, event: React.MouseEvent) => {
  bar.data.category
}

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


Проблема несоответствия типов между компонентами

В различных графиках Nivo одинаковые по смыслу события могут иметь разные типы datum. Например:

  • BarDatum содержит indexValue
  • Point содержит serieId
  • ArcDatum содержит arc

Это приводит к необходимости явного разделения обработчиков:

type ChartEvent =
  | { type: 'bar'; dat a: BarDatum }
  | { type: 'line'; dat a: Point }
  | { type: 'pie'; dat a: ArcDatum }

Дальнейшая обработка строится через дискриминируемые объединения:

const handleEvent = (e: ChartEvent) => {
  if (e.type === 'bar') {
    e.data.value
  }
}

Интеграция с React SyntheticEvent

Все события Nivo основаны на React.SyntheticEvent, что накладывает ограничения:

  • события пулаются React
  • доступ к event.target может быть нестабильным
  • рекомендуется извлекать данные синхронно

Типизация:

React.MouseEvent<SVGElement, MouseEvent>

При необходимости сохранения события используется:

event.persist()

Типизация кастомных data attributes

В некоторых случаях графики обогащаются пользовательскими полями:

type ExtendedDatum = BarDatum & {
  metadata: {
    region: string
    category: string
  }
}

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

onCl ick={(bar: ExtendedDatum) => {
  bar.metadata.region
}}

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


Строгая типизация и предотвращение runtime ошибок

Основное преимущество типизации обработчиков в Nivo заключается в снижении зависимости от runtime-структур. Ошибки доступа к несуществующим полям заменяются компиляционными ошибками.

Типичные классы ошибок, устраняемых TypeScript:

  • обращение к несуществующему datum.value в неподходящем графике
  • путаница между id и indexValue
  • неправильная интерпретация координат
  • потеря связи между legend item и chart datum

Унификация обработчиков в архитектуре приложения

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

type ChartHandlers<T> = {
  onClick?: (datum: T, event: React.MouseEvent) => void
  onHover?: (datum: T, event: React.MouseEvent) => void
}

Далее конкретные графики подставляют соответствующие типы:

const barHandlers: ChartHandlers<BarDatum> = { ... }
const lineHandlers: ChartHandlers<Point> = { ... }

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


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

В сложных интерфейсах события часто интегрируются с состоянием:

const handleClick = (bar: BarDatum) => {
  setSelectedId(bar.id)
}

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