Группировка данных: d3.group() и d3.rollup()

Функции d3.group и d3.rollup предназначены для преобразования плоских наборов данных в структурированные представления, удобные для агрегации, визуализации и построения многомерных иерархий. Оба метода работают с итерируемыми коллекциями и возвращают структуры на основе Map, но различаются уровнем агрегации и формой результата.


Функция d3.group() формирует вложенные структуры данных, где каждый уровень группировки создаётся на основе переданной функции-ключа. Результатом является вложенный Map, в котором ключи соответствуют значениям группирующих функций, а значения — либо вложенные Map, либо массив исходных элементов.

Базовая сигнатура

d3.group(data, ...keys)
  • data — исходный массив объектов
  • keys — функции, каждая из которых определяет уровень группировки

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


Простая группировка

const data = [
  { country: "KZ", city: "Karaganda", value: 10 },
  { country: "KZ", city: "Almaty", value: 20 },
  { country: "RU", city: "Moscow", value: 30 }
];

const grouped = d3.group(data, d => d.country);

Результат:

Map {
  "KZ" => [ {...}, {...} ],
  "RU" => [ {...} ]
}

На верхнем уровне создаётся Map, где ключи — страны, а значения — массивы объектов.


Многоуровневая группировка

const grouped = d3.group(
  data,
  d => d.country,
  d => d.city
);

Структура результата:

Map {
  "KZ" => Map {
    "Karaganda" => [ {...} ],
    "Almaty" => [ {...} ]
  },
  "RU" => Map {
    "Moscow" => [ {...} ]
  }
}

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


Особенности структуры Map

d3.group() возвращает не обычные объекты, а Map, что даёт важные свойства:

  • порядок вставки сохраняется
  • ключи могут быть любого типа (строки, числа, объекты)
  • удобные методы .get(), .set(), .has(), .forEach()

Пример обхода:

for (const [country, cities] of grouped) {
  for (const [city, items] of cities) {
    console.log(country, city, items.length);
  }
}

Преобразование в массив

Для визуализаций часто требуется массив вместо Map:

const arrayForm = Array.from(grouped, ([key, value]) => ({
  key,
  value
}));

Или рекурсивно:

const toArray = map =>
  Array.from(map, ([key, value]) => ({
    key,
    value: value instanceof Map ? toArray(value) : value
  }));

d3.rollup(): группировка с агрегацией

Функция d3.rollup() расширяет идею группировки, добавляя вычисление агрегированных значений на каждом уровне. Вместо массивов элементов возвращаются результаты редукции.

Сигнатура

d3.rollup(data, reducer, ...keys)
  • data — массив объектов
  • reducer — функция агрегации
  • keys — функции группировки

Простой пример: сумма значений

const rolled = d3.rollup(
  data,
  v => d3.sum(v, d => d.value),
  d => d.country
);

Результат:

Map {
  "KZ" => 30,
  "RU" => 30
}

Здесь:

  • v — массив элементов внутри группы
  • d3.sum вычисляет сумму значений

Среднее значение по группам

const rolled = d3.rollup(
  data,
  v => d3.mean(v, d => d.value),
  d => d.country
);

Результат содержит средние значения по каждой стране.


Многоуровневый rollup

const rolled = d3.rollup(
  data,
  v => d3.sum(v, d => d.value),
  d => d.country,
  d => d.city
);

Структура:

Map {
  "KZ" => Map {
    "Karaganda" => 10,
    "Almaty" => 20
  },
  "RU" => Map {
    "Moscow" => 30
  }
}

Каждый уровень сохраняет агрегированное значение вместо массива объектов.


Отличие d3.group от d3.rollup

Характер результата

  • d3.group() → вложенные Map с массивами данных
  • d3.rollup() → вложенные Map с агрегированными значениями

Когда используется d3.group

Подходит для случаев, когда требуется:

  • сохранить все исходные данные
  • выполнить последующую обработку внутри групп
  • построить сложные интерактивные иерархии

Пример сценария:

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

Когда используется d3.rollup

Подходит для задач:

  • статистическая агрегация
  • построение сводных таблиц
  • предварительная подготовка данных для графиков

Примеры метрик:

  • сумма
  • среднее
  • минимум / максимум
  • количество элементов

Использование нескольких функций-ключей

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

Пример с числовой категоризацией

const rolled = d3.rollup(
  data,
  v => d3.sum(v, d => d.value),
  d => d.country,
  d => Math.floor(d.value / 10)
);

Это создаёт группы по стране и диапазонам значений.


Работа с ключами и типами данных

Ключи в группировке не ограничиваются строками:

d3.group(data, d => new Date(d.date).getFullYear());

или даже объектами:

const keyFn = d => ({ type: d.type });

d3.group(data, keyFn);

Map позволяет использовать такие ключи без приведения к строке, в отличие от обычных объектов JavaScript.


Использование агрегаторов d3-array

В d3.rollup() часто применяются функции из d3-array:

  • d3.sum
  • d3.mean
  • d3.min
  • d3.max
  • d3.median
  • d3.count (через v.length)

Пример комбинирования:

const stats = d3.rollup(
  data,
  v => ({
    total: d3.sum(v, d => d.value),
    avg: d3.mean(v, d => d.value),
    count: v.length
  }),
  d => d.category
);

Здесь агрегатор возвращает объект, а не число.


Преобразование результата в плоскую структуру

Для визуализаций часто требуется преобразовать Map в массив объектов:

const flattened = Array.from(rolled, ([key, value]) => ({
  key,
  value
}));

Для вложенных структур:

function flatten(map, depth = 0) {
  return Array.from(map, ([key, value]) => ({
    key,
    value: value instanceof Map ? flatten(value, depth + 1) : value,
    depth
  }));
}

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

Гистограммы по группам

d3.rollup() часто используется для подготовки данных:

const counts = d3.rollup(
  data,
  v => v.length,
  d => d.category
);

Далее результат легко преобразуется в шкалу:

const dataset = Array.from(counts, ([category, count]) => ({
  category,
  count
}));

Подготовка данных для stacked charts

const stacked = d3.rollup(
  data,
  v => d3.sum(v, d => d.value),
  d => d.group,
  d => d.subgroup
);

Такой формат удобно преобразовать для d3.stack().


Поведение при пустых данных

Если входной массив пуст, обе функции возвращают пустой Map:

d3.group([], d => d.key); // Map {}
d3.rollup([], v => v.length, d => d.key); // Map {}

Это важно для безопасной обработки динамических данных.


Сложные комбинации агрегаций

Внутри rollup допустимо использовать произвольную логику:

d3.rollup(
  data,
  v => {
    const sorted = v.sort((a, b) => b.value - a.value);
    return {
      top: sorted[0],
      bottom: sorted[sorted.length - 1]
    };
  },
  d => d.group
);

Таким образом можно строить не только числовые метрики, но и структурные срезы данных.


Итерация по результатам

d3.group

grouped.forEach((value, key) => {
  console.log(key, value);
});

d3.rollup

rolled.forEach((value, key) => {
  console.log(key, value);
});

Методы идентичны, так как обе структуры основаны на Map.


Комбинация group и rollup в одном пайплайне

Иногда используется двухэтапная обработка:

const grouped = d3.group(data, d => d.country);

const stats = new Map(
  Array.from(grouped, ([country, values]) => [
    country,
    d3.rollup(values, v => d3.sum(v, d => d.value), d => d.city)
  ])
);

Так создаются гибридные структуры: группировка + агрегирование на разных уровнях.