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 — базовый класс, определяющий
жизненный цикл набора данных.
Ключевые методы, которые переопределяются:
Вызывается при создании графика:
initialize() {
super.initialize();
this._cachedMeta.customState = {};
}
Используется для:
Отвечает за пересчёт координат:
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() {
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);
}
При создании графика происходит последовательность:
initialize()update()draw()При изменении данных:
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() — позиция для tooltiptooltipPosition() — кастомное позиционирование
подсказкиПример:
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-графика.
В сложных реализациях используется разделение:
/controllers/elements/scales/pluginsПример организации:
controllers/
CustomController.js
elements/
CustomPoint.js
CustomLine.js
Регистрация централизуется:
export function registerCustomChart() {
Chart.register(CustomController, CustomPoint);
}
Ключевые источники проблем:
Инструментальная отладка:
console.log(this.getMeta());
console.log(this.getDataset());
Часто полезно визуализировать промежуточные точки:
ctx.fillRect(x, y, 2, 2);