Обход выборок: .each() и .call()

При работе с выборками в D3.js нередко возникает необходимость выполнить произвольный код для каждого элемента или вынести повторяющиеся операции в переиспользуемые функции. Для этого библиотека предоставляет два важных метода:

  • .each() — перебор элементов выборки;
  • .call() — передача выборки во внешнюю функцию.

Оба метода активно используются при построении сложных визуализаций, создании плагинов, компонентном подходе и организации архитектуры D3-приложений.


Метод .each()

Назначение

Метод .each() позволяет выполнить функцию отдельно для каждого элемента выборки.

Внутри callback-функции доступны:

  • текущий DOM-элемент через this;
  • связанные данные;
  • индекс элемента;
  • группа элементов.

Синтаксис:

selection.each(function(d, i, nodes) {

});

Параметры:

Параметр Описание
d Данные, связанные с элементом
i Индекс элемента
nodes Массив элементов текущей группы

Базовый пример

d3.selectAll("div")
    .each(function(d, i) {
        console.log(i);
    });

Для каждого div будет выведен его индекс.


Работа с this

Главная особенность .each() — возможность обращаться к текущему DOM-элементу через this.

d3.selectAll("circle")
    .each(function() {
        d3.select(this)
            .attr("stroke", "black");
    });

Внутри callback:

this === текущий DOM-элемент

Это особенно важно, поскольку многие операции D3 выполняются именно относительно текущего узла.


Отличие .each() от .attr()

Многие операции можно выполнить через callback в .attr():

selection.attr("r", function(d) {
    return d.value;
});

Но .each() нужен в случаях, когда:

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

Пример:

d3.selectAll("rect")
    .each(function(d) {

        const rect = d3.select(this);

        rect.attr("fill", "steelblue");
        rect.attr("stroke", "black");
        rect.attr("stroke-width", 2);

    });

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

.each() часто используется для создания локальных выборок внутри каждого элемента.

Пример: генерация подписей

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

groups.each(function(d) {

    const group = d3.select(this);

    group.append("circle")
        .attr("r", d.radius);

    group.append("text")
        .text(d.label);

});

Каждый g получает собственные дочерние элементы.


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

Индекс особенно полезен при позиционировании элементов.

d3.selectAll("rect")
    .each(function(d, i) {

        d3.select(this)
            .attr("x", i * 50);

    });

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

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

d3.selectAll("li")
    .each(function(d, i, nodes) {

        console.log(nodes.length);

    });

Это позволяет:

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

Контекст this и стрелочные функции

Одна из наиболее распространённых ошибок — использование стрелочных функций.

Неправильно:

selection.each((d) => {

    console.log(this);

});

У стрелочных функций нет собственного this.

Правильно:

selection.each(function(d) {

    console.log(this);

});

Изменение DOM внутри .each()

Метод подходит для сложных операций с DOM.

d3.selectAll(".card")
    .each(function(d) {

        const el = d3.select(this);

        if (d.active) {
            el.classed("active", true);
        } else {
            el.remove();
        }

    });

Асинхронные операции

.each() допускает использование асинхронной логики.

selection.each(async function(d) {

    const result = await fetchData(d.id);

    d3.select(this)
        .text(result.value);

});

Однако важно понимать:

  • D3 не ожидает завершения await;
  • выполнение продолжается сразу;
  • возможны проблемы синхронизации.

Использование .each() после переходов

Метод часто применяется совместно с анимациями.

selection
    .transition()
    .duration(1000)
    .each(function() {

        console.log("transition started");

    });

Однако у transitions существует собственная версия .each().

В современных версиях D3 предпочтительнее использовать события:

selection
    .transition()
    .on("start", function() {

    });

Возвращаемое значение

.each() возвращает исходную выборку.

Это делает возможным chaining.

selection
    .each(function() {

    })
    .attr("fill", "red");

Практический пример: генерация сложных карточек

const cards = d3.select("#container")
    .selectAll(".card")
    .data(data)
    .enter()
    .append("div")
    .attr("class", "card");

cards.each(function(d) {

    const card = d3.select(this);

    card.append("h2")
        .text(d.title);

    card.append("p")
        .text(d.description);

    card.append("span")
        .text(d.date);

});

Метод .call()

Назначение

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

Это позволяет:

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

Синтаксис:

selection.call(function(selection) {

});

Базовый пример

function colorize(selection) {

    selection.attr("fill", "steelblue");

}

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

Фактически происходит следующее:

colorize(selection);

Но при этом .call() возвращает исходную выборку.


Почему .call() важен

Без .call() код быстро становится громоздким:

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

circles.attr("fill", "red");
circles.attr("stroke", "black");
circles.attr("r", 10);

С .call():

function styleCircles(selection) {

    selection
        .attr("fill", "red")
        .attr("stroke", "black")
        .attr("r", 10);

}

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

Передача аргументов

.call() поддерживает дополнительные параметры.

function setColor(selection, color) {

    selection.attr("fill", color);

}

d3.selectAll("rect")
    .call(setColor, "orange");

Внутри функции:

selection → первый аргумент
"orange" → второй аргумент

Несколько аргументов

