При работе с выборками в D3.js нередко возникает необходимость выполнить произвольный код для каждого элемента или вынести повторяющиеся операции в переиспользуемые функции. Для этого библиотека предоставляет два важных метода:
.each() — перебор элементов выборки;.call() — передача выборки во внешнюю функцию.Оба метода активно используются при построении сложных визуализаций, создании плагинов, компонентном подходе и организации архитектуры D3-приложений.
.each()Метод .each() позволяет выполнить функцию отдельно для
каждого элемента выборки.
Внутри callback-функции доступны:
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() нужен в случаях, когда:
Пример:
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);
});
.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);
});
Однако важно понимать:
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
interfaceD3 построен на цепочках вызовов.
.call() поддерживает этот стиль, поскольку возвращает
исходную выборку.
selection
.attr("opacity", 0.5)
.call(addBorder)
.style("cursor", "pointer");
.call() от обычного вызова функцииБез .call():
decorate(selection);
С .call():
selection.call(decorate);
Преимущества .call():
.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() — за обработку отдельных элементов.
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);
Неправильно:
function style(selection) {
selection.attr("fill", "red");
return 5;
}
.call() игнорирует возвращаемое значение функции, но
подобный код создаёт путаницу.
Лучше:
function style(selection) {
selection.attr("fill", "red");
}
.each() и
.call()| Метод | Назначение |
|---|---|
.each() |
Выполнение логики для каждого элемента |
.call() |
Передача выборки в переиспользуемую функцию |
.each().each() подходит, если:
this;.call().call() подходит, если:
В крупных проектах распространена следующая схема:
function component(selection) {
selection.each(function(data) {
// логика элемента
});
}
Использование:
selection.call(component);
Такой подход:
.each() может быть затратным при обработке большого
количества элементов.
Особенно при:
Рекомендации:
d3.select(this);.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;
};
Именно поэтому:
.each()Упрощённая логика:
selection.each = function(callback) {
for (const node of nodes) {
callback.call(node, node.__data__);
}
return selection;
};
Отсюда появляется:
this;