Traffic area analytics module
The Traffic Area Analytics Module visualizes historical traffic data for a geographic region on the map. Data is fetched from the TomTom Move Portal API and rendered using one of five visualization modes: 3D or flat hexagonal grids, 3D or flat square grids, or a density heatmap.
What this module owns: the grid and heatmap layers it renders, and how they look — the colour, height and layer-position configuration below. It never restyles the base map underneath, which belongs to the Styling module, and styling knobs never touch these layers. See Who owns what.
Initialization
import { TomTomMap, TrafficAreaAnalyticsModule } from '@tomtom-org/maps-sdk/map';
const map = new TomTomMap({ mapLibre: { container: 'map' } });
const analyticsModule = await TrafficAreaAnalyticsModule.create(map);
Configure the module at initialization time using a config object:
const analyticsModule = await TrafficAreaAnalyticsModule.create(map, {
displayMode: 'hexgrid-3d',
activeMetric: 'congestionLevel',
metricConfig: {
congestionLevel: { palette: 'trafficLight' },
},
});
Fetching and displaying data
Fetch area analytics data using the trafficAreaAnalytics service and pass the response directly to show().
import { geocodeOne, geometryData, trafficAreaAnalytics } from '@tomtom-org/maps-sdk/services';
import { bboxFromGeoJSON } from '@tomtom-org/maps-sdk/core';
// 1. Geocode a city to get its boundary
const place = await geocodeOne('Amsterdam, Netherlands');
const boundary = await geometryData({ geometries: [place] });
const regionGeometry = boundary.features[0].geometry;
// 2. Fetch analytics for the last 7 days
const endDate = new Date();
endDate.setDate(endDate.getDate() - 3); // the latest date that always has data
const startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - 6);
const fmt = (d) => d.toISOString().slice(0, 10);
const analytics = await trafficAreaAnalytics({
apiKey: MOVE_PORTAL_KEY,
name: 'Amsterdam, Netherlands',
startDate: fmt(startDate),
endDate: fmt(endDate),
metrics: 'all',
functionalRoadClasses: 'all',
hours: 'all',
geometry: regionGeometry,
});
// 3. Display on the map
await analyticsModule.show(analytics);
To remove data from the map:
await analyticsModule.clear();
Visualization modes
Five modes are available, set via displayMode at init time or changed at runtime with updateConfig({ displayMode }):
| Mode | Description |
|---|---|
'hexgrid-3d' |
3D extruded hexagonal cells colored and raised by metric value (default) |
'hexgrid-2d' |
Flat hexagonal cells colored by metric value |
'square-3d' |
3D extruded square cells colored and raised by metric value |
'square-2d' |
Flat square cells colored by metric value |
'heatmap' |
MapLibre density heatmap from tile center points |
Switch modes at any time:
analyticsModule.updateConfig({ displayMode: 'heatmap' });
analyticsModule.updateConfig({ displayMode: 'hexgrid-2d' });
analyticsModule.updateConfig({ displayMode: 'square-3d' });
Metrics
Five metrics are available to drive color and extrusion height. Set the active one via activeMetric. Per-metric style settings (palette or colour stops, height, filters) are configured via metricConfig.
| Metric | Description |
|---|---|
'congestionLevel' |
Percentage increase in travel time above free-flow conditions (default active metric) |
'speed' |
Average vehicle speed in km/h |
'travelTime' |
Average travel time per 10 km in minutes |
'freeFlowSpeed' |
Average speed under uncongested conditions in km/h |
'networkLength' |
Total road length within each tile in metres — useful as a road density indicator |
analyticsModule.updateConfig({ activeMetric: 'speed' });
analyticsModule.updateConfig({ activeMetric: 'travelTime' });
analyticsModule.updateConfig({ activeMetric: 'freeFlowSpeed' });
You can also pre-configure multiple metrics at init time:
const analyticsModule = await TrafficAreaAnalyticsModule.create(map, {
activeMetric: 'congestionLevel',
metricConfig: {
congestionLevel: { palette: 'trafficLight' },
speed: { palette: 'heat' },
travelTime: { height: { maxHeightMeters: 300 } },
},
});
Color
Each metric takes its colours from metricConfig.[metricKey].palette, a named palette, or from metricConfig.[metricKey].colorStops, your own stops. When both are set, colorStops wins; with neither, the metric draws 'trafficLight'. setPalette() and setColorStops() update every metric, or only the metrics you pass as their second argument, and each one changes only its own field.
Palettes
areaAnalyticsPalettes lists the palette names. Each one runs from good traffic to bad:
analyticsModule.setPalette('trafficLight'); // green → amber → red (default)
analyticsModule.setPalette('heat'); // blue → orange → red
analyticsModule.setPalette('monochrome'); // light grey → dark grey
analyticsModule.setPalette('viridis'); // yellow → green → blue → purple
analyticsModule.setPalette('plasma'); // yellow → orange → magenta → purple
analyticsModule.setPalette('viridis', ['speed', 'freeFlowSpeed']); // only these metrics
A palette spans the metric's predefined range, and on networkLength the loaded range, since its per-tile values vary too widely for a fixed one. For speed and freeFlowSpeed its order is inverted, so that slow speeds render as bad (red) and fast speeds as good (green). getConfig() keeps the palette as its name.
Custom color stops
Provide an AreaAnalyticsColorStopsConfig object with a valueType and stops array to setColorStops(), or as colorStops in the config. Stops are used as given, with no inversion for speed.
'raw' (default) — stop value fields are actual data values:
analyticsModule.setColorStops({
valueType: 'raw',
stops: [
{ value: 0, color: '#2dc653' }, // 0% congestion
{ value: 30, color: '#f5a623' }, // 30% congestion
{ value: 100, color: '#e03030' }, // 100% congestion
],
});
'relativeToPredefinedRangePCT' — stop value fields are 0–100 percentages relative to the SDK's predefined metric ranges (congestion 0–100, speed 0–120 km/h, travelTime 0–20 s/km). Useful for consistent coloring across datasets:
analyticsModule.setColorStops({
valueType: 'relativeToPredefinedRangePCT',
stops: [
{ value: 0, color: '#2dc653' }, // 0% of predefined range
{ value: 50, color: '#f5a623' }, // 50% of predefined range
{ value: 100, color: '#e03030' }, // 100% of predefined range
],
});
'relativeToActualRangePCT' — stop value fields are 0–100 percentages relative to the live data range actually present in the loaded tiles (0 % = loaded minimum, 100 % = loaded maximum). Maximises contrast regardless of absolute values:
analyticsModule.setColorStops({
valueType: 'relativeToActualRangePCT',
stops: [
{ value: 0, color: '#2dc653' }, // 0% of loaded range
{ value: 50, color: '#f5a623' }, // 50% of loaded range
{ value: 100, color: '#e03030' }, // 100% of loaded range
],
});
Pass undefined to clear a field. Clearing the stops brings back the palette; clearing both draws 'trafficLight':
analyticsModule.setColorStops(undefined); // back to the metric's palette
analyticsModule.setPalette(undefined); // and with no palette, to 'trafficLight'
Height
Height (extrusion) configuration is per-metric under metricConfig.[metricKey].height. setHeight() updates it for every metric, or only for the metrics you pass as its second argument.
// Predefined range (default): metric normalized to SDK predefined range, visual max = maxHeightMeters
analyticsModule.setHeight({ maxHeightMeters: 200 });
// Set a minimum height floor
analyticsModule.setHeight({ maxHeightMeters: 100, minHeightMeters: 10 });
// Current range: normalizes to live data range — maximises contrast for whatever data is loaded
analyticsModule.setHeight({ maxHeightMeters: 150, scaleMode: 'currentRange' });
// Raw: each unit of the metric adds metersPerUnit metres of height (default 1)
analyticsModule.setHeight({ scaleMode: 'raw', metersPerUnit: 10 });
Filtering
Filter visible tiles by value range using setFilters(), for every metric or only for the metrics you pass as its second argument. The active metric's filter decides which tiles are rendered.
// Show only tiles with congestion ≥ 50
analyticsModule.setFilters({ min: 50 });
// Show only tiles within a range
analyticsModule.setFilters({ min: 20, max: 80 });
// Clear the filter
analyticsModule.setFilters(undefined);
Region boundary
When show() is called, all region polygons in the analytics response are rendered alongside the analytics cells as a subtle fill with an outline. Customize their appearance using regionPolygon:
const analyticsModule = await TrafficAreaAnalyticsModule.create(map, {
regionPolygon: {
color: '#0052a5', // fill and outline color (default: black on a light style, white on a dark one)
fillOpacity: 0.08, // default: 0
outlineOpacity: 1.0, // default: 0.5
outlineWidth: 3, // default: 2
}
});
Layer positioning
Control where analytics layers are placed in the map's rendering stack using beforeLayerConfig. Use 'top' to draw above all layers, or supply a well-known style layer ID to insert the analytics layers below it.
Give one target for every display mode, or a record keyed by display mode — heatmap,
hexgrid-2d, hexgrid-3d, square-2d, square-3d — plus all for the modes it leaves out. By
default the heatmap and the 2D modes sit below lowestLabel, and the 3D modes below
lowestPlaceLabel. The region boundary always sits below the labels.
// Place everything on top
const analyticsModule = await TrafficAreaAnalyticsModule.create(map, {
beforeLayerConfig: 'top',
});
// Flat layers below all labels, extruded ones below place names
const analyticsModule = await TrafficAreaAnalyticsModule.create(map, {
beforeLayerConfig: { all: 'lowestLabel', 'hexgrid-3d': 'lowestPlaceLabel', 'square-3d': 'lowestPlaceLabel' },
});
// Reposition after initialization
analyticsModule.updateConfig({ beforeLayerConfig: { 'hexgrid-3d': 'top' } });
A mode the record leaves out, with no all to cover it, goes back to its default position.
Visibility
Control overall layer visibility. isVisible() reports this visible setting, not whether any tile is
drawn: a module showing nothing still returns true. Read getShown() to know whether data is shown.
// Hidden at startup
const analyticsModule = await TrafficAreaAnalyticsModule.create(map, { visible: false });
// Toggle later
analyticsModule.setVisible(true);
// Check the setting, and whether there is data to draw
console.log(analyticsModule.isVisible()); // true
console.log(analyticsModule.getShown().hexgrid.features.length > 0);
Events
Listen to click and hover events on hexgrid and square cells. Events fire for whichever mode is currently active.
analyticsModule.events.on('click', (feature, lngLat) => {
console.log('Congestion:', feature.properties.congestionLevel);
console.log('Speed:', feature.properties.speed);
});
analyticsModule.events.on('hover', (feature) => {
console.log('Hovered cell:', feature.properties);
});
analyticsModule.events.on('long-hover', (feature) => {
console.log('Long-hovered cell:', feature.properties);
});
// Remove a specific listener
analyticsModule.events.off('click');
A hovered or clicked cell is highlighted. Highlight one from your own UI, or read which ones are, with the module's event state methods:
analyticsModule.putEventState({ index: 0, state: 'click' });
const { click = [] } = analyticsModule.getEventStates();
Custom tooltip on hover
Use events.on('hover') to build a hover tooltip:
import { Popup } from 'maplibre-gl';
const popup = new Popup({ closeButton: false, anchor: 'left', offset: 12 });
analyticsModule.events.on('hover', (feature, lngLat) => {
if (!feature) { popup.remove(); return; }
const { congestionLevel, speed } = feature.properties;
popup
.setLngLat(lngLat)
.setHTML(`<strong>Congestion:</strong> ${congestionLevel ?? 0}%<br/><strong>Speed:</strong> ${speed ?? 0} km/h`)
.addTo(map.mapLibreMap);
});
map.mapLibreMap.getCanvas().addEventListener('mouseleave', () => popup.remove());
Changing the configuration
applyConfig(config) replaces the whole configuration: what it omits returns to its default, not to what an earlier call set. updateConfig(partial) changes the given properties and keeps the rest, one level deep — a metricConfig you pass replaces the whole record, so the metrics it leaves out return to their defaults.
To change one metric and keep the others, use the setters setPalette, setColorStops, setHeight and setFilters, which take the metrics to target:
analyticsModule.applyConfig({ displayMode: 'heatmap' }); // everything else back to its default
analyticsModule.updateConfig({ activeMetric: 'speed' }); // displayMode stays 'heatmap'
analyticsModule.setPalette('viridis', ['speed']); // the other metrics keep their colours
Config change events
Subscribe to config changes, emitted by every setter and by applyConfig and updateConfig:
const unsub = analyticsModule.events.on('config-change', (config) => {
console.log('Active metric:', config?.activeMetric);
});
// Later: unsub();
Querying shown features
Use getShown() to access the GeoJSON feature collections currently rendered for each visualization mode:
const { heatmap, hexgrid, square } = analyticsModule.getShown();
console.log(`Hexagonal cells in view: ${hexgrid.features.length}`);
Every setting as data
trafficAreaAnalyticsKnobCatalogue lists the module's plain-valued settings with their kind, range
or options and, where one value holds everywhere, their default. A UI can build its controls from it, with no map loaded:
import { trafficAreaAnalyticsKnobCatalogue } from '@tomtom-org/maps-sdk/map';
const speedKnobs = trafficAreaAnalyticsKnobCatalogue.filter(({ id }) => id.startsWith('metricConfig.speed.'));
// [{ id: 'metricConfig.speed.palette', kind: 'enum', description: '…', options: ['trafficLight', 'heat', …], default: 'trafficLight' }, …,
// { id: 'metricConfig.speed.filters.min', kind: 'number', description: '…', range: { min: 0, max: 120, bounds: 'hard-min' } }, …]
The module-wide knobs come first, then the same seven for each metric, <metric> standing for any of
areaAnalyticsMetricKeys:
| Id | Kind |
|---|---|
visible |
toggle |
displayMode |
enum |
activeMetric |
enum |
regionPolygon.color |
color |
regionPolygon.fillOpacity, regionPolygon.outlineOpacity |
number, 0 to 1 |
regionPolygon.outlineWidth |
number, 0 to 10 pixels |
regionPolygon.inverted |
toggle |
metricConfig.<metric>.palette |
enum: areaAnalyticsPalettes, default trafficLight |
metricConfig.<metric>.height.scaleMode |
enum |
metricConfig.<metric>.height.maxHeightMeters, metricConfig.<metric>.height.minHeightMeters |
number, 0 to 5000 metres |
metricConfig.<metric>.height.metersPerUnit |
number, 0 to 100 metres per unit of the metric, in the raw scale mode |
metricConfig.<metric>.filters.min, metricConfig.<metric>.filters.max |
number, over the metric's predefined range |
trafficAreaAnalyticsKnobIdslists the ids,TrafficAreaAnalyticsKnobIdis their union, andTrafficAreaAnalyticsKnobValueOf<ID>the plain value one takes.- A metric's filters hide cells only while it is the
activeMetric, and never in the heatmap. Their range is the one the module scales colours and heights over, not a bound on the data. - Heights, outline width and the filters have
hard-minranges: a larger value thanmaxstill applies. - Not in it:
colorStops,beforeLayerConfigandevents, which take structured values. The entry shape and its kinds are the modules guide's.
API reference
For complete documentation of all properties, methods, and types, see the TrafficAreaAnalyticsModule API Reference.
Related guides and examples
Traffic overview
- Traffic Overview - Overview of all traffic visualization options
Related traffic modules
- Traffic Flow Module - Real-time traffic speed conditions across the road network
- Traffic Incidents Module - Real-time traffic events and incidents
Related examples
- Traffic Area Analytics - Basic area analytics with a geocoded location
- Traffic Area Analytics Playground - Full-featured playground with city search, legend, tooltips, and the most congested cell selected with
putEventState - Traffic Area Analytics Config Playground - 2D/3D mode toggle and custom color pickers