Кастомные элементы (Element)

В Chart.js график строится как композиция нескольких слоёв абстракций: контроллеров (controllers), элементов (elements), скейлов (scales) и плагинов (plugins). Элементы представляют собой базовые графические примитивы, из которых собирается визуальное представление данных: линии, точки, прямоугольники, дуги и пользовательские формы.

Каждый элемент в Chart.js инкапсулирует:

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

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


Базовый класс Element

Все элементы наследуются от базового класса Element. Он определяет контракт, который должен соблюдать любой визуальный объект внутри графика.

Ключевые методы жизненного цикла:

  • draw() — отрисовка элемента
  • update() — обновление состояния
  • inRange() — проверка попадания курсора
  • getProps() — вычисление свойств анимации

Простейшая структура пользовательского элемента:

import { Element } from 'chart.js';

class CustomElement extends Element {
  constructor(cfg) {
    super();
    this.x = cfg.x;
    this.y = cfg.y;
  }

  draw(ctx) {
    ctx.save();
    ctx.fillStyle = 'blue';
    ctx.beginPath();
    ctx.arc(this.x, this.y, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  update(props) {
    this.x = props.x;
    this.y = props.y;
  }
}

Такой элемент уже может быть встроен в систему рендеринга Chart.js при корректной регистрации.


Регистрация пользовательского элемента

Chart.js использует реестр компонентов. Любой новый элемент должен быть зарегистрирован перед использованием.

import { Chart, Element } from 'chart.js';

class BubbleElement extends Element {
  draw(ctx) {
    ctx.save();
    ctx.fillStyle = this.options.backgroundColor || 'red';
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  update(props) {
    this.x = props.x;
    this.y = props.y;
    this.radius = props.radius;
  }
}

Chart.register(BubbleElement);

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


Интеграция с контроллерами

Элементы сами по себе не создают график. Они управляются контроллерами (например, LineController, BarController). Для полной кастомизации часто создаётся собственный контроллер, который генерирует экземпляры элементов.

Пример связи контроллера и элемента:

import { DatasetController, Chart } from 'chart.js';

class CustomController extends DatasetController {
  createElements() {
    this._elements = this.data.map(() => new BubbleElement());
  }

  updateElements(elements, start, count, mode) {
    for (let i = start; i < start + count; i++) {
      const element = elements[i];
      const value = this.getParsed(i);

      element.update({
        x: this.xScale.getPixelForValue(value.x),
        y: this.yScale.getPixelForValue(value.y),
        radius: value.r
      });
    }
  }
}

Chart.register(CustomController);

Контроллер становится источником истины, а элементы — визуальным представлением данных.


Геометрия и расчёты координат

Одной из ключевых задач кастомного элемента является преобразование данных в экранные координаты. Chart.js предоставляет доступ к шкалам (scales), которые выполняют трансформации.

Типичная схема вычислений:

  • получение сырых данных из dataset
  • преобразование через scale
  • передача результата в элемент
const px = chart.scales.x.getPixelForValue(dataPoint.x);
const py = chart.scales.y.getPixelForValue(dataPoint.y);

При кастомных элементах важно не хранить только «логические» значения, а поддерживать синхронизацию между:

  • исходными данными
  • пиксельными координатами
  • промежуточным состоянием анимации

Анимация пользовательских элементов

Chart.js использует систему анимации на основе промежуточных состояний (tweening). Элемент должен поддерживать плавное обновление через getProps и update.

Пример анимируемого элемента:

class AnimatedPoint extends Element {
  draw(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
  }

  update(props) {
    this.x = props.x;
    this.y = props.y;
    this.radius = props.radius;
  }

  getProps(props) {
    return {
      x: props.x,
      y: props.y,
      radius: props.radius
    };
  }
}

Система анимации Chart.js автоматически интерполирует значения между состояниями.


Обработка событий и интерактивность

Элементы участвуют в системе событий: hover, click, tooltip detection. Для этого реализуется метод inRange.

inRange(mouseX, mouseY) {
  const dx = mouseX - this.x;
  const dy = mouseY - this.y;
  return dx * dx + dy * dy <= this.radius * this.radius;
}

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

  • определять активный элемент под курсором
  • связывать tooltip с кастомной геометрией
  • реализовывать собственные интерактивные зоны

Дополнительно можно расширять поведение через обработку событий canvas.


Кастомные формы и сложная геометрия

Элементы не ограничиваются кругами и прямоугольниками. Возможна реализация любых форм: многоугольников, сплайнов, векторных фигур.

Пример элемента с многоугольником:

class PolygonElement extends Element {
  draw(ctx) {
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(this.points[0].x, this.points[0].y);

    for (let i = 1; i < this.points.length; i++) {
      ctx.lineTo(this.points[i].x, this.points[i].y);
    }

    ctx.closePath();
    ctx.fillStyle = this.options.color;
    ctx.fill();
    ctx.restore();
  }

  update(props) {
    this.points = props.points;
  }
}

Такой подход используется для:

  • radar-диаграмм
  • пользовательских визуализаций сетей
  • географических отображений

Работа с контекстом Canvas

Каждый элемент получает CanvasRenderingContext2D, через который выполняется отрисовка. Важно соблюдать:

  • сохранение состояния ctx.save()
  • восстановление ctx.restore()
  • минимизацию операций внутри draw()

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


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

При создании кастомных элементов необходимо учитывать:

  • количество экземпляров в dataset
  • частоту обновлений
  • сложность отрисовки

Рекомендации архитектурного уровня:

  • избегать аллокаций в draw()
  • кэшировать вычисленные значения
  • использовать примитивные формы вместо сложных путей
  • минимизировать доступ к объектам Chart.js внутри цикла рендеринга

Расширение стандартных элементов

Иногда нет необходимости создавать элемент с нуля. Более практичный подход — наследование стандартных элементов Chart.js.

import { PointElement } from 'chart.js';

class HighlightPoint extends PointElement {
  draw(ctx) {
    super.draw(ctx);

    ctx.save();
    ctx.strokeStyle = 'yellow';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.options.radius + 4, 0, Math.PI * 2);
    ctx.stroke();
    ctx.restore();
  }
}

Такой подход позволяет:

  • сохранить совместимость с контроллерами
  • переиспользовать логику Chart.js
  • расширять визуализацию без переписывания базового поведения

Связь с системой датасетов

Каждый элемент напрямую связан с конкретной точкой данных dataset. Chart.js поддерживает синхронизацию через индексы:

  • индекс элемента соответствует индексу данных
  • обновление dataset приводит к пересозданию или обновлению элементов

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


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

Элементы могут создаваться на лету при изменении данных:

addData(chart, label, value) {
  chart.data.labels.push(label);
  chart.data.datasets[0].data.push(value);

  chart.update();
}

При обновлении Chart.js пересчитывает элементы через контроллер и заново вызывает их update().


Сложные сценарии кастомизации

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

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

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