В экосистеме 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 проходит трансформацию в набор внутренних
структур:
indexBykeysscaleLinear,
scaleBand)Схематично:
BarDatum
↓ indexBy
CategoryNode
↓ keys mapping
SeriesValues[]
↓ scale transformation
BarSegments
Хотя базовый контракт ограничивается string | number, на
практике используется более узкое определение:
string — только для indexBynumber — для всех keysЭто важно, поскольку:
Нарушение этого правила приводит к некорректному отображению графика
или NaN в вычислениях.
BarDatum с опциональными полямиДопускается расширение данных дополнительными полями:
type Data = {
country: string;
apples: number;
bananas: number;
color?: string;
region?: string;
}
Дополнительные поля:
layers)Таким образом BarDatum выступает как открытая структура
с фиксированными обязательными и произвольными дополнительными
полями.
В реальных сценариях данные могут нарушать ожидаемый контракт:
{
country: "KZ",
apples: "10"
}
Такое значение формально соответствует string | number,
но ломает вычислительную модель.
Для защиты используется:
BarDatum с stacked и grouped режимамиТип данных не меняется, но меняется интерпретация:
Каждый BarDatum → отдельная категория Каждое поле из
keys → отдельный бар внутри группы
Каждый BarDatum → основание стека Каждое поле из
keys → сегмент одного столбца
Тип BarDatum остаётся идентичным, но:
Часто исходные данные не соответствуют требованиям
BarDatum и требуют трансформации:
const raw = [
["KZ", 10, 20],
["RU", 15, 25]
];
const data: BarDatum[] = raw.map(([country, apples, bananas]) => ({
country,
apples,
bananas
}));
На этом этапе формируется строгий контракт, соответствующий ожиданиям
@nivo/bar.
Основной компонент:
<ResponsiveBar<BarDatum>
data={data}
keys={["apples", "bananas"]}
indexBy="country"
/>
Здесь BarDatum:
TooltipPropscustom layersКастомные слои получают доступ к данным:
layer: (props: BarCustomLayerProps<BarDatum>) => ReactNode
где BarDatum позволяет:
Типизация обеспечивает корректное извлечение значений:
datum.apples
datum.bananas
Несмотря на гибкость, модель BarDatum имеет ряд
ограничений:
keys ↔︎ keyof BarDatumЭто связано с тем, что библиотека ориентирована на runtime-динамику, а не на строгую доменную модель.
Формально BarDatum можно представить как:
type BarDatum<K extends string = string> = {
[key in K]: number;
} & {
[key: string]: string | number;
}
Но фактическая интерпретация ближе к:
indexBy)keys)Эта структура обеспечивает баланс между гибкостью визуализации и частичной типовой безопасностью в TypeScript-окружении.