Потоковое обновление в реальном времени

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

В основе механизма лежит разделение данных и рендера. Chart.js хранит состояние графика в объекте chart.data, а визуализация формируется через вызов chart.update().

Ключевая особенность потоковой модели заключается в том, что:

  • данные изменяются инкрементально;
  • перерисовка выполняется по требованию;
  • минимизируется полная пересборка графика.

Типичная структура данных для потокового режима:

const data = {
  datasets: [{
    label: 'Сенсор',
    data: []
  }]
};

Добавление новых точек в реальном времени

Основной операцией становится добавление элемента в массив data:

chart.data.datasets[0].data.push({
  x: Date.now(),
  y: Math.random() * 100
});

chart.update('none');

Режим 'none' отключает анимацию, что критично для высокой частоты обновлений.

При высоких нагрузках важно ограничивать размер массива:

const dataset = chart.data.datasets[0];

dataset.data.push({ x: Date.now(), y: value });

if (dataset.data.length > 200) {
  dataset.data.shift();
}

chart.update('none');

Такой подход предотвращает деградацию производительности при длительной работе.

Использование временных шкал

Для потоковых данных наиболее естественно использовать временную ось:

const config = {
  type: 'line',
  data,
  options: {
    scales: {
      x: {
        type: 'time',
        time: {
          unit: 'second'
        }
      }
    }
  }
};

Временная шкала обеспечивает:

  • автоматическую сортировку точек;
  • корректное отображение интервалов;
  • упрощённую агрегацию.

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

WebSocket

WebSocket обеспечивает двусторонний канал передачи данных:

const socket = new WebSocket('wss://example.com/data');

socket.onmess age = (event) => {
  const parsed = JSON.parse(event.data);

  chart.data.datasets[0].data.push({
    x: parsed.timestamp,
    y: parsed.value
  });

  chart.update('none');
};

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

EventSource (Server-Sent Events)

SSE используется для одностороннего потока:

const source = new EventSource('/stream');

source.onmess age = (event) => {
  const dataPoint = JSON.parse(event.data);

  chart.data.datasets[0].data.push(dataPoint);
  chart.update('none');
};

SSE проще в реализации, но ограничен HTTP-моделью.

Таймерный поток (имитация данных)

Используется при тестировании:

setInterval(() => {
  chart.data.datasets[0].data.push({
    x: Date.now(),
    y: Math.sin(Date.now() / 1000)
  });

  chart.update('none');
}, 500);

Контроль частоты обновлений

Частота вызова chart.update() является критическим фактором производительности.

Чрезмерное обновление приводит к:

  • блокировке основного потока;
  • росту CPU load;
  • падению FPS.

Оптимальная стратегия — буферизация:

const buffer = [];

function pushData(point) {
  buffer.push(point);
}

setInterval(() => {
  if (buffer.length === 0) return;

  chart.data.datasets[0].data.push(...buffer);
  buffer.length = 0;

  chart.update('none');
}, 200);

Использование requestAnimationFrame

Для визуально плавных графиков применяется синхронизация с рендерным циклом браузера:

function renderLoop() {
  if (buffer.length > 0) {
    chart.data.datasets[0].data.push(...buffer);
    buffer.length = 0;

    chart.update('none');
  }

  requestAnimationFrame(renderLoop);
}

requestAnimationFrame(renderLoop);

Такой подход согласует обновления с частотой экрана.

Ограничение объёма данных

Долгоживущие графики требуют стратегии удаления старых точек:

FIFO-очистка

const MAX_POINTS = 300;

const dataset = chart.data.datasets[0];

dataset.data.push(newPoint);

while (dataset.data.length > MAX_POINTS) {
  dataset.data.shift();
}

Скользящее окно по времени

const now = Date.now();
const windowSize = 60000;

dataset.data = dataset.data.filter(p => now - p.x < windowSize);

Второй вариант предпочтителен для временных рядов.

Множественные потоки данных

При работе с несколькими источниками структура расширяется:

chart.data.datasets[0].data.push({ x, y: sensorA });
chart.data.datasets[1].data.push({ x, y: sensorB });

chart.update('none');

Важно синхронизировать временные метки, иначе визуальное сравнение теряет смысл.

Адаптация масштабов

Автоматическое масштабирование осей при потоковой передаче может вызывать визуальные скачки.

Для стабилизации применяются ограничения:

options: {
  scales: {
    y: {
      min: 0,
      max: 100
    }
  }
}

Или динамическое вычисление:

const values = dataset.data.map(p => p.y);

chart.options.scales.y.min = Math.min(...values);
chart.options.scales.y.max = Math.max(...values);

Оптимизация производительности

При высокочастотных потоках критичны следующие техники:

Отключение анимации

chart.update('none');

Минимизация структуры данных

Использование примитивных объектов вместо сложных:

// быстрее
{ x: 123, y: 45 }

// медленнее
{ x: new Date(), y: computeValue(), meta: {...} }

Частичный рендеринг

Избегание полного пересчёта datasets:

chart.data.datasets[0].data[chart.data.datasets[0].data.length - 1] = newPoint;
chart.update('none');

Поведение при больших нагрузках

При превышении допустимой частоты обновлений наблюдаются:

  • накопление очереди вызовов update();
  • деградация интерактивности;
  • рост задержек между источником и визуализацией.

Решение заключается в принудительной дедупликации обновлений:

let scheduled = false;

function scheduleUpdate() {
  if (scheduled) return;

  scheduled = true;

  requestAnimationFrame(() => {
    chart.update('none');
    scheduled = false;
  });
}

Завершение жизненного цикла графика

При остановке потоков необходимо освобождать ресурсы:

socket.close();
chart.destroy();

Некорректное завершение приводит к утечкам памяти из-за сохранённых ссылок на DOM и обработчики событий.