При работе с динамическими графиками в Chart.js одной из ключевых задач становится контроль объёма данных, отображаемых на экране. При бесконечном накоплении точек производительность падает, визуальная читаемость ухудшается, а взаимодействие с графиком становится менее отзывчивым. Решением выступает механизм скользящего окна, при котором на графике отображается фиксированное число последних значений, а устаревшие данные автоматически удаляются.
Скользящее окно представляет собой ограниченный буфер данных, который постоянно обновляется:
Фактически это реализация очереди фиксированной длины, адаптированной под обновление графика.
Chart.js оперирует двумя основными массивами:
labels — подписи по оси X;datasets[].data — значения по оси Y.Простейшая конфигурация:
const config = {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Сигнал',
data: [],
borderColor: 'blue',
tension: 0.3
}]
},
options: {
animation: false,
responsive: true
}
};
Основной подход заключается в контроле длины массивов вручную.
const MAX_POINTS = 50;
function addData(chart, label, value) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(value);
if (chart.data.labels.length > MAX_POINTS) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none');
}
Ключевые моменты:
shift() удаляет первый элемент массива;push() добавляет новый элемент в конец;'none').В задачах телеметрии и мониторинга фиксированное количество точек часто заменяется временным интервалом. В этом случае данные фильтруются по времени.
const WINDOW_MS = 10000; // 10 секунд
const dataBuffer = [];
function addPoint(timestamp, value) {
dataBuffer.push({ timestamp, value });
const cutoff = timestamp - WINDOW_MS;
while (dataBuffer.length && dataBuffer[0].timestamp < cutoff) {
dataBuffer.shift();
}
}
Далее данные преобразуются для Chart.js:
chart.data.labels = dataBuffer.map(p => p.timestamp);
chart.data.datasets[0].data = dataBuffer.map(p => p.value);
chart.update('none');
При частых обновлениях (например, каждые 50–100 мс) критично минимизировать перерасчёты и перерисовки.
Используются следующие приёмы:
chart.options.animation = false;
Chart.js позволяет обновлять только набор данных:
chart.update('none');
let lastUpdate = 0;
function throttledUpdate(chart, label, value) {
const now = Date.now();
if (now - lastUpdate < 50) return;
lastUpdate = now;
addData(chart, label, value);
}
При высокочастотных потоках данных (сенсоры, аудио, финансы) массивы могут быстро расти даже при скользящем окне. Для таких случаев используется буферизация.
const buffer = [];
function ingest(value) {
buffer.push(value);
if (buffer.length >= 10) {
flushBuffer();
}
}
function flushBuffer() {
const chartData = buffer.splice(0, buffer.length);
chartData.forEach(v => {
addData(chart, v.label, v.value);
});
}
Такой подход снижает количество перерисовок.
Более производительное решение — кольцевой буфер фиксированного размера.
class RingBuffer {
constructor(size) {
this.size = size;
this.data = new Array(size);
this.index = 0;
this.count = 0;
}
push(value) {
this.data[this.index] = value;
this.index = (this.index + 1) % this.size;
if (this.count < this.size) this.count++;
}
toArray() {
return this.count === this.size
? [...this.data.slice(this.index), ...this.data.slice(0, this.index)]
: this.data.slice(0, this.count);
}
}
Интеграция с Chart.js:
const buffer = new RingBuffer(100);
function updateChart(chart, value) {
buffer.push(value);
const arr = buffer.toArray();
chart.data.datasets[0].data = arr;
chart.update('none');
}
При работе с несколькими линиями требуется синхронное обрезание всех datasets.
const MAX_POINTS = 100;
function addMulti(chart, label, values) {
chart.data.labels.push(label);
values.forEach((val, i) => {
chart.data.datasets[i].data.push(val);
});
if (chart.data.labels.length > MAX_POINTS) {
chart.data.labels.shift();
chart.data.datasets.forEach(ds => ds.data.shift());
}
chart.update('none');
}
При большом количестве точек применяются дополнительные техники:
pointRadius: 0);decimation в Chart.js.Пример настройки:
datasets: [{
data: [],
pointRadius: 0,
borderWidth: 2,
tension: 0.2
}]
В некоторых сценариях применяется гибридный подход:
const MAX_POINTS = 200;
const WINDOW_MS = 15000;
function add(chart, timestamp, value) {
chart.data.labels.push(timestamp);
chart.data.datasets[0].data.push(value);
const cutoff = timestamp - WINDOW_MS;
while (
chart.data.labels.length > MAX_POINTS ||
chart.data.labels[0] < cutoff
) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none');
}
При частоте обновления выше 10–20 раз в секунду основная нагрузка смещается в сторону DOM-рендеринга и Canvas перерисовки. В таких условиях важны:
chart.update().При использовании временной шкалы Chart.js
(type: 'time') скользящее окно упрощается, так как ось X
управляется автоматически:
options: {
scales: {
x: {
type: 'time',
time: {
unit: 'second'
}
}
}
}
Фильтрация данных по времени остаётся основной логикой, а отображение диапазона делегируется библиотеке.
update() без оптимизации режима;Изменение MAX_POINTS во время работы требует пересборки
текущего состояния:
function resizeWindow(chart, newSize) {
while (chart.data.labels.length > newSize) {
chart.data.labels.shift();
chart.data.datasets.forEach(ds => ds.data.shift());
}
chart.update('none');
}
Такая операция часто используется при динамическом изменении масштаба отображения или пользовательских настройках.