Сгруппированная и стекированная столбчатая диаграмма

Сгруппированная столбчатая диаграмма строится на основе разбиения категорий на подгруппы, где каждая категория содержит несколько связанных значений, отображаемых рядом друг с другом. В экосистеме визуализации данных на основе 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());

Различия в вычислении координат

Сгруппированная диаграмма опирается на независимое размещение столбцов:

  • каждый элемент имеет собственную базовую ось Y
  • высота вычисляется напрямую как 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:

  1. категории
  2. элементы внутри категорий

Стековая диаграмма использует один уровень join на серии:

  • каждая серия уже содержит все категории
  • вложенность реализуется через структуру stack

Частые источники ошибок позиционирования

  • неверное согласование bandwidth() между x0 и x1
  • отсутствие согласованного порядка ключей в stack().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("")
  );