Создание кастомного типа графика

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

В основе кастомного типа лежат три ключевых компонента:

1. Dataset Controller (контроллер набора данных) Определяет поведение графика:

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

2. Elements (элементы отрисовки) Примитивы, которые реально рисуются на canvas:

  • линии
  • точки
  • прямоугольники
  • кастомные фигуры

3. Chart Type Registration (регистрация типа) Связывает контроллер с именем нового типа графика.

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

Регистрация нового типа графика

В современных версиях используется регистрация через Chart.register() и расширение Chart.DatasetController.

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

import { Chart } from 'chart.js';

class CustomController extends Chart.DatasetController {
  draw() {
    // логика отрисовки
  }
}

CustomController.id = 'customLine';
CustomController.defaults = {
  datasetElementType: 'line',
  dataElementType: 'point'
};

Chart.register(CustomController);

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

new Chart(ctx, {
  type: 'customLine',
  data: {
    datasets: [{
      data: [10, 20, 30]
    }]
  }
});

Наследование DatasetController

DatasetController — базовый класс, определяющий жизненный цикл набора данных.

Ключевые методы, которые переопределяются:

initialize

Вызывается при создании графика:

initialize() {
  super.initialize();
  this._cachedMeta.customState = {};
}

Используется для:

  • создания внутренних структур
  • кэширования вычислений
  • подготовки данных

update

Отвечает за пересчёт координат:

update(mode) {
  const meta = this.getMeta();
  const data = this.getDataset().data;

  meta.data = data.map((value, index) => {
    return this.createElement(index, mode);
  });

  this.updateElements(meta.data, 0, meta.data.length, mode);
}

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

draw

Отвечает за отрисовку:

draw() {
  const meta = this.getMeta();

  meta.data.forEach(element => {
    element.draw(this.chart.ctx);
  });
}

Создание кастомных элементов

Элементы определяют геометрию и визуальное поведение.

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

import { Element } from 'chart.js';

class CustomPoint extends Element {
  draw(ctx) {
    const { x, y, radius } = this;

    ctx.save();
    ctx.beginPath();
    ctx.arc(x, y, radius || 5, 0, Math.PI * 2);
    ctx.fillStyle = this.options.backgroundColor;
    ctx.fill();
    ctx.restore();
  }

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

CustomPoint.id = 'customPoint';

Элемент обязан реализовать:

  • draw(ctx)
  • методы взаимодействия (опционально): inRange, getCenterPoint, tooltipPosition

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

Контроллер создаёт элементы через createElement:

createElement(index, mode) {
  const element = new CustomPoint();

  const value = this.getDataset().data[index];
  const meta = this.getMeta();

  element.x = this.calculateX(index);
  element.y = this.calculateY(value);

  element.options = this.resolveDataElementOptions(index, mode);

  return element;
}

Методы расчёта координат обычно зависят от шкал:

calculateX(index) {
  const xScale = this.getScaleForId(this.getDataset().xAxisID);
  return xScale.getPixelForValue(index);
}

calculateY(value) {
  const yScale = this.getScaleForId(this.getDataset().yAxisID);
  return yScale.getPixelForValue(value);
}

Полный жизненный цикл кастомного графика

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

  1. Инициализация контроллера
  2. Вызов initialize()
  3. Парсинг dataset
  4. Вызов update()
  5. Создание элементов
  6. Расчёт координат
  7. draw()
  8. Отображение на canvas

При изменении данных:

  • пересоздание или переиспользование элементов
  • повторный вызов update(mode)
  • анимация переходов между состояниями

Пример: кастомный график типа “ступенчатая тепловая линия”

Задача: создать график, где:

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

Элемент линии

class StepLineElement extends Element {
  draw(ctx) {
    const { points, color } = this;

    ctx.save();
    ctx.strokeStyle = color;
    ctx.lineWidth = 2;

    ctx.beginPath();

    points.forEach((p, i) => {
      if (i === 0) {
        ctx.moveTo(p.x, p.y);
      } else {
        const prev = points[i - 1];
        ctx.lineTo(p.x, prev.y);
        ctx.lineTo(p.x, p.y);
      }
    });

    ctx.stroke();
    ctx.restore();
  }
}

Контроллер

class StepHeatController extends Chart.DatasetController {
  update() {
    const meta = this.getMeta();
    const data = this.getDataset().data;

    const points = data.map((value, i) => ({
      x: this.calculateX(i),
      y: this.calculateY(value)
    }));

    meta.dataset = new StepLineElement();
    meta.dataset.points = points;
    meta.dataset.color = this.resolveDatasetColor();
  }

  draw() {
    this.getMeta().dataset.draw(this.chart.ctx);
  }
}

Регистрация

StepHeatController.id = 'stepHeat';
Chart.register(StepHeatController);

Работа с масштабами и координатами

Любой кастомный тип обязан учитывать систему шкал.

Типовые операции:

Получение шкалы:

const scale = this.getScaleForId('y');

Преобразование значений:

scale.getPixelForValue(value);
scale.getValueForPixel(pixel);

Интерполяция: используется при анимации между состояниями данных

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

Анимация в контроллере управляется через контекст состояния:

updateElements(elements, start, count, mode) {
  for (let i = start; i < start + count; i++) {
    const element = elements[i];
    element.x = this.calculateX(i);
    element.y = this.calculateY(this.getDataset().data[i]);

    if (mode === 'active') {
      element.transition(1);
    }
  }
}

Метод transition обычно реализуется вручную:

transition(progress) {
  this._progress = progress;
}

Интерактивность кастомных графиков

Интерактивность обеспечивается методами элементов:

  • inRange() — проверка попадания курсора
  • getCenterPoint() — позиция для tooltip
  • tooltipPosition() — кастомное позиционирование подсказки

Пример:

inRange(mouseX, mouseY) {
  return Math.abs(mouseX - this.x) < 10;
}

Расширение существующих типов

Кастомный тип не обязан быть полностью новым. Возможна модификация поведения:

class ExtendedLineController extends Chart.controllers.line {
  draw() {
    super.draw();

    // дополнительная визуализация
    const ctx = this.chart.ctx;
    ctx.fillText('custom overlay', 10, 10);
  }
}

Это позволяет переиспользовать всю инфраструктуру line-графика.

Структура production-уровня кастомного типа

В сложных реализациях используется разделение:

  • /controllers
  • /elements
  • /scales
  • /plugins

Пример организации:

controllers/
  CustomController.js
elements/
  CustomPoint.js
  CustomLine.js

Регистрация централизуется:

export function registerCustomChart() {
  Chart.register(CustomController, CustomPoint);
}

Особенности отладки кастомных графиков

Ключевые источники проблем:

  • неверные координаты шкал
  • отсутствие синхронизации update/draw
  • некорректная работа meta.data
  • утечка элементов при обновлении dataset

Инструментальная отладка:

console.log(this.getMeta());
console.log(this.getDataset());

Часто полезно визуализировать промежуточные точки:

ctx.fillRect(x, y, 2, 2);