В Chart.js график строится как композиция нескольких слоёв абстракций: контроллеров (controllers), элементов (elements), скейлов (scales) и плагинов (plugins). Элементы представляют собой базовые графические примитивы, из которых собирается визуальное представление данных: линии, точки, прямоугольники, дуги и пользовательские формы.
Каждый элемент в Chart.js инкапсулирует:
Кастомизация элементов позволяет выходить за рамки стандартных визуализаций и внедрять собственные формы отображения данных, не переписывая всю архитектуру графика.
Все элементы наследуются от базового класса 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), которые выполняют трансформации.
Типичная схема вычислений:
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;
}
Этот метод позволяет:
Дополнительно можно расширять поведение через обработку событий 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;
}
}
Такой подход используется для:
Каждый элемент получает CanvasRenderingContext2D, через
который выполняется отрисовка. Важно соблюдать:
ctx.save()ctx.restore()draw()Оптимизация критична при больших наборах данных, так как элементы отрисовываются на каждом кадре анимации.
При создании кастомных элементов необходимо учитывать:
Рекомендации архитектурного уровня:
draw()Иногда нет необходимости создавать элемент с нуля. Более практичный подход — наследование стандартных элементов 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();
}
}
Такой подход позволяет:
Каждый элемент напрямую связан с конкретной точкой данных dataset. Chart.js поддерживает синхронизацию через индексы:
При кастомных реализациях важно не нарушать эту связь, иначе возникают расхождения между моделью и визуализацией.
Элементы могут создаваться на лету при изменении данных:
addData(chart, label, value) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(value);
chart.update();
}
При обновлении Chart.js пересчитывает элементы через контроллер и
заново вызывает их update().
Пользовательские элементы часто применяются в случаях:
В таких сценариях элемент становится не просто графическим примитивом, а полноценной единицей бизнес-логики визуализации внутри графика.