В D3.js текстовые подписи строятся поверх SVG-графики как отдельные
элементы <text>, привязанные к данным через механизм
data binding. Ключевая особенность заключается в том, что текст не имеет
собственной геометрии взаимодействия с компоновкой — он зависит от
выбранной стратегии позиционирования и контекста визуализации.
Минимальная модель размещения строится вокруг явного задания координат:
d3.select("svg")
.selectAll("text.label")
.data(data)
.enter()
.append("text")
.attr("class", "label")
.attr("x", d => xScale(d.x))
.attr("y", d => yScale(d.y))
.text(d => d.value);
В этой схеме текст является прямой проекцией данных в пространство SVG. Такой подход подходит для простых диаграмм, где плотность подписей низкая и отсутствует риск наложений.
Критически важным параметром становится управление системой
координат: шкалы xScale и yScale определяют не
только позицию, но и визуальную читаемость всей подписи.
SVG-текст по умолчанию позиционируется относительно базовой линии и начала строки. Для корректного выравнивания применяются свойства:
text-anchor — горизонтальное выравниваниеdominant-baseline — вертикальное выравнивание.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
Типовые стратегии:
Центрирование (scatter plot, точки):
middle / middle — подпись строго над точкой данныхВыравнивание по оси X (bar chart):
start для левостороннего размещенияmiddle для центрирования внутри столбцаend для выравнивания к правому краюВертикальная читаемость:
hanging используется для текстов, расположенных над
объектамиКорректная настройка базовой линии часто важнее точного вычисления координаты, так как позволяет компенсировать визуальные смещения без дополнительных трансформаций.
В bar chart основная проблема заключается в ограниченном пространстве внутри столбца. Выделяют три стратегии:
.attr("x", d => xScale(d.category) + xScale.bandwidth() / 2)
.attr("y", d => yScale(d.value) + 15)
.attr("text-anchor", "middle")
.attr("fill", "white")
Используется при достаточной высоте столбцов. Ключевой параметр — контраст текста и фона.
.attr("y", d => yScale(d.value) - 5)
.attr("text-anchor", "middle")
Применяется при малых значениях, когда текст внутри столбца теряет читаемость.
Комбинированный подход:
.attr("y", d => {
const barHeight = height - yScale(d.value);
return barHeight > 20
? yScale(d.value) + 15
: yScale(d.value) - 5;
})
.attr("fill", d => {
const barHeight = height - yScale(d.value);
return barHeight > 20 ? "white" : "black";
});
Здесь размещение зависит от геометрии данных, а не фиксированной логики.
При высокой плотности точек текстовые подписи начинают пересекаться, что требует дополнительных стратегий.
.attr("dx", 5)
.attr("dy", -5)
Метод минимально влияет на структуру графика, но не решает конфликтов полностью.
Используется модуль force simulation:
const simulation = d3.forceSimulation(data)
.force("x", d3.forceX(d => xScale(d.x)))
.force("y", d3.forceY(d => yScale(d.y)))
.force("collide", d3.forceCollide(10))
.stop();
Далее координаты подписей корректируются отдельно от точек:
simulation.tick(50);
labels
.attr("x", d => d.x)
.attr("y", d => d.y);
Эта стратегия разделяет визуальное положение данных и текстовых элементов, вводя независимую систему стабилизации.
Часто используется связка:
Это позволяет сохранить точную геометрию данных при улучшении читаемости текста.
tspan для многострочных подписейSVG-текст не поддерживает автоматический перенос строк, поэтому
применяется разбиение через <tspan>:
const label = d3.select("text")
.selectAll("tspan")
.data(d.value.split(" "))
.enter()
.append("tspan")
.attr("x", 0)
.attr("dy", (d, i) => i === 0 ? 0 : 12)
.text(d => d);
Ключевая стратегия — управление вертикальным шагом через
dy, а не создание отдельных текстовых элементов.
Применяется в:
Автоматический перенос реализуется вручную через измерение длины строки:
function wrap(text, width) {
text.each(function() {
const text = d3.select(this);
const words = text.text().split(/\s+/).reverse();
let word;
let line = [];
let lineNumber = 0;
const lineHeight = 1.1;
let tspan = text.text(null)
.append("tspan")
.attr("x", 0)
.attr("dy", "0em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", 0)
.attr("dy", `${++lineNumber * lineHeight}em`)
.text(word);
}
}
});
}
Эта техника опирается на getComputedTextLength, что
делает её зависимой от рендеринга SVG, а не только от математических
вычислений.
Для pie chart используется геометрический центр сегмента:
const arcLabel = d3.arc()
.innerRadius(0)
.outerRadius(radius);
labels.attr("transform", d => `translate(${arcLabel.centroid(d)})`);
Функция centroid вычисляет точку равновесия дуги,
обеспечивая естественное расположение подписи.
При малых сегментах применяется стратегия вынесения текста за пределы диаграммы с соединительными линиями.
SVG поддерживает привязку текста к пути через
textPath:
svg.append("defs")
.append("path")
.attr("id", "curve")
.attr("d", lineGenerator(data));
svg.append("text")
.append("textPath")
.attr("href", "#curve")
.text("подпись вдоль линии");
Это используется в:
Ключевой фактор — синхронизация текста с изменением пути при обновлении данных.
При увеличении количества элементов возникает необходимость фильтрации отображаемого текста.
data
.sort((a, b) => b.value - a.value)
.slice(0, 10)
Используется для отображения только значимых значений.
.attr("display", d => d.value > threshold ? null : "none")
Позволяет динамически управлять плотностью подписей без изменения структуры DOM.
Группировка близких точек с последующим выводом агрегированного текста:
Эта стратегия уменьшает визуальный шум и повышает читаемость при больших наборах данных.
При масштабировании графика текстовые элементы требуют отдельной обработки, чтобы избежать чрезмерного увеличения или исчезновения:
svg.call(d3.zoom().on("zoom", (event) => {
g.attr("transform", event.transform);
}));
Проблема заключается в том, что текст масштабируется вместе с графикой. Для стабилизации применяют обратное масштабирование:
labels.attr("transform", d => {
const t = d3.zoomTransform(svg.node());
return `translate(${t.apply([d.x, d.y])}) scale(${1 / t.k})`;
});
Разделение визуализации на слои:
Аннотационный слой часто управляется отдельно:
const annotations = svg.append("g")
.attr("class", "annotations");
Такое разделение позволяет независимо управлять плотностью, стилем и поведением текста без влияния на геометрию данных.
SVG ограничен в типографике, поэтому применяется HTML внутри SVG:
svg.append("foreignObject")
.attr("x", x)
.attr("y", y)
.attr("width", 120)
.attr("height", 50)
.html("<div class='label'>Сложная подпись</div>");
Это позволяет использовать CSS:
Однако увеличивается стоимость рендеринга и усложняется синхронизация с трансформациями SVG.
При изменении формы графика (например, radial layout) текст требует преобразований:
.attr("transform", d => {
const angle = (d.angle * 180) / Math.PI;
return `rotate(${angle})`;
})
Для сохранения читаемости часто применяется инверсия текста при перевороте:
Основные подходы:
Каждая стратегия выбирается исходя из плотности данных и требований к точности отображения, при этом часто комбинируется несколько методов одновременно для достижения устойчивой визуальной структуры