Стекируемая площадь с d3.stack()

Построение стекированных площадных диаграмм в D3.js основано на преобразовании многослойных временных или категориальных рядов в структуру, удобную для визуального наложения. Ключевым механизмом выступает d3.stack(), который преобразует исходные данные в массивы нижних и верхних границ каждого слоя.


Исходные данные для стекированной площади представляют собой таблицу, где каждая строка соответствует точке по оси X, а остальные поля — значения категорий:

const data = [
  { year: 2018, apples: 120, bananas: 80, cherries: 45 },
  { year: 2019, apples: 150, bananas: 90, cherries: 60 },
  { year: 2020, apples: 170, bananas: 110, cherries: 75 },
  { year: 2021, apples: 200, bananas: 130, cherries: 95 }
];

Каждое поле после year рассматривается как отдельный слой стековой диаграммы.


Принцип работы d3.stack()

Функция d3.stack() преобразует таблицу объектов в структуру слоёв:

const stack = d3.stack()
  .keys(["apples", "bananas", "cherries"]);

const series = stack(data);

Результат series имеет следующий формат:

  • массив слоёв (по числу ключей)
  • каждый слой содержит пары [y0, y1]
  • y0 — нижняя граница
  • y1 — верхняя граница

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

[
  [ [0, 120], [0, 150], [0, 170], [0, 200] ],   // apples
  [ [120, 200], [150, 240], [170, 280], ... ],  // bananas
  [ [200, 245], [240, 300], [280, 355], ... ]   // cherries
]

Настройка порядка слоёв

Порядок наложения влияет на визуальное восприятие. Он управляется методами:

d3.stack()
  .keys(["apples", "bananas", "cherries"])
  .order(d3.stackOrderNone)
  .offset(d3.stackOffsetNone);

Основные варианты:

  • d3.stackOrderNone — порядок как в keys
  • d3.stackOrderAscending — по возрастанию суммарных значений
  • d3.stackOrderDescending — по убыванию
  • d3.stackOrderInsideOut — оптимизация визуального баланса

Смещения (offset)

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

  • d3.stackOffsetNone — абсолютные значения
  • d3.stackOffsetExpand — нормализация до 100%
  • d3.stackOffsetSilhouette — центрирование вокруг нуля
  • d3.stackOffsetWiggle — «streamgraph»-форма

Пример процентного отображения:

const stack = d3.stack()
  .keys(["apples", "bananas", "cherries"])
  .offset(d3.stackOffsetExpand);

Масштабирование осей

Для построения графика используются линейные шкалы:

const xScale = d3.scaleLinear()
  .domain(d3.extent(data, d => d.year))
  .range([0, width]);

const yScale = d3.scaleLinear()
  .domain([0, d3.max(series[series.length - 1], d => d[1])])
  .range([height, 0]);

Для stackOffsetExpand домен по Y фиксируется:

.domain([0, 1])

Генератор площадей

Основной визуальный элемент строится через d3.area():

const area = d3.area()
  .x(d => xScale(d.data.year))
  .y0(d => yScale(d[0]))
  .y1(d => yScale(d[1]));

Каждый элемент d содержит:

  • d.data — исходная строка данных
  • d[0] — нижняя граница
  • d[1] — верхняя граница

Построение SVG-слоёв

Каждый слой отрисовывается отдельным path:

svg.selectAll("path.layer")
  .data(series)
  .join("path")
  .attr("class", "layer")
  .attr("d", area)
  .attr("fill", (d, i) => color(i));

Цветовая шкала:

const color = d3.scaleOrdinal()
  .domain(["apples", "bananas", "cherries"])
  .range(["#4caf50", "#ff9800", "#f44336"]);

Полный пример построения

const svg = d3.select("svg");

const stack = d3.stack()
  .keys(["apples", "bananas", "cherries"]);

const series = stack(data);

const xScale = d3.scaleLinear()
  .domain(d3.extent(data, d => d.year))
  .range([0, width]);

const yScale = d3.scaleLinear()
  .domain([0, d3.max(series[series.length - 1], d => d[1])])
  .range([height, 0]);

const area = d3.area()
  .x(d => xScale(d.data.year))
  .y0(d => yScale(d[0]))
  .y1(d => yScale(d[1]));

svg.selectAll("path")
  .data(series)
  .join("path")
  .attr("d", area)
  .attr("fill", (d, i) => d3.schemeCategory10[i]);

Работа с ключами динамически

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

const keys = Object.keys(data[0]).filter(k => k !== "year");

const stack = d3.stack()
  .keys(keys);

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


Анимация изменения данных

При обновлении данных применяется переход:

svg.selectAll("path")
  .data(series)
  .join(
    enter => enter.append("path")
      .attr("d", area),
    update => update
      .transition()
      .duration(750)
      .attr("d", area)
  );

Плавная интерполяция достигается за счёт перерасчёта d[0] и d[1].


Нормализованная стековая диаграмма (100%)

При использовании stackOffsetExpand значения приводятся к долям:

const stack = d3.stack()
  .keys(keys)
  .offset(d3.stackOffsetExpand);

Ось Y интерпретируется как:

  • 0 — 0%
  • 1 — 100%

Такая форма применяется для сравнения долей категорий во времени.


Streamgraph-эффект

Смещение wiggle создаёт органическую форму:

const stack = d3.stack()
  .keys(keys)
  .offset(d3.stackOffsetWiggle);

График перестаёт быть строго базовым по оси X и приобретает волнообразную структуру, минимизируя визуальные «скачки».


Обработка пропущенных значений

Если данные содержат null или NaN, стек может разрываться. Применяется предобработка:

data.forEach(d => {
  if (!d.apples) d.apples = 0;
  if (!d.bananas) d.bananas = 0;
  if (!d.cherries) d.cherries = 0;
});

Либо фильтрация на этапе доступа:

.y0(d => yScale(d[0] || 0))

Управление порядком слоёв через custom order

Для специализированной визуализации порядок может вычисляться вручную:

const stack = d3.stack()
  .keys(keys)
  .order((series) => series.sort((a, b) => b.total - a.total));

Каждый слой содержит агрегированное значение total, используемое для сортировки.


Добавление осей и сетки

Оси создаются стандартными компонентами:

svg.append("g")
  .attr("transform", `translate(0,${height})`)
  .call(d3.axisBottom(xScale));

svg.append("g")
  .call(d3.axisLeft(yScale));

Сетка может быть добавлена через дополнительные оси с уменьшенной прозрачностью линий.


Практическая структура рендеринга

Типовая организация включает:

  • загрузку данных
  • вычисление ключей
  • создание stack
  • формирование scales
  • генерацию area
  • отрисовку paths
  • обновление при изменении данных

Эта последовательность обеспечивает стабильную реакцию графика на любые трансформации входных данных.