Генератор символов: d3.symbol()

В D3.js геометрические маркеры используются для обозначения точек данных в диаграммах рассеяния, временных рядах, легендах и комбинированных визуализациях. Для этого применяется генератор пути d3.symbol(), который формирует SVG-путь (<path d="...">) из набора стандартных графических фигур.

Генератор символов работает как функция, возвращающая строку path-данных в формате SVG, и может динамически изменять форму и размер в зависимости от данных.


Принцип работы d3.symbol()

d3.symbol() создаёт генератор, который преобразует два параметра:

  • type — форма символа
  • size — площадь символа в пикселях

Возвращаемое значение используется как атрибут d в SVG <path>.

Базовая структура:

const symbol = d3.symbol()
  .type(d3.symbolCircle)
  .size(64);

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

svg.append("path")
  .attr("d", symbol)
  .attr("fill", "steelblue");

Типы встроенных символов

D3 предоставляет набор стандартных геометрических форм:

  • d3.symbolCircle — круг
  • d3.symbolCross — крест
  • d3.symbolDiamond — ромб
  • d3.symbolSquare — квадрат
  • d3.symbolStar — звезда
  • d3.symbolTriangle — треугольник
  • d3.symbolWye — Y-образная фигура

Пример переключения формы:

const symbol = d3.symbol()
  .type(d3.symbolStar)
  .size(120);

Параметр size и геометрия масштабирования

size определяет площадь символа, а не его ширину или высоту напрямую.

Это ключевая особенность генератора:

  • увеличение size влияет на масштаб пропорционально
  • форма сохраняет геометрические пропорции
  • фактический bounding box вычисляется автоматически

Пример:

d3.symbol().type(d3.symbolCircle).size(20)
d3.symbol().type(d3.symbolCircle).size(200)

Второй символ будет не в 10 раз шире, а примерно в √10 раз больше по линейным размерам.


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

Генератор символов часто применяется в визуализациях, где форма кодирует категорию, а размер — величину.

const data = [
  { x: 10, y: 20, type: d3.symbolCircle, size: 64 },
  { x: 40, y: 80, type: d3.symbolStar, size: 120 },
  { x: 70, y: 30, type: d3.symbolDiamond, size: 90 }
];

svg.selectAll("path")
  .data(data)
  .enter()
  .append("path")
  .attr("d", d => d3.symbol()
    .type(d.type)
    .size(d.size)())
  .attr("transform", d => `translate(${d.x},${d.y})`)
  .attr("fill", "black");

Особенность вызова: генератор возвращает функцию, поэтому требуется дополнительный вызов ().


Динамическое назначение типа символа

Тип символа может задаваться функцией, зависящей от данных:

const symbol = d3.symbol()
  .type(d => d.type)
  .size(d => d.size);

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

.attr("d", symbol)

Такой подход позволяет унифицировать создание элементов без ручного пересоздания генератора для каждого узла.


Параметр type как пользовательская функция

Помимо встроенных символов, type может быть определён как функция, возвращающая путь:

const customSymbol = d3.symbol()
  .type((context, size) => {
    const r = Math.sqrt(size / Math.PI);
    context.moveTo(0, -r);
    context.arc(0, 0, r, 0, 2 * Math.PI);
  });

Функция получает:

  • context — SVG path context
  • size — площадь символа

Это позволяет полностью контролировать геометрию.


Масштабирование и визуальная консистентность

При работе с символами важно учитывать, что:

  • одинаковый size даёт визуально разные размеры для разных форм
  • сложные формы (звезда, wye) выглядят более «плотными»
  • для визуальной консистентности часто требуется ручная нормализация

Пример корректировки:

const symbol = d3.symbol()
  .type(d3.symbolTriangle)
  .size(d => d.size * 1.2);

Стилизация через SVG

После генерации пути внешний вид определяется SVG-атрибутами:

.attr("fill", "#4a90e2")
.attr("stroke", "#333")
.attr("stroke-width", 1)

Дополнительно возможно использование:

  • opacity для плотности отображения
  • class для CSS-стилизации
  • filter для эффектов (тень, размытие)

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

Типичный сценарий — диаграмма рассеяния:

svg.selectAll("path")
  .data(points)
  .enter()
  .append("path")
  .attr("d", d3.symbol().type(d3.symbolCircle).size(80))
  .attr("transform", d => `translate(${x(d.x)},${y(d.y)})`)
  .attr("fill", "tomato");

При добавлении категорий:

const shape = d => {
  if (d.group === "A") return d3.symbolCircle;
  if (d.group === "B") return d3.symbolSquare;
  return d3.symbolTriangle;
};

Производительность при большом количестве элементов

При десятках тысяч точек:

  • генерация d3.symbol() внутри .attr("d") становится затратной
  • предпочтительно создавать генераторы заранее
  • избегается повторное создание функций

Оптимизированный вариант:

const circle = d3.symbol().type(d3.symbolCircle).size(64);

svg.selectAll("path")
  .data(data)
  .enter()
  .append("path")
  .attr("d", circle);

Комбинация с масштабами и осями

Символы часто позиционируются через d3.scaleLinear():

.attr("transform", d =>
  `translate(${xScale(d.x)},${yScale(d.y)})`
)

При изменении масштаба графика символы остаются фиксированного размера в пикселях, что отличает их от геометрически масштабируемых элементов.


Работа с легендами

Символы активно применяются в легендах:

legend.append("path")
  .attr("d", d3.symbol().type(d.type).size(80))
  .attr("transform", "translate(10,10)");

Это позволяет унифицировать визуальный язык графика и легенды.


Интеграция с переходами

Анимация символов:

selection.transition()
  .duration(500)
  .attr("d", d3.symbol().type(d3.symbolStar).size(120));

Также возможно плавное изменение через пересоздание пути при каждом кадре перехода.


Ограничения генератора символов

  • отсутствует встроенная поддержка текста внутри символа
  • фиксированная ориентация (без поворота внутри генератора)
  • ограниченный набор базовых форм
  • сложные кастомные формы требуют ручной реализации

Несмотря на это, d3.symbol() остаётся универсальным инструментом для компактного кодирования категориальных данных в SVG-визуализациях.