Работа с визуализациями в Nivo почти всегда начинается не с построения графика, а с подготовки данных. В реальных приложениях сырые наборы редко подходят напрямую для компонентов: требуется группировка, агрегация, суммирование, вычисление средних значений и преобразование структуры под формат конкретного графика.
Nivo не навязывает собственный слой агрегации данных. Это принципиальное решение: библиотека ориентирована на чистую визуализацию, где входные данные уже приведены к ожидаемой форме. Поэтому основная работа по группировке выполняется на уровне бизнес-логики или отдельного слоя трансформации.
Типичная проблема — наличие плоского массива событий:
const rawData = [
{ category: 'A', value: 10 },
{ category: 'B', value: 15 },
{ category: 'A', value: 7 },
{ category: 'C', value: 3 },
{ category: 'B', value: 9 }
];
Для большинства компонентов Nivo такой формат непригоден напрямую, если требуется агрегированное представление.
Наиболее распространённый инструмент для агрегации —
d3-array. Он предоставляет функции group,
rollup, sum, mean и другие.
Группировка по категории:
import { rollup } from 'd3-array';
const grouped = Array.from(
rollup(
rawData,
v => v.reduce((acc, item) => acc + item.value, 0),
d => d.category
),
([key, value]) => ({ category: key, total: value })
);
Результат:
[
{ category: 'A', total: 17 },
{ category: 'B', total: 24 },
{ category: 'C', total: 3 }
]
Такой формат уже может использоваться в Bar или Pie визуализациях Nivo.
Компонент ResponsiveBar ожидает массив объектов, где
каждая запись соответствует категории:
import { ResponsiveBar } from '@nivo/bar';
Прямое отображение агрегированных данных:
const data = grouped;
Конфигурация ключей:
<ResponsiveBar
data={data}
keys={['total']}
indexBy="category"
margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
/>
Ключевой момент заключается в том, что агрегация отделена от визуализации. Nivo не изменяет значения, а лишь интерпретирует их структуру.
При наличии дополнительного измерения (например, дата + категория) используется вложенная группировка.
Исходные данные:
const rawData = [
{ date: '2024-01', category: 'A', value: 10 },
{ date: '2024-01', category: 'B', value: 5 },
{ date: '2024-02', category: 'A', value: 8 },
{ date: '2024-02', category: 'B', value: 12 }
];
Группировка по двум уровням:
const grouped = Array.from(
rollup(
rawData,
v => v.reduce((sum, d) => sum + d.value, 0),
d => d.date,
d => d.category
)
);
Результат представляет собой вложенную структуру:
[
[
'2024-01',
Map(2) { 'A' => 10, 'B' => 5 }
],
[
'2024-02',
Map(2) { 'A' => 8, 'B' => 12 }
]
]
Nivo требует «плоский» формат с ключами серий:
const formatted = grouped.map(([date, map]) => {
const obj = { date };
for (const [key, value] of map) {
obj[key] = value;
}
return obj;
});
Результат:
[
{ date: '2024-01', A: 10, B: 5 },
{ date: '2024-02', A: 8, B: 12 }
]
Использование в ResponsiveBar:
<ResponsiveBar
data={formatted}
keys={['A', 'B']}
indexBy="date"
/>
Pie-диаграмма в Nivo требует формат:
[{ id, value }]
Преобразование:
const pieData = Array.from(
rollup(
rawData,
v => v.reduce((sum, d) => sum + d.value, 0),
d => d.category
),
([id, value]) => ({ id, value })
);
Использование:
<ResponsivePie data={pieData} />
Особенность Pie-визуализаций заключается в том, что агрегация почти всегда обязательна: исходные события не имеют смысла без суммирования.
Для древовидных структур требуется вложенный формат:
const treeData = {
name: 'root',
children: Array.from(
rollup(
rawData,
v => v.reduce((sum, d) => sum + d.value, 0),
d => d.category
),
([name, value]) => ({
name,
value
})
)
};
Компонент:
<ResponsiveTreeMap data={treeData} />
Здесь агрегация формирует веса узлов, которые определяют размеры блоков.
Для line charts часто требуется агрегация по времени с нормализацией:
const groupedByDate = Array.from(
rollup(
rawData,
v => v.reduce((sum, d) => sum + d.value, 0),
d => d.date
),
([date, value]) => ({ x: date, y: value })
);
Использование:
const lineData = [
{
id: 'series 1',
data: groupedByDate
}
];
<ResponsiveLine data={lineData} />
В сложных сценариях агрегация дополняется нормализацией:
const normalized = rawData.map(d => ({
...d,
value: Number(d.value) || 0
}));
Это предотвращает искажения при суммировании и вычислениях.
Иногда использование reduce оказывается проще и
быстрее:
const grouped = Object.values(
rawData.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = { category: item.category, total: 0 };
}
acc[item.category].total += item.value;
return acc;
}, {})
);
Этот подход часто применяется при отсутствии необходимости в сложной иерархии.
Heatmap требует матричной структуры:
const heatmapData = [
{
id: 'A',
data: [
{ x: 'Jan', y: 10 },
{ x: 'Feb', y: 5 }
]
},
{
id: 'B',
data: [
{ x: 'Jan', y: 7 },
{ x: 'Feb', y: 12 }
]
}
];
Формирование через агрегацию:
const grouped = rollup(
rawData,
v => v.reduce((sum, d) => sum + d.value, 0),
d => d.category,
d => d.date
);
Далее происходит преобразование в формат с id и
вложенными точками.
В больших приложениях агрегация выносится в отдельные функции:
const sumByKey = (data, key) =>
Array.from(
rollup(
data,
v => v.reduce((s, d) => s + d.value, 0),
d => d[key]
),
([id, value]) => ({ id, value })
);
Это позволяет переиспользовать логику для Pie, Bar и TreeMap без дублирования кода.
При работе с большими наборами данных важно учитывать стоимость операций группировки. Основные подходы:
Типичный пример с useMemo:
const aggregated = useMemo(() => {
return sumByKey(rawData, 'category');
}, [rawData]);
Это предотвращает повторные вычисления при каждом рендере компонента Nivo.
Разные компоненты требуют разных схем данных:
{ id, value }{ id, data: [{x, y}] }x/yАгрегация становится этапом адаптации одной и той же исходной модели к множеству представлений, где ключевым фактором выступает форма данных, а не их источник