Пользовательские генераторы путей

Пользовательские генераторы путей в D3.js опираются на единый принцип: преобразование произвольных данных в SVG path-строку или последовательность команд рисования через контекст. Внутри экосистемы d3-shape генератор пути — это функция с расширяемым поведением, которая либо возвращает строку атрибута d, либо выполняет отрисовку через переданный контекст (SVG или Canvas).

Базовая идея генератора пути заключается в разделении двух этапов:

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

В D3 это реализовано через интерфейс, где генератор может работать в двух режимах:

  • строковый режим (SVG) — возвращается строка path;
  • контекстный режим (Canvas или кастомный контекст) — вызываются методы moveTo, lineTo, bezierCurveTo и другие.

Генератор, реализующий такой интерфейс, может быть использован в attr("d", generator) или в context(generator).

Контекстный интерфейс d3-path

Базовый строительный блок пользовательских путей — объект, реализующий интерфейс контекста рисования:

  • moveTo(x, y)
  • lineTo(x, y)
  • quadraticCurveTo(x1, y1, x, y)
  • bezierCurveTo(x1, y1, x2, y2, x, y)
  • closePath()

D3 предоставляет реализацию d3.path(), которая аккумулирует команды и преобразует их в SVG path-строку.

const path = d3.path();

path.moveTo(10, 10);
path.lineTo(100, 10);
path.lineTo(100, 100);
path.closePath();

console.log(path.toString());

Этот механизм позволяет строить собственные генераторы, не зависящие от конкретного представления.

Создание собственного генератора пути

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

function customPath(data) {
  const path = d3.path();

  data.forEach((d, i) => {
    const x = d.x;
    const y = d.y;

    if (i === 0) {
      path.moveTo(x, y);
    } else {
      path.lineTo(x, y);
    }
  });

  return path.toString();
}

Такой подход повторяет структуру d3.line, но полностью контролируется разработчиком.

Контекстные генераторы и универсальность

Более гибкая форма — поддержка внешнего контекста. Генератор становится функцией, принимающей context:

function customGenerator(context) {
  return function(data) {
    let ctx = context || d3.path();

    data.forEach((d, i) => {
      const x = d.x;
      const y = d.y;

      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    });

    if (!context) return ctx.toString();
  };
}

Такой паттерн позволяет использовать генератор и в SVG, и в Canvas без изменений логики.

Геометрические преобразования внутри генераторов

Пользовательские генераторы часто включают преобразование координат. Например, полярная система координат:

function polarGenerator(radiusAccessor, angleAccessor) {
  return function(data) {
    const path = d3.path();

    data.forEach((d, i) => {
      const r = radiusAccessor(d);
      const a = angleAccessor(d);

      const x = r * Math.cos(a);
      const y = r * Math.sin(a);

      if (i === 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    });

    return path.toString();
  };
}

Здесь генератор не просто соединяет точки, а преобразует пространство данных.

Сплайны и сглаживание кривых

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

Простейшая реализация квадратичного сглаживания:

function smoothPath(data) {
  const path = d3.path();

  for (let i = 0; i < data.length - 1; i++) {
    const p0 = data[i];
    const p1 = data[i + 1];

    const mx = (p0.x + p1.x) / 2;
    const my = (p0.y + p1.y) / 2;

    if (i === 0) {
      path.moveTo(p0.x, p0.y);
    }

    path.quadraticCurveTo(p0.x, p0.y, mx, my);
  }

  return path.toString();
}

Такой подход заменяет стандартные интерполяции d3.curve*, предоставляя полный контроль над формой линии.

Интеграция с d3-line как основа расширения

Внутренне d3.line() реализует тот же принцип: генератор с доступом к:

  • x(d)
  • y(d)
  • defined(d)
  • curve

Пользовательский генератор может имитировать этот интерфейс:

function lineGenerator() {
  let xAccessor = d => d.x;
  let yAccessor = d => d.y;

  function line(data) {
    const path = d3.path();

    data.forEach((d, i) => {
      const x = xAccessor(d);
      const y = yAccessor(d);

      if (i === 0) path.moveTo(x, y);
      else path.lineTo(x, y);
    });

    return path.toString();
  }

  line.x = fn => (xAccessor = fn, line);
  line.y = fn => (yAccessor = fn, line);

  return line;
}

Этот паттерн делает генератор конфигурируемым, повторяя архитектуру D3.

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

Генераторы путей часто используются для визуализации функций:

function functionPath(f, xStart, xEnd, step) {
  return function() {
    const path = d3.path();

    let first = true;

    for (let x = xStart; x <= xEnd; x += step) {
      const y = f(x);

      if (first) {
        path.moveTo(x, y);
        first = false;
      } else {
        path.lineTo(x, y);
      }
    }

    return path.toString();
  };
}

Такая реализация позволяет строить графики без использования d3.line.

Генераторы сложных форм

Пользовательские генераторы часто применяются для:

  • спиралей;
  • L-систем;
  • диаграмм потоков;
  • геометрических паттернов.

Пример спирали:

function spiral(turns, step) {
  return function() {
    const path = d3.path();

    let angle = 0;
    let radius = 0;

    for (let i = 0; i < turns * 100; i++) {
      angle += 0.1;
      radius += step;

      const x = radius * Math.cos(angle);
      const y = radius * Math.sin(angle);

      if (i === 0) path.moveTo(x, y);
      else path.lineTo(x, y);
    }

    return path.toString();
  };
}

Комбинирование нескольких сегментов пути

Сложные генераторы часто состоят из нескольких независимых сегментов:

  • линия;
  • дуга;
  • кривая Безье.
function compositePath(parts) {
  const path = d3.path();

  parts.forEach(part => {
    if (part.type === "line") {
      path.lineTo(part.x, part.y);
    }

    if (part.type === "move") {
      path.moveTo(part.x, part.y);
    }

    if (part.type === "curve") {
      path.bezierCurveTo(
        part.x1, part.y1,
        part.x2, part.y2,
        part.x, part.y
      );
    }
  });

  return path.toString();
}

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

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

Генераторы путей часто используются в больших визуализациях, где важно:

  • минимизировать аллокации d3.path;
  • переиспользовать функции доступа;
  • кэшировать вычисления координат.

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

function reusableLine() {
  const path = d3.path();

  let x, y;

  function line(data) {
    path.clear?.();

    for (let i = 0; i < data.length; i++) {
      x = data[i].x;
      y = data[i].y;

      if (i === 0) path.moveTo(x, y);
      else path.lineTo(x, y);
    }

    return path.toString();
  }

  return line;
}

Роль пользовательских генераторов в архитектуре визуализаций

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

  • заменять стандартные линии и области;
  • внедрять произвольные математические модели;
  • создавать нестандартные координатные системы;
  • комбинировать SVG и Canvas без изменения логики.

Эта модель делает D3 не просто библиотекой визуализации, а системой композиции геометрических преобразований.