Временные ряды данных

Deck.gl предоставляет высокопроизводительную архитектуру для работы с пространственными и временными данными, где ключевым фактором становится возможность рендеринга больших массивов точек и линий на GPU без потери интерактивности. Временные ряды в таком контексте рассматриваются не как статические наборы значений, а как непрерывные или дискретные изменения состояния объектов во времени, требующие эффективного представления, фильтрации и анимации.

Представление временных рядов в данных

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

  • последовательность измерений (timestamp → value)
  • набор траекторий (time → coordinates)
  • агрегированные временные интервалы (bucketed data)

Типичная форма данных для Deck.gl:

const data = [
  {
    id: "sensor-1",
    values: [
      { t: 1710000000000, v: 12.3 },
      { t: 1710003600000, v: 15.1 },
      { t: 1710007200000, v: 13.8 }
    ]
  }
];

При работе с WebGL критически важно избегать избыточной вложенности в рендер-цикле. Поэтому данные часто предварительно нормализуются:

const flatData = [
  { id: "sensor-1", t: 1710000000000, v: 12.3 },
  { id: "sensor-1", t: 1710003600000, v: 15.1 },
  { id: "sensor-1", t: 1710007200000, v: 13.8 }
];

Такой формат лучше подходит для GPU-ориентированных слоёв.

Линейные временные ряды и LineLayer

Основной инструмент отображения временных рядов значений — LineLayer. Он соединяет точки в порядке времени, формируя непрерывную кривую.

Базовая конфигурация:

import { LineLayer } from '@deck.gl/layers';

const layer = new LineLayer({
  id: 'time-series-line',
  data: flatData,
  getSourcePosition: d => [d.t, d.v],
  getTargetPosition: (d, {index, data}) => {
    const next = data[index + 1];
    return next ? [next.t, next.v] : [d.t, d.v];
  },
  getColor: [0, 140, 255],
  widthMinPixels: 2
});

Временная ось часто отображается как X-координата, однако Deck.gl не ограничивает семантику осей: время может быть закодировано в любой компоненте пространства.

Работа с непрерывностью и разрывами

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

Подходы:

  • разбиение линий на сегменты
  • использование null-разрывов
  • фильтрация по допустимому интервалу

Пример сегментации:

function segmentByGap(data, maxGap) {
  const segments = [];
  let current = [];

  for (let i = 0; i < data.length; i++) {
    const prev = data[i - 1];
    if (prev && data[i].t - prev.t > maxGap) {
      segments.push(current);
      current = [];
    }
    current.push(data[i]);
  }

  if (current.length) segments.push(current);
  return segments;
}

Каждый сегмент затем передаётся как отдельный набор в слой.

Анимация временных рядов

Ключевая особенность временной визуализации — проигрывание данных во времени. В Deck.gl это реализуется через обновление фильтров или uniform-параметров.

Простейшая схема:

const layer = new LineLayer({
  id: 'animated-series',
  data: flatData,
  getSourcePosition: d => [d.t, d.v],
  getTargetPosition: d => [d.t, d.v],
  getColor: d => (d.v > 15 ? [255, 0, 0] : [0, 200, 100]),
  filterRange: [startTime, currentTime],
  extensions: []
});

Обновление текущего времени:

function tick() {
  currentTime += 60000;

  layer.setProps({
    filterRange: [startTime, currentTime]
  });

  requestAnimationFrame(tick);
}

Этот подход позволяет реализовать эффект “протекания времени” без пересоздания геометрии.

GPU-фильтрация и производительность

Временные ряды часто содержат десятки миллионов точек. CPU-фильтрация становится узким местом, поэтому используется GPU-based filtering.

Deck.gl поддерживает фильтрацию через шейдеры, где каждое значение сравнивается с временным окном прямо на GPU.

Ключевые преимущества:

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

Типичный паттерн:

  • timestamp кодируется в attribute
  • uniform передаёт текущий диапазон
  • fragment shader отбрасывает лишние фрагменты

PathLayer для траекторий во времени

Когда временной ряд описывает движение объекта в пространстве, используется PathLayer.

import { PathLayer } from '@deck.gl/layers';

const layer = new PathLayer({
  id: 'trajectory',
  data: trajectories,
  getPath: d => d.path,
  getColor: [30, 200, 0],
  widthMinPixels: 2
});

Формат данных:

const trajectories = [
  {
    id: 1,
    path: [
      [37.78, -122.41, 1710000000000],
      [37.79, -122.42, 1710003600000],
      [37.80, -122.43, 1710007200000]
    ]
  }
];

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

Синхронизация нескольких временных рядов

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

  • глобальный временной диапазон
  • локальные offsets
  • нормализация в [0..1]

Пример нормализации:

function normalizeTime(t, minT, maxT) {
  return (t - minT) / (maxT - minT);
}

Это позволяет использовать единый shader-параметр вместо множества независимых вычислений.

Агрегация временных данных

При больших объёмах данных применяется временная агрегация:

  • минутные бакеты
  • часовые интервалы
  • скользящие окна

Пример агрегации:

function aggregate(data, interval) {
  const map = new Map();

  for (const d of data) {
    const bucket = Math.floor(d.t / interval) * interval;

    if (!map.has(bucket)) map.set(bucket, []);
    map.get(bucket).push(d.v);
  }

  return Array.from(map.entries()).map(([t, values]) => ({
    t,
    v: values.reduce((a, b) => a + b, 0) / values.length
  }));
}

Агрегация часто выполняется до передачи в слой, но в высоконагруженных системах может быть реализована на GPU или в worker-потоках.

ScatterplotLayer для событийных временных рядов

Когда данные представляют собой события во времени, используется ScatterplotLayer.

import { ScatterplotLayer } from '@deck.gl/layers';

const layer = new ScatterplotLayer({
  id: 'events',
  data: events,
  getPosition: d => [d.t, d.value],
  getRadius: 5,
  getFillColor: d => d.critical ? [255, 0, 0] : [0, 120, 255]
});

Такой подход применяется для:

  • логов
  • сенсорных событий
  • финансовых транзакций

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

Временные ряды часто приходят потоково. Deck.gl поддерживает частичные обновления через обновление props без пересоздания слоя.

function appendData(newPoints) {
  data.push(...newPoints);

  layer.setProps({
    data
  });
}

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

  • накопление за интервал времени
  • батчевое обновление
  • ограничение частоты рендера

Оптимизация памяти и структуры атрибутов

GPU-слои требуют компактного представления:

  • Float32Array вместо объектов
  • плоские массивы координат
  • минимизация строковых ключей

Пример подготовки:

const positions = new Float32Array(data.length * 2);

data.forEach((d, i) => {
  positions[i * 2] = d.t;
  positions[i * 2 + 1] = d.v;
});

Это снижает нагрузку на garbage collector и ускоряет передачу в WebGL буферы.

Комбинирование временных и пространственных измерений

Во многих задачах время становится дополнительной осью в пространстве:

  • 2D + time → 3D визуализация
  • time + category → цветовая сегментация
  • time + magnitude → радиус/толщина

Deck.gl позволяет комбинировать эти измерения без изменения базовой архитектуры слоя, используя accessor-функции и shader-параметры.