function style(selection, fill, stroke, width) {

    selection
        .attr("fill", fill)
        .attr("stroke", stroke)
        .attr("stroke-width", width);

}

selection.call(style, "red", "black", 2);

Повторное использование компонентов

.call() особенно важен в компонентном подходе.

Пример: ось координат

const axis = d3.axisBottom(xScale);

svg.append("g")
    .call(axis);

Именно так работает большая часть API D3.


Создание собственного компонента

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

function grid(selection) {

    selection
        .attr("stroke", "#ccc")
        .attr("stroke-width", 1);

}

svg.selectAll("line")
    .call(grid);

Компоненты с настройками

function label(selection, color, size) {

    selection
        .attr("fill", color)
        .style("font-size", size + "px");

}

svg.selectAll("text")
    .call(label, "black", 14);

Компоновка поведения

.call() удобно использовать для композиции операций.

function base(selection) {

    selection.attr("fill", "steelblue");

}

function bordered(selection) {

    selection.attr("stroke", "black");

}

selection
    .call(base)
    .call(bordered);

Инкапсуляция сложной логики

Метод помогает скрывать внутреннюю реализацию.

function card(selection) {

    selection.each(function(d) {

        const root = d3.select(this);

        root.append("h3")
            .text(d.title);

        root.append("p")
            .text(d.description);

    });

}

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

cards.call(card);

.call() и fluent interface

D3 построен на цепочках вызовов.

.call() поддерживает этот стиль, поскольку возвращает исходную выборку.

selection
    .attr("opacity", 0.5)
    .call(addBorder)
    .style("cursor", "pointer");

Отличие .call() от обычного вызова функции

Без .call():

decorate(selection);

С .call():

selection.call(decorate);

Преимущества .call():

  • сохраняется chain;
  • код читается естественнее;
  • поддерживается стиль D3;
  • проще комбинировать операции.

Комбинация .each() и .call()

Оба метода часто используются вместе.

function card(selection) {

    selection.each(function(d) {

        const root = d3.select(this);

        root.append("h2")
            .text(d.title);

        root.append("p")
            .text(d.description);

    });

}

selection.call(card);

.call() отвечает за переиспользование компонента, а .each() — за обработку отдельных элементов.


Реальный паттерн D3-компонента

function chart(selection) {

    selection.each(function(data) {

        const svg = d3.select(this);

        svg.append("circle")
            .attr("r", data.radius)
            .attr("cx", data.x)
            .attr("cy", data.y);

    });

}

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

d3.select("#chart")
    .datum(data)
    .call(chart);

Так строится большинство профессиональных D3-компонентов.


Типичные ошибки при использовании .call()

Ошибка: вызов функции вместо передачи

Неправильно:

selection.call(myFunction());

Правильно:

selection.call(myFunction);

Ошибка с потерей chain

Неправильно:

function style(selection) {

    selection.attr("fill", "red");

    return 5;

}

.call() игнорирует возвращаемое значение функции, но подобный код создаёт путаницу.

Лучше:

function style(selection) {

    selection.attr("fill", "red");

}

Сравнение .each() и .call()

Метод Назначение
.each() Выполнение логики для каждого элемента
.call() Передача выборки в переиспользуемую функцию

Когда использовать .each()

.each() подходит, если:

  • требуется работа с отдельными DOM-узлами;
  • создаются вложенные элементы;
  • нужен доступ к this;
  • логика зависит от конкретного элемента.

Когда использовать .call()

.call() подходит, если:

  • код нужно переиспользовать;
  • создаётся компонент;
  • требуется композиция поведения;
  • необходимо сохранить chain-стиль D3.

Архитектурный подход D3

В крупных проектах распространена следующая схема:

function component(selection) {

    selection.each(function(data) {

        // логика элемента

    });

}

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

selection.call(component);

Такой подход:

  • изолирует код;
  • улучшает читаемость;
  • упрощает поддержку;
  • позволяет создавать независимые визуальные модули.

Производительность

.each() может быть затратным при обработке большого количества элементов.

Особенно при:

  • создании множества вложенных выборок;
  • сложных DOM-операциях;
  • постоянных перерисовках.

Рекомендации:

  • минимизировать количество d3.select(this);
  • избегать лишних изменений DOM;
  • кэшировать выборки;
  • выносить повторяющуюся логику в .call().

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

function tooltip(selection) {

    selection.each(function(d) {

        const el = d3.select(this);

        el.on("mouseenter", function() {

            d3.select("#tooltip")
                .style("display", "block")
                .text(d.value);

        });

        el.on("mouseleave", function() {

            d3.select("#tooltip")
                .style("display", "none");

        });

    });

}

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

svg.selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
    .call(tooltip);

Внутреннее устройство .call()

Упрощённо реализация выглядит так:

selection.call = function(callback, ...args) {

    callback(this, ...args);

    return this;

};

Именно поэтому:

  • выборка передаётся первым аргументом;
  • chain сохраняется;
  • функция может модифицировать selection напрямую.

Внутреннее устройство .each()

Упрощённая логика:

selection.each = function(callback) {

    for (const node of nodes) {

        callback.call(node, node.__data__);

    }

    return selection;

};

Отсюда появляется:

  • контекст this;
  • доступ к данным;
  • перебор элементов.