Работа с вложенными структурами в D3.js опирается на концепцию
последовательных data join-ов, где результат одного связывания данных
становится входом для следующего уровня визуализации. В отличие от
плоских массивов, вложенные данные требуют организации цепочек
.data() внутри уже существующих выборок, формируя каскадные
структуры DOM.
Ключевой принцип: каждый уровень DOM-иерархии отражает уровень структуры данных, а каждая вложенная выборка управляется отдельным data join.
Типичный паттерн вложенной привязки данных строится вокруг конструкции:
selection
.selectAll(".group")
.data(data)
.join("g")
.each(function(d) {
d3.select(this)
.selectAll("circle")
.data(d.values)
.join("circle");
});
Здесь первый .data(data) создает группы, а второй
.data(d.values) — элементы внутри каждой группы. Важный
момент заключается в том, что второй join выполняется в контексте
каждого узла отдельно.
Вложенные вызовы .data() получают доступ к текущему
объекту данных через функцию-аксессор:
.data(d => d.values)
На каждом уровне d представляет объект родительского
слоя. Таким образом формируется дерево зависимостей, где:
Метод .each() часто используется для явного разбиения
логики по уровням:
const groups = svg
.selectAll(".group")
.data(dataset)
.join("g")
.attr("class", "group");
groups.each(function(groupData) {
d3.select(this)
.selectAll("rect")
.data(groupData.items)
.join("rect")
.attr("width", d => d.value);
});
Здесь каждый g становится контейнером для собственного
независимого data join.
Часто данные имеют форму:
const data = [
{
name: "A",
values: [10, 20, 30]
},
{
name: "B",
values: [15, 25]
}
];
Такая структура естественно отображается через два уровня
.data():
При работе с вложенными структурами критически важно использовать
key функции на каждом уровне.
.data(d => d.values, d => d.id)
Без ключей D3 будет пересоздавать DOM-элементы при каждом обновлении, что приводит к потере состояния и анимаций.
На вложенных уровнях ключи должны быть локально уникальными внутри группы:
groups.selectAll("circle")
.data(d => d.values, v => v.id);
При обновлении вложенной структуры важно понимать, что внешний и внутренний join не синхронизируются автоматически.
const groups = svg.selectAll("g")
.data(data)
.join("g");
groups.selectAll("rect")
.data(d => d.values)
.join("rect");
При изменении data необходимо повторно вызвать внешний
join, чтобы корректно пересобрать внутренние связи.
Часто используется разделение логики на функцию, которая вызывается для каждого контейнера:
function renderGroup(selection) {
selection
.selectAll("circle")
.data(d => d.values)
.join("circle")
.attr("r", d => d);
}
svg.selectAll("g")
.data(data)
.join("g")
.call(renderGroup);
Такой подход повышает читаемость и позволяет переиспользовать рендеринг вложенных структур.
Современные структуры часто формируются через
d3.group:
const grouped = d3.group(data, d => d.category);
Результат — Map, который удобно преобразуется в массив для data join:
const hierarchyData = Array.from(grouped, ([key, values]) => ({
key,
values
}));
После этого применяется стандартный вложенный
.data().
Каждый уровень имеет собственный lifecycle:
const groups = svg.selectAll("g")
.data(data)
.join(
enter => enter.append("g"),
update => update,
exit => exit.remove()
);
groups.selectAll("rect")
.data(d => d.values)
.join(
enter => enter.append("rect"),
update => update,
exit => exit.remove()
);
Важно учитывать, что exit на внутреннем уровне не удаляет
родительский g, если внешний join не изменился.
Вложенные .data() часто ломаются при неправильном
использовании стрелочных функций:
selection.selectAll("rect")
.data(function(d) { return d.values; })
Использование function сохраняет доступ к this, что
полезно при сложных DOM-структурах, тогда как стрелочные функции
фиксируют контекст лексически.
Типовая структура:
const groups = svg.selectAll(".group")
.data(data)
.join("g")
.attr("transform", d => `translate(${d.x},0)`);
groups.selectAll("rect")
.data(d => d.values)
.join("rect")
.attr("y", (d, i) => i * 10)
.attr("width", d => d.value);
Каждая группа управляет собственной шкалой расположения элементов.
При изменении только внутреннего массива возможна ситуация, когда внешний join не пересоздается, а внутренний теряет актуальность. Решение заключается в принудительном обновлении внутреннего слоя:
groups
.data(data)
.join("g")
.each(function(d) {
d3.select(this)
.selectAll("circle")
.data(d.values, v => v.id)
.join("circle");
});
Для древовидных данных применяется специализированный механизм:
const root = d3.hierarchy(data);
Он автоматически формирует уровни:
После этого .data() часто заменяется на
root.descendants():
svg.selectAll("circle")
.data(root.descendants())
.join("circle");
Иногда используется гибридный подход:
d3.hierarchy для логической структуры.data() внутри узлов для дополнительных слоев
визуализацииnodes.each(function(d) {
d3.select(this)
.selectAll("text")
.data(d.data.labels)
.join("text");
});
Основные узкие места:
d => d.valuesg.each() без мемоизацииОптимизация достигается через:
Структура визуализации часто строится как независимые слои:
const layer1 = svg.selectAll("g.layer1")
.data(data)
.join("g");
const layer2 = layer1.selectAll("g.layer2")
.data(d => d.values)
.join("g");
Каждый слой имеет собственный lifecycle и может обновляться независимо, сохраняя связь через родительские данные.
Индекс на внутреннем уровне относится к локальному массиву:
.data(d => d.values)
.attr("x", (d, i) => i * 15)
Важно учитывать, что i не связан с внешним уровнем и не
глобален.
Комбинация нескольких .data() позволяет строить
полностью динамическую DOM-иерархию, где каждый уровень зависит от
результата предыдущего:
Такая модель масштабируется до произвольной глубины вложенности при условии строгого управления контекстом данных и ключами.