Ограничение количества точек (скользящее окно)

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


Принцип работы скользящего окна

Скользящее окно представляет собой ограниченный буфер данных, который постоянно обновляется:

  • добавляется новое значение в конец массива;
  • если размер превышает заданный лимит — удаляется первое значение;
  • визуально отображается только актуальный диапазон данных.

Фактически это реализация очереди фиксированной длины, адаптированной под обновление графика.


Базовая структура данных 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').

Использование временного окна (time window)

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

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');
}

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

При большом количестве точек применяются дополнительные техники:

  • уменьшение плотности точек (downsampling);
  • отключение рендеринга точек (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().

Особенности работы с time scale

При использовании временной шкалы Chart.js (type: 'time') скользящее окно упрощается, так как ось X управляется автоматически:

options: {
  scales: {
    x: {
      type: 'time',
      time: {
        unit: 'second'
      }
    }
  }
}

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


Типовые ошибки при реализации окна

  • удаление только labels без синхронизации datasets;
  • отсутствие контроля длины при асинхронных обновлениях;
  • использование 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');
}

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