Heatmap
Visualize point density with a declarative, theme-aware heatmap layer.
Basic Example
#ng-map-heatmap renders a GeoJSON point source as a MapLibre heatmap layer. The default gradient follows the map theme. This example weights USGS earthquakes by magnitude.
import { Component } from '@angular/core';
import { MapComponent, MapHeatmapComponent } from 'ng-mapcn';
@Component({
selector: 'app-heatmap-map',
standalone: true,
imports: [MapComponent, MapHeatmapComponent],
template: `
<div class="h-[420px]">
<ng-map [center]="[-103.5917, 40.6699]" [zoom]="3">
<ng-map-heatmap
[data]="earthquakesUrl"
weightProperty="mag"
[weightRange]="[0, 7.5]"
[radius]="20"
/>
</ng-map>
</div>
`,
})
export class HeatmapMapComponent {
// USGS earthquake data (GeoJSON points)
readonly earthquakesUrl =
'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson';
}Inline data
#data also takes a GeoJSON FeatureCollection directly, so no request is needed. Here a per-point visits count is the weight.
import { Component } from '@angular/core';
import { MapComponent, MapHeatmapComponent } from 'ng-mapcn';
// Inline GeoJSON: no network request. Each point carries a "visits" weight.
const points: GeoJSON.FeatureCollection<GeoJSON.Point, { visits: number }> = {
type: 'FeatureCollection',
features: [
{ type: 'Feature', properties: { visits: 120 }, geometry: { type: 'Point', coordinates: [-46.6333, -23.5505] } },
{ type: 'Feature', properties: { visits: 80 }, geometry: { type: 'Point', coordinates: [-46.6565, -23.5614] } },
// ...
],
};
@Component({
selector: 'app-inline-heatmap',
standalone: true,
imports: [MapComponent, MapHeatmapComponent],
template: `
<ng-map [center]="[-46.6333, -23.5505]" [zoom]="10.5">
<ng-map-heatmap
[data]="points"
weightProperty="visits"
[weightRange]="[0, 120]"
[radius]="40"
/>
</ng-map>
`,
})
export class InlineHeatmapComponent {
readonly points = points;
}Custom gradient
# Pass gradient to replace the theme default. Changing it at runtime updates the existing layer instead of re-creating it.
import { Component, signal } from '@angular/core';
import { type HeatmapGradient, MapComponent, MapHeatmapComponent } from 'ng-mapcn';
@Component({
selector: 'app-gradient-map',
standalone: true,
imports: [MapComponent, MapHeatmapComponent],
template: `
<button (click)="palette.set('fire')">Fire</button>
<button (click)="palette.set('ocean')">Ocean</button>
<button (click)="palette.set('mono')">Mono</button>
<ng-map [center]="[-103.5917, 40.6699]" [zoom]="3">
<ng-map-heatmap [data]="earthquakesUrl" [gradient]="gradients[palette()]" />
</ng-map>
`,
})
export class GradientMapComponent {
readonly gradients: Record<'fire' | 'ocean' | 'mono', HeatmapGradient> = {
fire: [[0, 'rgba(255, 237, 160, 0)'], [0.4, '#fd8d3c'], [0.7, '#e31a1c'], [1, '#800026']],
ocean: [[0, 'rgba(8, 81, 156, 0)'], [0.4, '#4292c6'], [0.7, '#2171b5'], [1, '#08306b']],
mono: [[0, 'rgba(255, 255, 255, 0)'], [1, '#ffffff']],
};
readonly palette = signal<'fire' | 'ocean' | 'mono'>('fire');
readonly earthquakesUrl =
'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson';
}Live tuning
#radius, intensity and opacity are applied with diffed setters, so dragging the sliders never re-creates the layer.
import { Component, signal } from '@angular/core';
import { MapComponent, MapHeatmapComponent } from 'ng-mapcn';
@Component({
selector: 'app-tuning-map',
standalone: true,
imports: [MapComponent, MapHeatmapComponent],
template: `
<input type="range" min="5" max="60" [value]="radius()"
(input)="radius.set(+$any($event.target).value)" />
<input type="range" min="0.2" max="3" step="0.1" [value]="intensity()"
(input)="intensity.set(+$any($event.target).value)" />
<input type="range" min="0.1" max="1" step="0.05" [value]="opacity()"
(input)="opacity.set(+$any($event.target).value)" />
<ng-map [center]="[-103.5917, 40.6699]" [zoom]="3">
<ng-map-heatmap
[data]="earthquakesUrl"
weightProperty="mag"
[weightRange]="[0, 7.5]"
[radius]="radius()"
[intensity]="intensity()"
[opacity]="opacity()"
/>
</ng-map>
`,
})
export class TuningMapComponent {
readonly radius = signal(20);
readonly intensity = signal(1);
readonly opacity = signal(1);
readonly earthquakesUrl =
'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson';
}Zoom-dependent heatmap
# Zoom in and out: the radius and intensity expressions scale with the zoom level, and the layer stops rendering from maxZoom 7.
import { Component } from '@angular/core';
import { MapComponent, MapHeatmapComponent } from 'ng-mapcn';
@Component({
selector: 'app-zoom-map',
standalone: true,
imports: [MapComponent, MapHeatmapComponent],
template: `
<ng-map [center]="[-103.5917, 40.6699]" [zoom]="3">
<ng-map-heatmap
[data]="earthquakesUrl"
weightProperty="mag"
[weightRange]="[0, 7.5]"
[radius]="radius"
[intensity]="intensity"
[maxZoom]="7"
/>
</ng-map>
`,
})
export class ZoomMapComponent {
// Blobs grow and brighten as you zoom in; the layer stops at zoom 7.
readonly radius = ['interpolate', ['linear'], ['zoom'], 0, 6, 6, 40];
readonly intensity = ['interpolate', ['linear'], ['zoom'], 0, 0.6, 6, 2];
readonly earthquakesUrl =
'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson';
}Weight
# Set weightProperty to weight points by a numeric feature property. weightRange is the [min, max] of that property, mapped linearly to a weight of 0 to 1.
<!-- Every point weighs the same -->
<ng-map-heatmap [data]="points" />
<!-- Weight by a numeric property; [min, max] is mapped linearly to 0..1.
Missing or non-numeric values count as 0. -->
<ng-map-heatmap [data]="points" weightProperty="mag" [weightRange]="[0, 7.5]" />Gradient and theme
# Without gradient the layer uses defaultHeatmapGradients for the resolved theme and re-adds itself with the matching one after a theme switch. A custom gradient is kept across theme changes.
import { type HeatmapGradient, defaultHeatmapGradients } from 'ng-mapcn';
// [density 0..1, color] stops. A transparent stop at 0 is prepended when you
// start higher, so empty areas stay clear; unsorted stops are sorted.
readonly gradient: HeatmapGradient = [
[0, 'rgba(34, 197, 94, 0)'],
[0.5, '#eab308'],
[1, '#ef4444'],
];
// <ng-map-heatmap [data]="points" [gradient]="gradient" />
// Without [gradient], defaultHeatmapGradients.light / .dark follow the map theme.Zoom-dependent styling
#radius, intensity and opacity accept MapLibre expressions. Use minZoom / maxZoom to limit the zoom range; the heatmap is not rendered from maxZoom up, so a circle layer can take over when zoomed in.
// radius, intensity and opacity accept numbers or MapLibre expressions
readonly radius = ['interpolate', ['linear'], ['zoom'], 0, 8, 6, 34];
readonly opacity = ['interpolate', ['linear'], ['zoom'], 4.5, 0.75, 6.5, 0.08];
// <ng-map-heatmap
// [data]="points"
// [radius]="radius"
// [opacity]="opacity"
// [minZoom]="0"
// [maxZoom]="7" // stop rendering from zoom 7 (e.g. a point layer takes over)
// [paint]="{ 'heatmap-intensity': 1.5 }" // escape hatch, merged last
// />Ids and z-order
# Set id to get stable source and layer ids, and beforeId to insert the layer below an existing one. Like the other layer components, beforeId is applied at creation only, and changing id re-creates the layer.
<ng-map-heatmap id="earthquakes" beforeId="labels" [data]="earthquakes" />
// Layer ids derived from id="earthquakes":
// source: heatmap-source-earthquakes
// layer: heatmap-layer-earthquakes
// Without id they are auto-generated (e.g. heatmap-layer-ng-heatmap-1).