Компонент BarDatum и типизация данных

В экосистеме Nivo компонент @nivo/bar опирается на строгую модель данных, где центральным контрактом выступает тип BarDatum. Именно он определяет форму входного объекта, используемого для построения столбчатых диаграмм, а также задаёт правила взаимодействия между данными, шкалами и визуальными элементами.

В TypeScript-реализации BarDatum является обобщённым (generic) объектом, допускающим расширение, но сохраняющим обязательную структуру ключей, используемых библиотекой для вычисления значений по осям и построения серий.


Базовая структура BarDatum

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

Минимальная форма:

type BarDatum = {
    [key: string]: string | number;
}

Такое определение подчёркивает фундаментальную идею: объект должен содержать произвольные ключи, но значения обязаны быть либо числовыми, либо строковыми (строка используется как идентификатор категории).

Однако на практике структура строго управляется через параметры keys и indexBy.


Роль indexBy в формировании BarDatum

Ключ indexBy определяет поле, которое используется как идентификатор категории.

Пример:

type DataItem = {
    country: string;
    apples: number;
    bananas: number;
}

Конфигурация:

indexBy="country"
keys={["apples", "bananas"]}

Здесь BarDatum должен удовлетворять следующему контракту:

  • поле country — уникальный индекс категории
  • поля apples и bananas — числовые значения, формирующие серии

Фактически BarDatum расширяется до:

type BarDatum = {
    country: string;
    apples: number;
    bananas: number;
}

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


keys и типизация серий

Параметр keys определяет набор числовых полей, которые интерпретируются как серии данных.

keys: string[]

Каждый элемент массива keys должен соответствовать ключу в BarDatum, значение которого является числом.

С точки зрения TypeScript, это создаёт частичную связь между строковыми литералами и полями объекта, однако строгая проверка достигается только при явной типизации пользователя:

type MyDatum = {
    country: string;
    apples: number;
    bananas: number;
};

keys: (keyof Omit<MyDatum, "country">)[]

Обобщённый тип BarDatum в Nivo

Внутри реализации используется обобщение:

interface BarDatum {
    [key: string]: string | number;
}

Но при использовании в компонентах применяется generic:

<ResponsiveBar<DatumType> />

где DatumType расширяет базовый контракт.

Это позволяет:

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

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

Несмотря на наличие generics, связь между keys и полями BarDatum не всегда строго проверяется на уровне компиляции. Это связано с динамической природой JavaScript-объектов.

Пример потенциально опасной конфигурации:

type Data = {
    country: string;
    apples: number;
};

keys={["bananas"]}

TypeScript не всегда может определить ошибку, поскольку keys имеет тип string[], а не keyof Data.


Усиление типизации через литералы

Для повышения строгости применяется подход с as const:

const keys = ["apples", "bananas"] as const;

type Keys = typeof keys[number];

Теперь можно связать BarDatum с конкретными ключами:

type Data = {
    country: string;
} & Record<Keys, number>;

Такой подход формирует более жёсткий контракт:

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

Внутреннее использование BarDatum в вычислениях

На уровне реализации @nivo/bar каждый BarDatum проходит трансформацию в набор внутренних структур:

  1. Индексация по indexBy
  2. Разделение на серии по keys
  3. Преобразование значений в шкалы (scaleLinear, scaleBand)
  4. Формирование stacked или grouped структуры

Схематично:

BarDatum
   ↓ indexBy
CategoryNode
   ↓ keys mapping
SeriesValues[]
   ↓ scale transformation
BarSegments

Типизация вложенных значений

Хотя базовый контракт ограничивается string | number, на практике используется более узкое определение:

  • string — только для indexBy
  • number — для всех keys

Это важно, поскольку:

  • шкалы D3 ожидают числовые значения
  • строки не участвуют в арифметических операциях

Нарушение этого правила приводит к некорректному отображению графика или NaN в вычислениях.


Использование BarDatum с опциональными полями

Допускается расширение данных дополнительными полями:

type Data = {
    country: string;
    apples: number;
    bananas: number;
    color?: string;
    region?: string;
}

Дополнительные поля:

  • игнорируются графиком
  • могут использоваться в кастомных тултипах
  • применяются в кастомных слоях (layers)

Таким образом BarDatum выступает как открытая структура с фиксированными обязательными и произвольными дополнительными полями.


Проблема неоднородных данных

В реальных сценариях данные могут нарушать ожидаемый контракт:

{
    country: "KZ",
    apples: "10"
}

Такое значение формально соответствует string | number, но ломает вычислительную модель.

Для защиты используется:

  • нормализация входных данных
  • runtime validation (например, Zod или Yup)
  • явное приведение типов перед передачей в компонент

Связь BarDatum с stacked и grouped режимами

Тип данных не меняется, но меняется интерпретация:

grouped:

Каждый BarDatum → отдельная категория Каждое поле из keys → отдельный бар внутри группы

stacked:

Каждый BarDatum → основание стека Каждое поле из keys → сегмент одного столбца

Тип BarDatum остаётся идентичным, но:

  • изменяется алгоритм агрегации
  • изменяется порядок суммирования значений

Преобразование данных перед передачей в Bar

Часто исходные данные не соответствуют требованиям BarDatum и требуют трансформации:

const raw = [
    ["KZ", 10, 20],
    ["RU", 15, 25]
];

const data: BarDatum[] = raw.map(([country, apples, bananas]) => ({
    country,
    apples,
    bananas
}));

На этом этапе формируется строгий контракт, соответствующий ожиданиям @nivo/bar.


Типизация через дженерики в ResponsiveBar

Основной компонент:

<ResponsiveBar<BarDatum>
    data={data}
    keys={["apples", "bananas"]}
    indexBy="country"
/>

Здесь BarDatum:

  • обеспечивает согласованность структуры
  • задаёт основу для TooltipProps
  • влияет на типы custom layers

BarDatum и кастомные слои

Кастомные слои получают доступ к данным:

layer: (props: BarCustomLayerProps<BarDatum>) => ReactNode

где BarDatum позволяет:

  • обращаться к исходным значениям
  • вычислять дополнительные визуальные элементы
  • строить аннотации

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

datum.apples
datum.bananas

Ограничения типовой модели

Несмотря на гибкость, модель BarDatum имеет ряд ограничений:

  • отсутствует строгая связь keys ↔︎ keyof BarDatum
  • нет встроенной валидации значений
  • допускается лишняя структура данных
  • возможны runtime ошибки при несоответствии типов

Это связано с тем, что библиотека ориентирована на runtime-динамику, а не на строгую доменную модель.


Итоговая модель данных

Формально BarDatum можно представить как:

type BarDatum<K extends string = string> = {
    [key in K]: number;
} & {
    [key: string]: string | number;
}

Но фактическая интерпретация ближе к:

  • один ключ индекса (indexBy)
  • набор числовых ключей (keys)
  • произвольные дополнительные поля

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