Представление иерархии в D3: d3.hierarchy()

Модуль иерархий в D3 предназначен для работы с древовидными структурами данных. Функция d3.hierarchy() преобразует вложенный объект JavaScript в специальную структуру узлов D3, пригодную для построения:

  • древовидных диаграмм;
  • кластерных схем;
  • treemap;
  • sunburst-диаграмм;
  • circle packing;
  • дендрограмм;
  • организационных структур.

После преобразования каждый узел получает набор служебных свойств и методов, позволяющих обходить дерево, вычислять глубину, потомков, связи между элементами и выполнять визуальное размещение.


Структура иерархических данных

Иерархия представляет собой дерево объектов, где каждый элемент содержит дочерние узлы.

Простейший пример:

const data = {
    name: "Компания",
    children: [
        {
            name: "Отдел разработки",
            children: [
                { name: "Frontend" },
                { name: "Backend" }
            ]
        },
        {
            name: "Маркетинг"
        }
    ]
};

Здесь:

  • корневой узел — "Компания";
  • дочерние элементы находятся в массиве children;
  • вложенность определяет структуру дерева.

Создание иерархии

Для преобразования данных используется d3.hierarchy():

const root = d3.hierarchy(data);

Результатом становится объект типа HierarchyNode.


Структура объекта HierarchyNode

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

Основные свойства

Свойство Описание
data Исходный объект данных
depth Глубина узла
height Максимальная глубина потомков
parent Родительский узел
children Массив дочерних узлов
value Числовое значение
x, y Координаты после layout
id Пользовательский идентификатор

Свойство depth

Показывает уровень вложенности.

console.log(root.depth);

Корневой элемент всегда имеет:

0

Дочерние элементы:

1

Следующий уровень:

2

Свойство height

height показывает расстояние до самого глубокого потомка.

Пример:

console.log(root.height);

Если дерево имеет 3 уровня вложенности, значение будет:

2

Доступ к исходным данным

Исходный объект хранится в node.data.

console.log(root.data.name);

Результат:

Компания

Дочерние элементы

Свойство children содержит массив узлов.

console.log(root.children);

Каждый элемент массива — полноценный HierarchyNode.


Родительский узел

Свойство parent позволяет подняться вверх по дереву.

const department = root.children[0];

console.log(department.parent.data.name);

Результат:

Компания

Настройка поля дочерних элементов

По умолчанию D3 ищет массив children.

При другой структуре данных можно указать собственный accessor.

Пример:

const data = {
    title: "Root",
    items: [
        {
            title: "Node A",
            items: [
                { title: "Node B" }
            ]
        }
    ]
};

const root = d3.hierarchy(data, d => d.items);

Второй аргумент — функция получения потомков.


Обход дерева

Метод descendants()

Возвращает всех потомков дерева.

const nodes = root.descendants();

console.log(nodes);

Порядок:

  1. корень;
  2. дочерние элементы;
  3. вложенные потомки.

Метод ancestors()

Возвращает цепочку родителей.

const node = root.children[0].children[0];

console.log(node.ancestors());

Результат:

[
    Frontend,
    Отдел разработки,
    Компания
]

Метод leaves()

Возвращает только конечные узлы.

const leaves = root.leaves();

Листьями считаются элементы без children.


Создаёт массив связей между узлами.

const links = root.links();

Каждая связь имеет вид:

{
    source: ...,
    target: ...
}

Используется при построении линий дерева.


Поиск узлов

Метод find()

Позволяет найти элемент по условию.

const node = root.find(d => d.data.name === "Backend");

console.log(node);

Итерация по дереву

Метод each()

Выполняет обход всех узлов.

root.each(node => {
    console.log(node.data.name);
});

Метод eachBefore()

Обход сверху вниз.

root.eachBefore(node => {
    console.log(node.data.name);
});

Метод eachAfter()

Обход снизу вверх.

