Вложенные выборки и контекст this

Библиотека D3.js строится вокруг концепции выборок. Любой вызов d3.select() или d3.selectAll() возвращает объект selection — специальную структуру, содержащую ссылки на DOM-элементы и методы для их обработки.

Простейшая выборка:

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

Множественная выборка:

const circles = d3.selectAll("circle");

Каждый метод выборки возвращает новую выборку, благодаря чему формируются цепочки вызовов:

d3.select("svg")
    .attr("width", 800)
    .attr("height", 400)
    .style("background", "#f5f5f5");

Главная особенность D3 заключается в том, что выборка может быть вложенной. Именно вложенные выборки позволяют строить сложные иерархические визуализации, таблицы, графики, группы SVG-элементов и повторяющиеся структуры.


Вложенные выборки

Вложенная выборка возникает тогда, когда новый select() или selectAll() вызывается относительно уже существующей выборки.

Пример:

const rows = d3.select("table")
    .selectAll("tr");

Сначала выбирается элемент <table>, затем внутри него — все строки <tr>.

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

const cells = d3.select("table")
    .selectAll("tr")
    .selectAll("td");

Здесь:

  1. выбирается таблица;
  2. внутри таблицы — все строки;
  3. внутри каждой строки — все ячейки.

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


Как D3 хранит вложенные выборки

Внутри D3 выборка представляет собой массив групп элементов.

Упрощённо:

[
    [element1, element2, element3]
]

После вложенного selectAll() структура становится глубже:

[
    [td1, td2],
    [td3, td4],
    [td5, td6]
]

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

Например:

d3.selectAll("tr")
    .selectAll("td");

создаёт отдельную группу <td> для каждой строки <tr>.

Это фундаментальная особенность D3, влияющая на:

  • привязку данных;
  • индексы;
  • порядок элементов;
  • работу callback-функций;
  • значение this.

Разница между select и selectAll

select

Метод select() выбирает только первый подходящий элемент внутри каждого родителя.

d3.selectAll(".card")
    .select("h2");

Если имеется 10 карточек, будет выбрано 10 заголовков — по одному из каждой карточки.

selectAll

Метод selectAll() выбирает все совпадающие элементы внутри каждого родителя.

d3.selectAll(".card")
    .selectAll("p");

Теперь внутри каждой карточки будут выбраны все абзацы.


Контекст вложенной выборки

Ключевая идея:

selection.selectAll(...)

не ищет элементы глобально по документу.

Поиск происходит только внутри текущей выборки.

Пример:

const groups = d3.selectAll("g");

groups.selectAll("circle");

Каждая группа <g> ищет собственные <circle>.

Это особенно важно при работе с SVG:

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

const groups = chart
    .selectAll("g")
    .data(data)
    .enter()
    .append("g");

Теперь каждая группа может содержать собственные линии, текст и фигуры:

groups.append("circle");
groups.append("text");

Вложенные выборки и привязка данных

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

Пример:

const data = [
    [10, 20, 30],
    [40, 50, 60]
];

const rows = d3.select("body")
    .selectAll("div.row")
    .data(data)
    .enter()
    .append("div");

Каждая строка получает массив:

[10, 20, 30]

или:

[40, 50, 60]

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

rows.selectAll("span")
    .data(d => d)
    .enter()
    .append("span")
    .text(d => d);

Что происходит:

  • rows содержит строки;
  • каждая строка имеет собственный массив;
  • data(d => d) возвращает массив текущей строки;
  • D3 создаёт вложенные элементы для каждого значения.

Результат:

<div class="row">
    <span>10</span>
    <span>20</span>
    <span>30</span>
</div>

<div class="row">
    <span>40</span>
    <span>50</span>
    <span>60</span>
</div>

Контекст this в D3

Во многих callback-функциях D3 предоставляет специальный контекст this.

Пример:

d3.selectAll("rect")
    .attr("width", function(d) {
        console.log(this);
    });

this указывает на текущий DOM-элемент.

Это позволяет обращаться к элементу напрямую:

d3.select(this)
    .style("fill", "red");

