Построение стекированных площадных диаграмм в 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() преобразует таблицу объектов в
структуру слоёв:
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 — порядок как в keysd3.stackOrderAscending — по возрастанию суммарных
значенийd3.stackOrderDescending — по убываниюd3.stackOrderInsideOut — оптимизация визуального
балансаСмещение определяет базовую линию стека:
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] — верхняя границаКаждый слой отрисовывается отдельным 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].
При использовании stackOffsetExpand значения приводятся
к долям:
const stack = d3.stack()
.keys(keys)
.offset(d3.stackOffsetExpand);
Ось Y интерпретируется как:
Такая форма применяется для сравнения долей категорий во времени.
Смещение 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))
Для специализированной визуализации порядок может вычисляться вручную:
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));
Сетка может быть добавлена через дополнительные оси с уменьшенной прозрачностью линий.
Типовая организация включает:
Эта последовательность обеспечивает стабильную реакцию графика на любые трансформации входных данных.