root.eachAfter(node => {
    console.log(node.data.name);
});

Сортировка узлов

Метод sort() изменяет порядок дочерних элементов.

root.sort((a, b) => b.height - a.height);

Пример сортировки по имени:

root.sort((a, b) => {
    return d3.ascending(a.data.name, b.data.name);
});

Вычисление значений узлов

Метод sum()

Для визуализаций вроде treemap каждому узлу часто требуется числовое значение.

root.sum(d => d.value);

Пример данных:

const data = {
    name: "Компания",
    children: [
        {
            name: "Frontend",
            value: 40
        },
        {
            name: "Backend",
            value: 60
        }
    ]
};

После вызова:

root.value

получит сумму всех потомков:

100

Метод count()

Подсчитывает количество листьев.

root.count();

После этого:

console.log(root.value);

будет содержать число конечных узлов.


Копирование дерева

Метод copy()

Создаёт независимую копию иерархии.

const clone = root.copy();

Это полезно при разных вариантах layout для одного набора данных.


Построение дерева с d3.tree()

d3.hierarchy() часто используется совместно с layout-функциями.

Пример:

const root = d3.hierarchy(data);

const treeLayout = d3.tree()
    .size([500, 300]);

treeLayout(root);

После layout каждый узел получает координаты:

node.x
node.y

Визуализация дерева

SVG-контейнер

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

Создание линий

svg.selectAll("line")
    .data(root.links())
    .enter()
    .append("line")
    .attr("x1", d => d.source.y)
    .attr("y1", d => d.source.x)
    .attr("x2", d => d.target.y)
    .attr("y2", d => d.target.x)
    .attr("stroke", "#999");

Создание узлов

svg.selectAll("circle")
    .data(root.descendants())
    .enter()
    .append("circle")
    .attr("cx", d => d.y)
    .attr("cy", d => d.x)
    .attr("r", 6)
    .attr("fill", "steelblue");

Добавление подписей

svg.selectAll("text")
    .data(root.descendants())
    .enter()
    .append("text")
    .attr("x", d => d.y + 10)
    .attr("y", d => d.x)
    .text(d => d.data.name);

Использование с d3.cluster()

cluster() похож на tree(), но размещает узлы равномерно.

const cluster = d3.cluster()
    .size([400, 300]);

cluster(root);

Разница:

  • tree() минимизирует пересечения;
  • cluster() создаёт более симметричную структуру.

Использование с d3.treemap()

Treemap отображает иерархию прямоугольниками.

const treemap = d3.treemap()
    .size([800, 600])
    .padding(2);

root.sum(d => d.value);

treemap(root);

После layout:

d.x0
d.y0
d.x1
d.y1

определяют границы прямоугольника.


Использование с d3.partition()

Применяется для sunburst-диаграмм.

const partition = d3.partition()
    .size([2 * Math.PI, 300]);

partition(root);

Узлы получают:

d.x0
d.x1
d.y0
d.y1

Использование с d3.pack()

Circle packing строит вложенные окружности.

const pack = d3.pack()
    .size([600, 600])
    .padding(5);

root.sum(d => d.value);

pack(root);

Каждый узел получает:

d.x
d.y
d.r

Формирование иерархии из табличных данных

Иногда данные находятся в плоском формате.

Пример:

const data = [
    { id: "A", parent: "" },
    { id: "B", parent: "A" },
    { id: "C", parent: "A" },
    { id: "D", parent: "B" }
];

Для таких случаев используется d3.stratify().

const stratify = d3.stratify()
    .id(d => d.id)
    .parentId(d => d.parent);

const root = stratify(data);

После этого создаётся полноценная иерархия D3.


Разница между d3.hierarchy() и d3.stratify()

Метод Тип данных
d3.hierarchy() Вложенные объекты
d3.stratify() Плоские таблицы

Работа с большими деревьями

