Сгруппированная столбчатая диаграмма строится на основе разбиения категорий на подгруппы, где каждая категория содержит несколько связанных значений, отображаемых рядом друг с другом. В экосистеме визуализации данных на основе D3.js этот тип диаграмм опирается на комбинацию шкал, вложенных структур данных и точного расчёта позиционирования прямоугольников.
Основой служит массив объектов, где каждая запись содержит категорию и набор значений:
const data = [
{
category: "2020",
values: [
{ name: "A", value: 30 },
{ name: "B", value: 80 },
{ name: "C", value: 45 }
]
},
{
category: "2021",
values: [
{ name: "A", value: 50 },
{ name: "B", value: 60 },
{ name: "C", value: 70 }
]
}
];
Такая структура позволяет легко выполнять вложенные привязки данных
через selectAll().data().enter() на двух уровнях: категории
и подкатегории.
Для сгруппированной диаграммы требуется две оси категорий и одна числовая ось:
x0 — категорииx1 — подкатегорииy — числовые значенияПример конфигурации:
const x0 = d3.scaleBand()
.domain(data.map(d => d.category))
.range([0, width])
.paddingInner(0.2);
const x1 = d3.scaleBand()
.domain(["A", "B", "C"])
.range([0, x0.bandwidth()])
.padding(0.1);
const y = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);
Цветовая шкала добавляет различие между подгруппами:
const color = d3.scaleOrdinal()
.domain(["A", "B", "C"])
.range(["#4e79a7", "#f28e2b", "#e15759"]);
Каждая категория создаёт отдельную группу g, внутри
которой располагаются столбцы подкатегорий.
const groups = svg.selectAll("g.layer")
.data(data)
.enter()
.append("g")
.attr("transform", d => `translate(${x0(d.category)},0)`);
Внутри каждой группы добавляются прямоугольники:
groups.selectAll("rect")
.data(d => d.values)
.enter()
.append("rect")
.attr("x", d => x1(d.name))
.attr("y", d => y(d.value))
.attr("width", x1.bandwidth())
.attr("height", d => height - y(d.value))
.attr("fill", d => color(d.name));
Ключевой момент заключается в том, что смещение по оси X разбивается на два уровня: сначала категория, затем подкатегория внутри неё.
Стековая столбчатая диаграмма отличается тем, что значения не располагаются рядом, а накладываются друг на друга по вертикали. Это позволяет анализировать вклад каждой подгруппы в общий итог.
Подготовка данных выполняется через d3.stack().
Исходные данные преобразуются в формат плоских рядов:
const stackGenerator = d3.stack()
.keys(["A", "B", "C"]);
const stackedData = stackGenerator([
{ category: "2020", A: 30, B: 80, C: 45 },
{ category: "2021", A: 50, B: 60, C: 70 }
]);
Каждый элемент результата содержит нижнюю и верхнюю границу слоя:
[
[[0, 30], [0, 50]], // A
[[30, 110], [50, 110]], // B
[[110, 155], [110, 180]] // C
]
Ось X остаётся категориальной:
const x = d3.scaleBand()
.domain(["2020", "2021"])
.range([0, width])
.padding(0.2);
Ось Y строится как линейная шкала суммарных значений:
const y = d3.scaleLinear()
.domain([0, 200])
.range([height, 0]);
Цветовая шкала аналогична сгруппированной диаграмме:
const color = d3.scaleOrdinal()
.domain(["A", "B", "C"])
.range(["#4e79a7", "#f28e2b", "#e15759"]);
Каждый слой соответствует отдельной серии данных:
const series = svg.selectAll("g.series")
.data(stackedData)
.enter()
.append("g")
.attr("fill", d => color(d.key));
Каждый слой содержит прямоугольники:
series.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("x", d => x(d.data.category))
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth());
Сгруппированная диаграмма опирается на независимое размещение столбцов:
valueСтековая диаграмма использует интервалы:
d[0]d[1]Это создаёт эффект накопления значений.
При увеличении количества категорий или серий данных важную роль играет динамическая настройка доменов:
const maxValue = d3.max(stackedData[stackedData.length - 1], d => d[1]);
y.domain([0, maxValue]);
Для сгруппированной диаграммы аналогично:
const maxValue = d3.max(data, d =>
d3.max(d.values, v => v.value)
);
y.domain([0, maxValue]);
Анимация улучшает читаемость изменений данных. В D3 используется
метод transition():
rects.transition()
.duration(750)
.attr("y", d => y(d.value))
.attr("height", d => height - y(d.value));
Для стековой диаграммы:
rects.transition()
.duration(750)
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]));
Сгруппированная диаграмма требует двух уровней join:
Стековая диаграмма использует один уровень join на серии:
stackbandwidth() между x0
и x1stack().keys()y(d[0]) - y(d[1])В некоторых визуализациях оба подхода могут использоваться параллельно: сгруппированные столбцы отображают отдельные метрики, а стековые — их внутреннюю структуру. Это требует раздельных SVG-групп и независимых шкал, синхронизированных по оси категорий.
const x = d3.scaleBand()
.domain(categories)
.range([0, width])
.padding(0.3);
При неизвестном количестве серий данные извлекаются автоматически:
const keys = Object.keys(data[0]).filter(k => k !== "category");
const stack = d3.stack().keys(keys);
Это позволяет строить универсальные стековые диаграммы без жёсткой привязки к структуре.
Оси добавляют контекст:
svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
Сетка часто строится через вспомогательные линии:
svg.append("g")
.attr("class", "grid")
.call(d3.axisLeft(y)
.tickSize(-width)
.tickFormat("")
);