Агрегация и группировка данных

Работа с визуализациями в 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

Наиболее распространённый инструмент для агрегации — 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.

Преобразование под BarChart

Компонент 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 для grouped bar

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"
/>

Агрегация для PieChart

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-визуализаций заключается в том, что агрегация почти всегда обязательна: исходные события не имеют смысла без суммирования.

Иерархическая агрегация для TreeMap

Для древовидных структур требуется вложенный формат:

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 как альтернатива d3

Иногда использование 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

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 без дублирования кода.

Производительность при агрегации больших массивов

При работе с большими наборами данных важно учитывать стоимость операций группировки. Основные подходы:

  • минимизация проходов по массиву;
  • предварительная фильтрация данных;
  • кэширование результатов агрегации;
  • использование мемоизации при React-рендеринге.

Типичный пример с useMemo:

const aggregated = useMemo(() => {
  return sumByKey(rawData, 'category');
}, [rawData]);

Это предотвращает повторные вычисления при каждом рендере компонента Nivo.

Согласование структуры агрегации с типами визуализаций

Разные компоненты требуют разных схем данных:

  • Bar → плоская структура с ключами серий
  • Pie → массив { id, value }
  • Line → { id, data: [{x, y}] }
  • TreeMap → иерархическое дерево
  • Heatmap → матрица x/y

Агрегация становится этапом адаптации одной и той же исходной модели к множеству представлений, где ключевым фактором выступает форма данных, а не их источник