Где доступен this

Контекст this работает в:

  • attr()
  • style()
  • property()
  • classed()
  • text()
  • html()
  • each()
  • filter()
  • on()
  • data()
  • transition callbacks

Пример:

selection.each(function(d, i) {
    console.log(this);
});

Аргументы callback-функций

Большинство callback-функций D3 получают:

function(d, i, nodes)

Где:

Аргумент Значение
d данные элемента
i индекс
nodes массив DOM-элементов

Пример:

d3.selectAll("circle")
    .attr("cx", function(d, i, nodes) {
        console.log(d);
        console.log(i);
        console.log(nodes);
    });

При этом:

this === nodes[i]

Почему this важен

D3 активно использует контекст исполнения функций.

Пример:

selection.on("click", function() {
    d3.select(this)
        .classed("active", true);
});

Без this пришлось бы выполнять повторный поиск элемента.

Контекст делает обработку:

  • быстрее;
  • проще;
  • безопаснее;
  • независимой от глобальных селекторов.

Проблема стрелочных функций

Стрелочные функции не создают собственный this.

Ошибка:

selection.on("click", (d) => {
    console.log(this);
});

Здесь this не указывает на DOM-элемент.

Вместо этого он наследуется из внешней области видимости.

Правильный вариант:

selection.on("click", function(d) {
    console.log(this);
});

Когда стрелочные функции допустимы

Если this не используется, стрелочные функции подходят отлично.

Пример:

selection.attr("width", d => d.value * 10);

Здесь контекст элемента не нужен.


Сравнение function и arrow function

Обычная функция

selection.style("color", function() {
    return this.dataset.color;
});

this — текущий элемент.

Стрелочная функция

selection.style("color", () => {
    return this.dataset.color;
});

Ошибка: this не является элементом.


Использование this внутри each

Метод each() особенно часто применяется с this.

Пример:

selection.each(function(d, i) {
    const element = d3.select(this);

    element.append("title")
        .text(d.name);
});

Каждый элемент обрабатывается отдельно.


Вложенные выборки и this

Во вложенных структурах this всегда относится к текущему элементу конкретной группы.

Пример:

rows.selectAll("td")
    .style("color", function() {
        console.log(this);
    });

this указывает именно на конкретную ячейку <td>.


Родительский элемент через this.parentNode

Часто требуется доступ к родителю.

Пример:

selection.each(function() {
    console.log(this.parentNode);
});

Можно получить выборку родителя:

const parent = d3.select(this.parentNode);

Работа с дочерними элементами

Через this можно выполнять локальные выборки:

selection.each(function() {

    d3.select(this)
        .select("text")
        .style("font-weight", "bold");

});

Это особенно удобно в компонентах графиков.


Контекст в обработчиках событий

Обработчики событий — одна из самых важных областей использования this.

Пример:

d3.selectAll("circle")
    .on("mouseenter", function() {

        d3.select(this)
            .attr("r", 20);

    });

При наведении изменяется только текущий круг.


Использование event и this

Современный синтаксис D3:

selection.on("click", function(event, d) {

    console.log(event);
    console.log(d);
    console.log(this);

});

Где:

Значение Содержимое
event объект события
d данные
this DOM-элемент

this и transition

Контекст сохраняется и внутри анимаций.

Пример:

selection.transition()
    .duration(1000)
    .attr("x", function() {

        return this.dataset.target;

    });

Вложенные SVG-группы

Типичная структура D3-графика:

<svg>
    <g class="axis"></g>
    <g class="bars"></g>
    <g class="labels"></g>
</svg>

Создание:

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

const barsGroup = svg.append("g")
    .attr("class", "bars");

barsGroup.selectAll("rect")
    .data(data)
    .enter()
    .append("rect");

Здесь:

  • barsGroup становится контекстом;
  • прямоугольники создаются только внутри группы .bars.

Локальные и глобальные выборки

Глобальная выборка

d3.selectAll("rect");

Поиск по всему документу.

Локальная выборка

group.selectAll("rect");

Поиск только внутри группы.

