При построении визуализаций данные редко используются в исходном
виде. Перед отображением массивы объектов обычно проходят этап
агрегации, группировки, фильтрации и вычисления производных значений. В
D3.js подобные операции выполняются средствами модуля
d3-array.
Сводные таблицы позволяют:
Большинство операций строится вокруг:
d3.group()d3.rollup()d3.index()d3.flatGroup()d3.flatRollup()d3.sort()d3.filter()d3.map()d3.reduce()Исходный набор данных:
const sales = [
{ city: "Москва", category: "Ноутбуки", amount: 120000 },
{ city: "Москва", category: "Смартфоны", amount: 80000 },
{ city: "Казань", category: "Ноутбуки", amount: 50000 },
{ city: "Казань", category: "Смартфоны", amount: 40000 },
{ city: "Москва", category: "Ноутбуки", amount: 70000 }
];
Каждый объект представляет отдельную запись продаж.
Функция d3.group() создаёт структуру Map,
где ключом выступает результат функции-группировщика.
const grouped = d3.group(sales, d => d.city);
Результат:
Map {
"Москва" => [...],
"Казань" => [...]
}
Получение данных конкретной группы:
console.log(grouped.get("Москва"));
d3.group() поддерживает неограниченное количество
уровней.
const grouped = d3.group(
sales,
d => d.city,
d => d.category
);
Структура:
Map {
"Москва" => Map {
"Ноутбуки" => [...],
"Смартфоны" => [...]
},
"Казань" => Map {
"Ноутбуки" => [...],
"Смартфоны" => [...]
}
}
Доступ к вложенным данным:
grouped
.get("Москва")
.get("Ноутбуки");
Многие визуализации работают именно с массивами.
const result = Array.from(grouped, ([key, values]) => {
return {
city: key,
values: values
};
});
Результат:
[
{
city: "Москва",
values: [...]
},
{
city: "Казань",
values: [...]
}
]
d3.groups() возвращает обычный массив вместо
Map.
const groups = d3.groups(sales, d => d.city);
Результат:
[
[
"Москва",
[...]
],
[
"Казань",
[...]
]
]
Преимущество:
d3.rollup() выполняет группировку и одновременно
вычисляет итоговое значение.
const totals = d3.rollup(
sales,
v => d3.sum(v, d => d.amount),
d => d.city
);
Результат:
Map {
"Москва" => 270000,
"Казань" => 90000
}
const averages = d3.rollup(
sales,
v => d3.mean(v, d => d.amount),
d => d.city
);
const maxValues = d3.rollup(
sales,
v => d3.max(v, d => d.amount),
d => d.city
);
Часто требуется сохранить несколько статистических показателей.
const stats = d3.rollup(
sales,
v => ({
total: d3.sum(v, d => d.amount),
average: d3.mean(v, d => d.amount),
max: d3.max(v, d => d.amount),
count: v.length
}),
d => d.city
);
Результат:
Map {
"Москва" => {
total: 270000,
average: 90000,
max: 120000,
count: 3
}
}
d3.rollups() аналогичен d3.rollup(), но
возвращает массив.
const result = d3.rollups(
sales,
v => d3.sum(v, d => d.amount),
d => d.city
);
Результат:
[
["Москва", 270000],
["Казань", 90000]
]
При глубокой вложенности Map становится неудобным. Для
этого используется плоское представление.
const flat = d3.flatGroup(
sales,
d => d.city,
d => d.category
);
Результат:
[
["Москва", "Ноутбуки", [...]],
["Москва", "Смартфоны", [...]],
["Казань", "Ноутбуки", [...]]
]
const flatRollup = d3.flatRollup(
sales,
v => d3.sum(v, d => d.amount),
d => d.city,
d => d.category
);
Результат:
[
["Москва", "Ноутбуки", 190000],
["Москва", "Смартфоны", 80000],
["Казань", "Ноутбуки", 50000]
]
Такой формат особенно полезен для:
d3.index() создаёт карту уникальных записей.
const index = d3.index(
sales,
d => d.city
);
Если ключи не уникальны, возникнет ошибка.
Обычно используется для быстрого доступа:
const cityData = index.get("Москва");
const nestedIndex = d3.index(
sales,
d => d.city,
d => d.category
);
const sorted = sales.sort(
(a, b) => b.amount - a.amount
);
d3.sort() не изменяет исходный массив.
const sorted = d3.sort(
sales,
d => d.amount
);
const sorted = d3.sort(
sales,
(a, b) => d3.descending(a.amount, b.amount)
);
const sorted = d3.sort(
sales,
d => d.city
);
const filtered = sales.filter(
d => d.amount > 60000
);
В новых версиях D3 чаще используется нативный
Array.filter(), однако D3 хорошо интегрируется с
итераторами.
const transformed = sales.map(d => ({
...d,
amountK: d.amount / 1000
}));
const prepared = sales.map(d => ({
city: d.city,
category: d.category,
amount: d.amount,
tax: d.amount * 0.2
}));
const total = sales.reduce(
(sum, d) => sum + d.amount,
0
);
const count = sales.reduce(
(acc) => acc + 1,
0
);
const total = d3.sum(
sales,
d => d.amount
);
const average = d3.mean(
sales,
d => d.amount
);
const median = d3.median(
sales,
d => d.amount
);
Функция возвращает минимальное и максимальное значение.
const extent = d3.extent(
sales,
d => d.amount
);
Результат:
[40000, 120000]
const max = d3.max(
sales,
d => d.amount
);
const min = d3.min(
sales,
d => d.amount
);
Исходные данные:
const data = [
{ month: "Январь", phones: 30, laptops: 20 },
{ month: "Февраль", phones: 40, laptops: 15 }
];
Создание стека:
const stack = d3.stack()
.keys(["phones", "laptops"]);
const series = stack(data);
Результат:
[
[
[0, 30],
[0, 40]
],
[
[30, 50],
[40, 55]
]
]
const pie = d3.pie()
.value(d => d.amount);
const arcs = pie(sales);
Каждый элемент получает:
d3.bin() используется для группировки чисел по
диапазонам.
const values = [10, 20, 25, 30, 35, 40, 45];
const bins = d3.bin()(values);
const histogram = d3.bin()
.thresholds(5);
const bins = histogram(values);
const histogram = d3.bin()
.domain([0, 100])
.thresholds([20, 40, 60, 80]);
Создание декартового произведения:
const result = d3.cross(
["Москва", "Казань"],
["Ноутбуки", "Смартфоны"]
);
Результат:
[
["Москва", "Ноутбуки"],
["Москва", "Смартфоны"],
["Казань", "Ноутбуки"],
["Казань", "Смартфоны"]
]
Объединение вложенных массивов:
const arrays = [
[1, 2],
[3, 4],
[5, 6]
];
const merged = d3.merge(arrays);
const names = ["A", "B", "C"];
const values = [10, 20, 30];
const zipped = d3.zip(names, values);
Результат:
[
["A", 10],
["B", 20],
["C", 30]
]
const matrix = [
[1, 2, 3],
[4, 5, 6]
];
const transposed = d3.transpose(matrix);
Результат:
[
[1, 4],
[2, 5],
[3, 6]
]
При работе с визуализациями важно формировать стабильные идентификаторы.
const prepared = sales.map(d => ({
id: `${d.city}-${d.category}`,
...d
}));
Исходные значения:
const values = [10, 50, 80];
Нормализация:
const max = d3.max(values);
const normalized = values.map(
d => d / max
);
let sum = 0;
const cumulative = values.map(v => {
sum += v;
return sum;
});
const parseDate = d3.timeParse("%Y-%m-%d");
const data = raw.map(d => ({
date: parseDate(d.date),
value: +d.value
}));
const grouped = d3.rollup(
data,
v => d3.sum(v, d => d.value),
d => d3.timeMonth(d.date)
);
const cleaned = data.filter(
d => d.value != null
);
const prepared = data.map(d => ({
...d,
value: d.value ?? 0
}));
const heatmap = d3.flatRollup(
sales,
v => d3.sum(v, d => d.amount),
d => d.city,
d => d.category
).map(([city, category, value]) => ({
city,
category,
value
}));
const nodes = [
{ id: "A" },
{ id: "B" }
];
const links = [
{ source: "A", target: "B" }
];
Преобразование плоских данных в иерархию:
const hierarchy = d3.hierarchy(data);
hierarchy.sum(d => d.value);
hierarchy.sort(
(a, b) => b.value - a.value
);
Исходные данные:
const raw = [
{ region: "Север", sales: 100 },
{ region: "Юг", sales: 200 },
{ region: "Север", sales: 150 }
];
Полная обработка:
const result = d3.rollups(
raw,
values => ({
total: d3.sum(values, d => d.sales),
average: d3.mean(values, d => d.sales)
}),
d => d.region
).map(([region, stats]) => ({
region,
...stats
}));
Результат:
[
{
region: "Север",
total: 250,
average: 125
},
{
region: "Юг",
total: 200,
average: 200
}
]
Плохо:
data.map(d => ({
total: expensiveCalculation(d)
}));
Хорошо:
const cache = new Map();
const prepared = data.map(d => {
if (!cache.has(d.id)) {
cache.set(d.id, expensiveCalculation(d));
}
return {
...d,
total: cache.get(d.id)
};
});
D3 хорошо сочетается с функциональным стилем.
const updated = data.map(d => ({
...d,
active: true
}));
Исходный массив не изменяется.
const result = sales
.filter(d => d.amount > 50000)
.map(d => ({
...d,
tax: d.amount * 0.2
}))
.sort((a, b) => b.amount - a.amount);
Ключи должны быть стабильными:
svg.selectAll("rect")
.data(data, d => d.id);
При отсутствии ключей D3 может:
Плохо:
data.sort(...)
Лучше:
const sorted = d3.sort(data, ...);
Проблема:
{ value: "100" }
Решение:
{ value: +d.value }
Плохо:
d.amount.toFixed(2)
Безопаснее:
(d.amount ?? 0).toFixed(2)
Крупные проекты обычно разделяют:
Пример:
const raw = await d3.csv("sales.csv");
const cleaned = cleanData(raw);
const aggregated = aggregate(cleaned);
const chartData = prepareChartData(aggregated);
renderChart(chartData);
Подобная архитектура упрощает: