Skip to content

Heatmap Layer

A heatmap visualizes the density or intensity of point data as a smooth color gradient. It's the right choice when you have many overlapping points and want to show concentration rather than individual features.

Simulated event density around major world cities. Color ramps from blue (sparse) to red (dense). Zoom in to see individual points emerge.

Add sources and layers after the style loads

Every call below needs a loaded style. Run them from onStyleLoadedCallback, not from onMapCreated, and run them there again after a style change: a new style discards every source and layer you added. See Constraints and gotchas.

When to use a heatmap

  • You have hundreds to millions of point features
  • Individual points overlap and become unreadable
  • You want to show geographic density (crime, traffic, sales, weather)
  • You want the map to transition from heatmap at low zoom to individual points at high zoom

Basic setup

// 1. Add a GeoJSON source with point features
await controller.addGeoJsonSource('events', {
  'type': 'FeatureCollection',
  'features': myPointFeatures,
});

// 2. Add the heatmap layer
await controller.addHeatmapLayer(
  'events',
  'events-heat',
  HeatmapLayerProperties(
    heatmapRadius: 30,       // pixel radius per point
    heatmapOpacity: 0.8,
  ),
);

Color ramp

The heatmapColor expression maps heatmap-density (0 to 1) to colors. Always start at rgba(..., 0) for the lowest density so sparse areas are transparent:

heatmapColor: [
  Expressions.interpolate,
  ['linear'],
  [Expressions.heatmapDensity],
  0.0, 'rgba(33, 102, 172, 0)',     // transparent
  0.2, 'rgb(103, 169, 207)',         // light blue
  0.4, 'rgb(209, 229, 240)',         // very light
  0.6, 'rgb(253, 219, 199)',         // light orange
  0.8, 'rgb(239, 138,  98)',         // orange
  1.0, 'rgb(178,  24,  43)',         // red
],

Weight per point

If some points are more significant than others (e.g. earthquake magnitude, sales value), use heatmapWeight to drive intensity from a property:

heatmapWeight: [
  Expressions.interpolate,
  ['linear'],
  [Expressions.get, 'magnitude'],
  0, 0,    // magnitude 0 → no weight
  6, 1,    // magnitude 6 → full weight
],

Zoom-responsive radius and intensity

Make the heatmap adapt as users zoom in:

HeatmapLayerProperties(
  heatmapRadius: [
    Expressions.interpolate,
    ['linear'],
    [Expressions.zoom],
    0,  2,    // tiny radius at world zoom
    9,  20,   // larger radius at city zoom
  ],
  heatmapIntensity: [
    Expressions.interpolate,
    ['linear'],
    [Expressions.zoom],
    0, 1,
    9, 3,
  ],
  heatmapOpacity: [
    Expressions.interpolate,
    ['linear'],
    [Expressions.zoom],
    7, 1.0,   // fully visible at zoom 7
    9, 0.5,   // fade out at zoom 9 (circles take over)
  ],
),

Transition from heatmap to individual points

A common pattern is to show the heatmap at low zoom and individual circles at high zoom, creating a smooth transition:

// Heatmap: visible at low zoom, fades out
await controller.addHeatmapLayer('events', 'events-heat',
  HeatmapLayerProperties(
    heatmapOpacity: [
      Expressions.interpolate, ['linear'], [Expressions.zoom],
      7, 1.0,
      9, 0.0,  // fully transparent at zoom 9
    ],
  ),
  minzoom: 0,
  maxzoom: 9,
);

// Circles: invisible at low zoom, fade in
await controller.addCircleLayer('events', 'events-circles',
  CircleLayerProperties(
    circleOpacity: [
      Expressions.interpolate, ['linear'], [Expressions.zoom],
      7, 0.0,  // transparent at zoom 7
      9, 0.8,  // visible at zoom 9
    ],
    circleRadius: 4,
    circleColor: '#E74C3C',
  ),
  minzoom: 7,
);

Key HeatmapLayerProperties fields

Property Description
heatmapRadius Pixel radius each point contributes to
heatmapWeight Per-point contribution (or expression)
heatmapIntensity Global multiplier on the accumulated density
heatmapColor Density-to-color ramp, keyed on heatmap-density
heatmapOpacity Layer opacity 0 to 1

Key APIs