Локальные выборки:

  • быстрее;
  • безопаснее;
  • уменьшают вероятность конфликтов;
  • упрощают повторное использование компонентов.

Компонентный подход

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

Пример:

function createLegend(container, data) {

    const items = container
        .selectAll(".legend-item")
        .data(data)
        .enter()
        .append("g");

    items.append("rect");

    items.append("text");
}

Использование:

createLegend(
    d3.select("#legend"),
    legendData
);

Все операции происходят внутри переданного контейнера.


Индексы во вложенных выборках

Индекс i относится только к текущей группе.

Пример:

rows.selectAll("td")
    .text(function(d, i) {
        return i;
    });

Каждая строка начнёт индексацию заново:

0 1 2
0 1 2
0 1 2

Это следствие групповой структуры вложенных выборок.


Доступ к группе nodes

Третий аргумент callback-функции:

function(d, i, nodes)

позволяет получить всю текущую группу элементов.

Пример:

selection.attr("x", function(d, i, nodes) {

    if (i === nodes.length - 1) {
        return 100;
    }

    return 0;
});

Метод filter и контекст this

Метод filter() также использует this.

selection.filter(function(d) {
    return this.dataset.active === "true";
});

Можно комбинировать данные и DOM-состояние.


Метод call и вложенные выборки

call() позволяет передавать выборку в функцию.

function highlight(selection) {

    selection
        .style("stroke", "red");

}

Использование:

d3.selectAll("circle")
    .call(highlight);

Во вложенных структурах:

groups.call(highlight);

Каждая группа остаётся независимой.


Типичные ошибки

Использование стрелочной функции вместо function

Ошибка:

selection.each(() => {
    d3.select(this);
});

Правильно:

selection.each(function() {
    d3.select(this);
});

Потеря контекста при вложенности

Ошибка:

selection.each(function() {

    setTimeout(function() {
        console.log(this);
    });

});

Внутри setTimeout контекст меняется.

Исправление:

selection.each(function() {

    const element = this;

    setTimeout(function() {
        console.log(element);
    });

});

Глобальные выборки вместо локальных

Ошибка:

d3.selectAll("text")
    .style("fill", "red");

Изменятся все тексты документа.

Безопаснее:

group.selectAll("text")
    .style("fill", "red");

Практический пример вложенных выборок

Создание таблицы:

const data = [
    ["A", "B", "C"],
    ["D", "E", "F"]
];

const table = d3.select("body")
    .append("table");

const rows = table
    .selectAll("tr")
    .data(data)
    .enter()
    .append("tr");

rows.selectAll("td")
    .data(d => d)
    .enter()
    .append("td")
    .text(d => d);

Механизм работы:

  1. создаются строки;
  2. каждая строка получает собственный массив;
  3. внутри строки создаются ячейки;
  4. каждая ячейка получает собственное значение.

Практический пример с this

d3.selectAll("td")
    .on("click", function(event, d) {

        d3.select(this)
            .style("background", "orange");

        console.log(d);

    });

При клике изменяется только выбранная ячейка.


Архитектура вложенных визуализаций

Практически любой сложный график в D3 строится через вложенные группы:

svg
└── g.chart
    ├── g.axis-x
    ├── g.axis-y
    ├── g.bars
    └── g.labels

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

Это обеспечивает:

  • модульность;
  • изоляцию;
  • предсказуемость;
  • повторное использование кода;
  • удобную анимацию;
  • независимую привязку данных.

Ключевые особенности вложенных выборок

Особенность Описание
Локальный контекст Поиск элементов внутри родителя
Групповая структура Выборки организованы по группам
Наследование данных Дочерние элементы получают данные родителя
Локальные индексы Индексы считаются внутри группы
Независимость компонентов Группы работают автономно

Ключевые особенности this

Особенность Описание
Указывает на DOM-элемент Текущий элемент обработки
Работает только с function Не работает со стрелочными функциями
Доступен в callbacks attr, style, each, on и др.
Упрощает локальную обработку Нет необходимости в повторных селекторах
Связан с группой Контекст зависит от текущей выборки