Динамические визуализации

Динамические визуализации в Mapbox GL JS строятся на принципе реактивного обновления источников данных (sources), слоёв (layers) и выражений (expressions), которые пересчитываются в рендеринг-пайплайне WebGL в реальном времени. В отличие от статических карт, где геометрия и стили фиксируются при инициализации, динамические карты предполагают непрерывное изменение состояния данных без полного перерисовывания сцены.

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

  • источников данных (GeoJSON, vector tiles, raster tiles)
  • визуального представления (layers)
  • вычисляемых свойств (paint/layout expressions)

Это позволяет обновлять только затронутые части сцены, минимизируя нагрузку на GPU.

Обновление данных через GeoJSON sources

Наиболее распространённый механизм динамики — обновление GeoJSON-источника через метод setData. Он позволяет изменять геометрию или свойства объектов без пересоздания слоя.

map.addSource('vehicles', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: []
  }
});

map.addLayer({
  id: 'vehicles-layer',
  type: 'circle',
  source: 'vehicles',
  paint: {
    'circle-radius': 6,
    'circle-color': '#ff3b30'
  }
});

Дальнейшие обновления выполняются точечно:

function updateVehicles(newData) {
  const source = map.getSource('vehicles');
  source.setData({
    type: 'FeatureCollection',
    features: newData
  });
}

Такой подход используется в задачах:

  • трекинга транспорта
  • отображения IoT-устройств
  • визуализации потоков событий

Анимация через requestAnimationFrame

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

function animate(timestamp) {
  const progress = (timestamp % 2000) / 2000;

  const pulseRadius = 5 + Math.sin(progress * Math.PI * 2) * 3;

  map.setPaintProperty('vehicles-layer', 'circle-radius', pulseRadius);

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

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

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

Однако постоянное обновление стилей требует осторожности, так как каждое изменение инициирует перерасчёт слоя.

Динамические выражения (Expressions)

Внутренний язык выражений позволяет переносить вычисления в GPU-пайплайн, избегая JavaScript-циклов.

Пример зависимости цвета от свойства скорости:

paint: {
  'circle-color': [
    'interpolate',
    ['linear'],
    ['get', 'speed'],
    0, '#2ecc71',
    50, '#f1c40f',
    100, '#e74c3c'
  ]
}

Преимущество заключается в том, что обновление данных не требует изменения стиля — достаточно изменить свойства feature.

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

Mapbox GL JS поддерживает transition-параметры для анимации изменений стиля:

map.setPaintProperty('vehicles-layer', 'circle-radius', 10, {
  duration: 800
});

Либо глобально:

paint: {
  'circle-radius': 6,
  'circle-radius-transition': {
    duration: 500,
    delay: 0
  }
}

Это используется для:

  • сглаживания изменения плотности данных
  • визуализации масштабирования событий
  • создания «живой» карты

Обновление геометрии в реальном времени

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

const features = vehicles.map(v => ({
  type: 'Feature',
  geometry: {
    type: 'Point',
    coordinates: [v.lon, v.lat]
  },
  properties: {
    speed: v.speed
  }
}));

map.getSource('vehicles').setData({
  type: 'FeatureCollection',
  features
});

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

Использование фильтров для динамического отображения

Фильтрация слоёв позволяет управлять видимостью объектов без изменения источника данных:

map.setFilter('vehicles-layer', [
  '>=',
  ['get', 'speed'],
  30
]);

Фильтры применяются на уровне рендеринга и эффективно исключают ненужные объекты из сцены.

Кластеризация как форма динамической агрегации

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

map.addSource('points', {
  type: 'geojson',
  data: data,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

Отображение кластеров меняется в зависимости от масштаба:

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'points',
  filter: ['has', 'point_count'],
  paint: {
    'circle-radius': [
      'step',
      ['get', 'point_count'],
      10,
      100, 20,
      1000, 30
    ]
  }
});

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

Потоковые данные и WebSocket интеграция

Динамические карты часто подключаются к потокам данных:

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

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

  map.getSource('vehicles').setData({
    type: 'FeatureCollection',
    features: data.features
  });
};

Такая архитектура применяется в:

  • транспортных системах
  • финансовых визуализациях
  • мониторинге сенсоров

Интерполяция и временные ряды

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

paint: {
  'circle-opacity': [
    'interpolate',
    ['linear'],
    ['get', 'timestamp'],
    1700000000, 0.2,
    1700003600, 1
  ]
}

Это позволяет визуализировать:

  • распространение событий во времени
  • тепловые карты активности
  • эволюцию явлений

Heatmap как динамическая агрегация плотности

Heatmap-слои автоматически перерасчитываются при изменении данных:

map.addLayer({
  id: 'heatmap',
  type: 'heatmap',
  source: 'points',
  paint: {
    'heatmap-intensity': 1,
    'heatmap-radius': 20,
    'heatmap-color': [
      'interpolate',
      ['linear'],
      ['heatmap-density'],
      0, 'rgba(0,0,255,0)',
      0.5, 'rgba(0,255,0,0.5)',
      1, 'rgba(255,0,0,1)'
    ]
  }
});

Такие слои реагируют на масштаб и плотность данных без ручного пересчёта.

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

Динамические визуализации часто требуют двусторонней синхронизации:

map.on('move', () => {
  const center = map.getCenter();
  updateExternalState(center);
});

Это позволяет:

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

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

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

  • агрегирование данных перед setData
  • использование requestAnimationFrame вместо таймеров
  • перенос вычислений в expressions
  • минимизация количества слоёв

Mapbox проектировала рендеринг таким образом, чтобы обновления данных проходили через оптимизированный WebGL-пайплайн, где пересчитываются только изменённые буферы, а не вся сцена целиком.

Комбинирование источников для сложных сцен

Сложные динамические визуализации часто используют несколько источников одновременно:

map.addSource('traffic', { type: 'vector', url: 'mapbox://traffic' });
map.addSource('devices', { type: 'geojson', data: geojsonData });
map.addSource('weather', { type: 'raster', url: 'mapbox://weather' });

Комбинация слоёв позволяет создавать:

  • мультимодальные карты
  • аналитические панели
  • ситуационные центры

Событийная модель и реактивное обновление

Mapbox GL JS предоставляет события для отслеживания изменений сцены:

map.on('data', (e) => {
  if (e.sourceId === 'vehicles') {
    console.log('Данные обновлены');
  }
});

Эта модель используется для построения реактивных интерфейсов, где карта является источником состояния, а не просто визуализацией.