Компонент Line с динамическими данными и стримингом

Компонент ResponsiveLine из библиотеки @nivo/line ожидает строго структурированные данные. Базовая модель представляет собой массив серий, где каждая серия содержит идентификатор и набор точек:

const data = [
  {
    id: "temperature",
    color: "hsl(210, 70%, 50%)",
    data: [
      { x: "10:00", y: 21 },
      { x: "10:05", y: 22 },
      { x: "10:10", y: 23 }
    ]
  }
];

Ключевые особенности формата:

  • id — уникальный идентификатор линии
  • data — массив точек
  • x и y — координаты (x может быть строкой, числом или датой)

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


Базовая конфигурация Line-компонента

Компонент ResponsiveLine строится вокруг набора параметров, определяющих визуализацию:

import { ResponsiveLine } from "@nivo/line";

<ResponsiveLine
  data={data}
  margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
  xScale={{ type: "point" }}
  yScale={{ type: "linear", min: "auto", max: "auto" }}
  axisBottom={{
    tickSize: 5,
    tickPadding: 5,
    tickRotation: 0
  }}
  axisLeft={{
    tickSize: 5,
    tickPadding: 5,
    tickRotation: 0
  }}
  enablePoints={true}
  pointSize={6}
  useMesh={true}
/>

Основные элементы:

  • xScale определяет тип шкалы (point, linear, time)
  • yScale отвечает за числовую ось
  • margin задаёт отступы
  • axisBottom / axisLeft управляют осями
  • useMesh улучшает взаимодействие с tooltip

Подготовка данных для динамического обновления

При работе с динамикой важно проектировать структуру данных так, чтобы добавление новых точек не требовало перерасчёта всей серии.

Типичная схема хранения:

const [data, setData] = useState([
  {
    id: "stream",
    data: []
  }
]);

Добавление новой точки выполняется через создание нового массива:

const addPoint = (point) => {
  setData((prev) => {
    const series = prev[0];

    const updatedSeries = {
      ...series,
      data: [
        ...series.data,
        point
      ]
    };

    return [updatedSeries];
  });
};

Работа с потоковыми данными (streaming)

Подключение через WebSocket

Для получения данных в реальном времени используется WebSocket или аналогичный поток:

useEffect(() => {
  const ws = new WebSocket("wss://example-stream.com");

  ws.onmess age = (event) => {
    const message = JSON.parse(event.data);

    addPoint({
      x: message.time,
      y: message.value
    });
  };

  return () => ws.close();
}, []);

Каждое сообщение преобразуется в точку графика и добавляется в серию.


Ограничение длины массива (windowing)

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

const MAX_POINTS = 50;

const addPoint = (point) => {
  setData((prev) => {
    const series = prev[0];

    const sliced = series.data.slice(-MAX_POINTS + 1);

    return [
      {
        ...series,
        data: [...sliced, point]
      }
    ];
  });
};

Этот подход сохраняет только последние значения, формируя «скользящее окно».


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

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

<ResponsiveLine
  data={data}
  xScale={{
    type: "time",
    format: "%H:%M:%S",
    precision: "second"
  }}
  xFormat="time:%H:%M:%S"
/>

Форматирование времени требует согласованности входных данных:

{
  x: new Date().toISOString(),
  y: 42
}

Анимации при обновлении данных

Nivo поддерживает анимации переходов между состояниями графика:

<ResponsiveLine
  data={data}
  animate={true}
  motionConfig="molasses"
/>

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

  • слишком частые обновления могут перегружать анимацию
  • оптимально группировать обновления или снижать частоту

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

При потоковой подаче данных ключевым фактором становится контроль перерисовок React-компонента.

Используются подходы:

useCallback для обновлений

const addPoint = useCallback((point) => {
  setData((prev) => {
    const series = prev[0];

    return [
      {
        ...series,
        data: [...series.data, point]
      }
    ];
  });
}, []);

useMemo для стабилизации данных

const chartData = useMemo(() => data, [data]);

разделение логики получения и отображения

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


Поведение tooltip при стриминге

При динамическом обновлении важно поведение tooltip:

<ResponsiveLine
  useMesh={true}
  enableSlices="x"
  crosshairType="cross"
/>

Особенности:

  • useMesh улучшает точность наведения
  • enableSlices синхронизирует данные по оси X
  • crosshair помогает визуально отслеживать поток

Обновление нескольких серий в реальном времени

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

const [data, setData] = useState([
  { id: "cpu", data: [] },
  { id: "memory", data: [] }
]);

Обновление конкретной серии:

const updateSeries = (id, point) => {
  setData((prev) =>
    prev.map((series) => {
      if (series.id !== id) return series;

      return {
        ...series,
        data: [...series.data, point]
      };
    })
  );
};

Синхронизация потоков

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

  • все серии используют одинаковый формат x
  • обновления приходят с одинаковым интервалом
  • при необходимости применяется буферизация

Пример буфера:

const bufferRef = useRef([]);

const flushBuffer = () => {
  setData((prev) => {
    const updated = [...prev];

    bufferRef.current.forEach(({ id, point }) => {
      const series = updated.find(s => s.id === id);
      series.data.push(point);
    });

    bufferRef.current = [];
    return updated;
  });
};

Управление производительностью при высокой частоте обновлений

При частоте обновления выше 10–20 сообщений в секунду возникают ограничения рендеринга React.

Используются стратегии:

throttle обновлений

let lastUpdate = 0;

const handleMessage = (point) => {
  const now = Date.now();

  if (now - lastUpdate > 100) {
    lastUpdate = now;
    addPoint(point);
  }
};

requestAnimationFrame синхронизация

const queueRef = useRef([]);

const tick = () => {
  if (queueRef.current.length > 0) {
    setData((prev) => {
      const series = prev[0];

      return [
        {
          ...series,
          data: [...series.data, ...queueRef.current]
        }
      ];
    });

    queueRef.current = [];
  }

  requestAnimationFrame(tick);
};

useEffect(() => {
  requestAnimationFrame(tick);
}, []);

Стилизация линии и точек

Компонент позволяет детально управлять внешним видом:

<ResponsiveLine
  curve="monotoneX"
  lineWidth={2}
  enablePoints={true}
  pointSize={8}
  pointColor={{ theme: "background" }}
  pointBorderWidth={2}
  pointBorderColor={{ from: "serieColor" }}
/>

Для стриминга часто отключают точки:

  • уменьшение нагрузки
  • визуальная чистота при высокой плотности данных

Поведение при переполнении данных

При очень длинных потоках важно контролировать:

  • количество точек
  • масштабирование оси X
  • пересчёт min/max Y

Пример адаптивного масштаба:

yScale={{
  type: "linear",
  min: "auto",
  max: "auto",
  stacked: false
}}

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


Использование time-series в реальных сценариях

Line-компонент в потоковом режиме часто применяется для:

  • мониторинга серверов (CPU, RAM)
  • финансовых котировок
  • телеметрии устройств
  • аналитики пользовательских событий

Во всех случаях ключевым становится баланс между:

  • частотой обновлений
  • объёмом хранимых данных
  • плавностью отрисовки

Архитектурный паттерн для стриминга

Типовая архитектура включает три слоя:

  1. Data ingestion layer

    • WebSocket / SSE / polling
  2. Processing layer

    • нормализация данных
    • буферизация
    • фильтрация
  3. Visualization layer

    • ResponsiveLine
    • управление состоянием React

Разделение слоёв снижает связанность и упрощает масштабирование потоковой системы.