Анимация тепловой карты

Визуализация плотности данных в браузерных картах в Google Maps JavaScript API реализуется через слой тепловой карты, основанный на классе HeatmapLayer. Этот механизм предназначен для отображения концентрации точек в пространстве с использованием цветовой градации и размытия, что позволяет выявлять зоны повышенной активности. При переходе к динамическим данным появляется необходимость анимации тепловой карты — изменения её состояния во времени, в зависимости от поступающих или фильтруемых данных.

Тепловая карта строится на наборе точек, представленных объектами LatLng или взвешенными значениями:

  • координаты (широта и долгота)
  • опциональный вес (intensity)

Базовая структура данных:

const heatmapData = [
  new google.maps.LatLng(55.751244, 37.618423),
  new google.maps.LatLng(55.761244, 37.628423),
];

Взвешенные точки:

const heatmapData = [
  { location: new google.maps.LatLng(55.751244, 37.618423), weight: 2 },
  { location: new google.maps.LatLng(55.761244, 37.628423), weight: 5 }
];

Инициализация слоя:

const heatmap = new google.maps.visualization.HeatmapLayer({
  data: heatmapData,
  radius: 20,
  opacity: 0.6,
  dissipating: true
});

heatmap.setMap(map);

Принципы анимации тепловой карты

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

Основные подходы:

  • обновление набора точек во времени
  • изменение весов данных
  • модификация визуальных параметров слоя
  • использование временного окна (sliding window)
  • циклическое переключение кадров данных

Обновление данных слоя

Ключевой метод управления динамикой — setData():

heatmap.setData(new google.maps.MVCArray(updatedPoints));

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

Пример пошагового обновления:

let frameIndex = 0;

function animate() {
  const frameData = datasets[frameIndex];

  heatmap.setData(new google.maps.MVCArray(frameData));

  frameIndex = (frameIndex + 1) % datasets.length;

  setTimeout(animate, 200);
}

animate();

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

Плавная анимация через requestAnimationFrame

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

function interpolatePoints(a, b, t) {
  return a.map((point, i) => {
    const lat = point.lat() + (b[i].lat() - point.lat()) * t;
    const lng = point.lng() + (b[i].lng() - point.lng()) * t;

    return new google.maps.LatLng(lat, lng);
  });
}

Цикл анимации:

let t = 0;

function animate() {
  const interpolated = interpolatePoints(frameA, frameB, t);

  heatmap.setData(new google.maps.MVCArray(interpolated));

  t += 0.02;

  if (t >= 1) {
    t = 0;
    swapFrames();
  }

  requestAnimationFrame(animate);
}

animate();

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

Анимация через изменение веса точек

Помимо координат, изменяется интенсивность влияния каждой точки.

const animatedData = baseData.map(p => ({
  location: p.location,
  weight: Math.sin(Date.now() * 0.001 + p.phase) * 5 + 5
}));

Обновление слоя:

heatmap.setData(new google.maps.MVCArray(animatedData));

Изменение весов позволяет создавать эффект пульсации и “дыхания” тепловой карты без изменения географии точек.

Управление визуальными параметрами

Анимационный эффект усиливается изменением параметров слоя:

Радиус влияния

heatmap.setOptions({
  radius: currentRadius
});

Динамическое изменение:

currentRadius = 10 + Math.sin(Date.now() * 0.002) * 20;

Прозрачность

heatmap.setOptions({
  opacity: currentOpacity
});
currentOpacity = 0.4 + Math.abs(Math.sin(Date.now() * 0.001));

Градиент

Изменение цветовой шкалы позволяет визуализировать “перетекание” интенсивности:

heatmap.setOptions({
  gradient: [
    "rgba(0, 0, 255, 0)",
    "rgba(0, 255, 255, 1)",
    "rgba(0, 255, 0, 1)",
    "rgba(255, 255, 0, 1)",
    "rgba(255, 0, 0, 1)"
  ]
});

Переключение градиентов во времени:

heatmap.setOptions({
  gradient: gradients[index]
});

Временные тепловые карты

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

function getWindowedData(allData, timestamp, windowSize) {
  return allData.filter(point =>
    point.time >= timestamp - windowSize &&
    point.time <= timestamp
  );
}

Анимация по времени:

let time = startTime;

function tick() {
  const currentData = getWindowedData(allPoints, time, 60000);

  heatmap.setData(new google.maps.MVCArray(currentData));

  time += 10000;

  setTimeout(tick, 300);
}

tick();

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

Оптимизация частоты обновлений

Частое обновление тепловой карты приводит к нагрузке на рендеринг. Для стабилизации используются:

  • ограничение FPS через setTimeout
  • батчинг обновлений данных
  • предварительное вычисление кадров
  • переиспользование MVCArray

Оптимизированный цикл:

const dataArray = new google.maps.MVCArray();

heatmap.setData(dataArray);

function update(frame) {
  dataArray.clear();

  frame.forEach(p => dataArray.push(p));
}

Такой подход снижает количество пересозданий структуры данных.

Комбинированная анимация

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

  • координат точек
  • их веса
  • радиуса влияния
  • прозрачности
  • цветового градиента

Пример синхронного обновления:

function animateFrame(frame) {
  heatmap.setOptions({
    radius: frame.radius,
    opacity: frame.opacity,
    gradient: frame.gradient
  });

  heatmap.setData(new google.maps.MVCArray(frame.points));
}

Поведение при масштабировании карты

При изменении масштаба (zoom) тепловая карта может требовать адаптации плотности отображения.

google.maps.event.addListener(map, "zoom_changed", () => {
  const zoom = map.getZoom();

  heatmap.setOptions({
    radius: Math.max(5, zoom * 2)
  });
});

Зависимость параметров от масштаба позволяет сохранить читаемость структуры распределения.

Буферизация кадров

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

const frames = precomputeFrames(rawData);

function precomputeFrames(data) {
  return data.map(step => ({
    points: transform(step),
    radius: computeRadius(step),
    opacity: computeOpacity(step)
  }));
}

Это снижает нагрузку в момент выполнения и переносит вычисления в этап подготовки данных.

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

При подключении потоковых данных (WebSocket, polling API) анимация становится непрерывной:

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

  heatmap.setData(new google.maps.MVCArray(newPoints));
};

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