При визуализации крупных структур возникают проблемы:

  • перегрузка DOM;
  • пересечение подписей;
  • снижение производительности;
  • медленный рендеринг SVG.

Оптимизация производительности

Ограничение глубины

const visible = root.descendants()
    .filter(d => d.depth < 3);

Сворачивание ветвей

Часть дерева временно скрывается:

node._children = node.children;
node.children = null;

Canvas вместо SVG

Для тысяч элементов предпочтительнее Canvas.


Ленивое раскрытие узлов

Данные загружаются только при раскрытии ветви.


Динамическое обновление дерева

D3 поддерживает реактивное обновление.

Пример:

const nodes = svg.selectAll("circle")
    .data(root.descendants(), d => d.data.name);

nodes.enter()
    .append("circle");

nodes.exit().remove();

Анимация изменений

svg.selectAll("circle")
    .transition()
    .duration(500)
    .attr("cx", d => d.y)
    .attr("cy", d => d.x);

Интерактивное раскрытие узлов

Пример обработчика:

function toggle(event, d) {
    if (d.children) {
        d._children = d.children;
        d.children = null;
    } else {
        d.children = d._children;
        d._children = null;
    }

    update();
}

Частые ошибки

Отсутствие массива children

Ошибка структуры:

{
    name: "Root",
    child: []
}

Правильно:

{
    name: "Root",
    children: []
}

или использование accessor-функции.


Отсутствие sum() для treemap

Без вычисления значений treemap не сможет определить размеры элементов.

root.sum(d => d.value);

Циклические ссылки

Иерархия должна быть деревом.

Недопустимо:

A -> B -> C -> A

Практический пример полного дерева

const data = {
    name: "Root",
    children: [
        {
            name: "A",
            children: [
                { name: "A1" },
                { name: "A2" }
            ]
        },
        {
            name: "B"
        }
    ]
};

const width = 800;
const height = 500;

const svg = d3.select("svg")
    .attr("width", width)
    .attr("height", height);

const root = d3.hierarchy(data);

const treeLayout = d3.tree()
    .size([height, width - 200]);

treeLayout(root);

svg.selectAll("line")
    .data(root.links())
    .enter()
    .append("line")
    .attr("x1", d => d.source.y)
    .attr("y1", d => d.source.x)
    .attr("x2", d => d.target.y)
    .attr("y2", d => d.target.x)
    .attr("stroke", "#999");

svg.selectAll("circle")
    .data(root.descendants())
    .enter()
    .append("circle")
    .attr("cx", d => d.y)
    .attr("cy", d => d.x)
    .attr("r", 5);

svg.selectAll("text")
    .data(root.descendants())
    .enter()
    .append("text")
    .attr("x", d => d.y + 10)
    .attr("y", d => d.x + 5)
    .text(d => d.data.name);

Внутренний механизм работы d3.hierarchy()

При создании иерархии D3:

  1. рекурсивно обходит дерево;
  2. создаёт HierarchyNode для каждого элемента;
  3. связывает родителей и потомков;
  4. вычисляет глубину;
  5. подготавливает структуру для layout-алгоритмов.

Поддерживаемые типы визуализаций

Layout Назначение
tree() Классическое дерево
cluster() Кластерная схема
treemap() Прямоугольная карта
partition() Sunburst
pack() Вложенные окружности

Архитектура типичного проекта

Обычно работа с иерархиями строится по этапам:

Данные
↓
d3.hierarchy()
↓
sum()/sort()
↓
layout
↓
SVG / Canvas
↓
анимация и интерактивность

Роль d3.hierarchy() в экосистеме D3

d3.hierarchy() является центральным механизмом для всех древовидных layout-алгоритмов D3. Функция не выполняет визуализацию самостоятельно, а формирует универсальную структуру данных, которая затем используется различными алгоритмами размещения. Благодаря этому одна и та же иерархия может быть визуализирована несколькими способами без изменения исходных данных.