← Files MapboxARCHIVED FILE
skills/mapbox-data-visualization-patterns/AGENTS.md
12.3 KB · Sep 30, 2026 · 23:11 UTC
# Data Visualization Patterns
Quick reference for visualizing data on Mapbox maps.
## Visualization Type Decision Matrix
| Data Type | Visualization | Layer Type | Use For |
| --------------------- | ------------- | ---------------- | ----------------------------------- |
| **Regional/Polygons** | Choropleth | `fill` | Statistics, demographics, elections |
| **Point Density** | Heat Map | `heatmap` | Crime, events, incident clustering |
| **Point Density** | Clustering | `circle` | Grouped markers, aggregated counts |
| **Point Magnitude** | Bubble/Circle | `circle` | Earthquakes, sales, metrics |
| **3D Data** | Extrusions | `fill-extrusion` | Buildings, elevation, volume |
| **Flow/Network** | Lines | `line` | Traffic, routes, connections |
## Data Structure
All code snippets below use **Style expressions** to style features based on their property data. Expressions like `['get', 'value']` access properties from your GeoJSON features:
```javascript
// Example GeoJSON feature
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-77.0323, 38.9131] // [longitude, latitude]
},
"properties": {
"magnitude": 7.8, // Custom data property
"value": 42, // Another property
"category": "coffee" // Can be any data type
}
}
```
**Accessing properties:**
```javascript
['get', 'magnitude']; // Returns 7.8
['get', 'value']; // Returns 42
['get', 'category']; // Returns "coffee"
```
## Choropleth Maps
**Pattern:** Color-code regions by data values
```javascript
map.addLayer({
id: 'choropleth',
type: 'fill',
source: 'regions',
paint: {
'fill-color': [
'interpolate',
['linear'],
['get', 'value'],
0,
'#f0f9ff', // Low
50,
'#7fcdff',
100,
'#0080ff' // High
],
'fill-opacity': 0.75
}
});
```
**Color Scale Types:**
<!-- prettier-ignore -->
```javascript
// Linear (continuous)
['interpolate', ['linear'], ['get', 'value'], 0, '#fff', 100, '#000']
// Steps (discrete buckets)
['step', ['get', 'value'], '#fff', 25, '#ccc', 50, '#888', 75, '#000']
// Categories (qualitative)
['match', ['get', 'category'], 'A', '#ff0000', 'B', '#0000ff', '#cccccc']
```
## Heat Maps
**Pattern:** Show point density
```javascript
map.addLayer({
id: 'heatmap',
type: 'heatmap',
source: 'points',
paint: {
'heatmap-weight': ['get', 'intensity'],
'heatmap-intensity': ['interpolate', ['linear'], ['zoom'], 0, 1, 15, 3],
'heatmap-color': [
'interpolate',
['linear'],
['heatmap-density'],
0,
'rgba(33,102,172,0)',
0.2,
'rgb(103,169,207)',
0.4,
'rgb(209,229,240)',
0.6,
'rgb(253,219,199)',
0.8,
'rgb(239,138,98)',
1,
'rgb(178,24,43)'
],
'heatmap-radius': ['interpolate', ['linear'], ['zoom'], 0, 2, 15, 20]
}
});
// Show individual points at high zoom
map.addLayer({
id: 'points',
type: 'circle',
source: 'points',
minzoom: 14,
paint: {
'circle-radius': 6,
'circle-color': '#ff4444'
}
});
```
## Clustering (Point Density)
**Pattern:** Group nearby points with aggregated counts
```javascript
// Add source with clustering enabled
map.addSource('points', {
type: 'geojson',
data: data,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (default 50)
});
// Clusters - sized by point count
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 10, '#f1f075', 30, '#f28cb1'],
'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 30, 40]
}
});
// Cluster count labels
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'points',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
}
});
// Unclustered points
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'points',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 6,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
// Click to expand clusters
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
map.getSource('points').getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return;
map.easeTo({ center: features[0].geometry.coordinates, zoom: zoom });
});
});
```
**When to use clustering vs heatmaps:**
- **Clustering:** Discrete grouping, exact counts, click to expand
- **Heatmaps:** Continuous density visualization, smoother appearance
## Bubble Maps
**Pattern:** Size circles by magnitude
```javascript
map.addLayer({
id: 'bubbles',
type: 'circle',
source: 'data',
paint: {
'circle-radius': ['interpolate', ['exponential', 2], ['get', 'magnitude'], 0, 2, 5, 20, 10, 100],
'circle-color': ['interpolate', ['linear'], ['get', 'magnitude'], 0, '#ffffcc', 50, '#78c679', 100, '#006837'],
'circle-opacity': 0.7,
'circle-stroke-color': '#fff',
'circle-stroke-width': 1
}
});
```
## 3D Extrusions
**Pattern:** Extrude polygons by height
> **Note:** This example works with **classic styles only** (`streets-v12`, `dark-v11`, `light-v11`, etc.). The **Mapbox Standard style** includes 3D buildings with much greater detail by default.
```javascript
// Add 3D buildings from basemap
map.on('load', () => {
// Insert the layer beneath any symbol layer
const layers = map.getStyle().layers;
const labelLayerId = layers.find((layer) => layer.type === 'symbol' && layer.layout['text-field']).id;
map.addLayer(
{
id: 'add-3d-buildings',
source: 'composite',
'source-layer': 'building',
filter: ['==', 'extrude', 'true'],
type: 'fill-extrusion',
minzoom: 15,
paint: {
'fill-extrusion-color': '#aaa',
'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'height']],
'fill-extrusion-base': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'min_height']],
'fill-extrusion-opacity': 0.6
}
},
labelLayerId
);
// Enable 3D view
map.setPitch(45);
map.setBearing(-17.6);
});
```
**Data-driven 3D (custom data):**
```javascript
// For your own data source
map.addLayer({
id: '3d-data',
type: 'fill-extrusion',
source: 'your-data',
paint: {
'fill-extrusion-height': ['get', 'height'],
'fill-extrusion-base': ['get', 'base_height'],
'fill-extrusion-color': [
'interpolate',
['linear'],
['get', 'height'],
0,
'#fafa6e',
100,
'#e64a45',
200,
'#a63e3e'
],
'fill-extrusion-opacity': 0.9
}
});
```
## Line Visualization
**Pattern:** Style lines by data
```javascript
map.addLayer({
id: 'traffic',
type: 'line',
source: 'roads',
paint: {
'line-width': ['interpolate', ['exponential', 2], ['get', 'volume'], 0, 1, 10000, 15],
'line-color': [
'interpolate',
['linear'],
['get', 'speed'],
0,
'#d73027', // Stopped
30,
'#fee08b', // Moderate
60,
'#1a9850' // Free flow
]
}
});
```
## Animated Data
**Time-Series:**
```javascript
let currentTime = 0;
function animate() {
currentTime++;
map.getSource('data').setData(getDataForTime(currentTime));
requestAnimationFrame(animate);
}
```
**Real-Time Updates:**
```javascript
setInterval(async () => {
const data = await fetch('/api/live-data').then((r) => r.json());
map.getSource('live').setData(data);
}, 5000);
```
## Performance
**Data Size Guidelines:**
| Size | Format | Strategy |
| ------- | ------------ | --------------------- |
| < 5 MB | GeoJSON | Direct load |
| 5-20 MB | GeoJSON | Consider vector tiles |
| > 20 MB | Vector Tiles | Required |
**Vector Tiles:**
```javascript
map.addSource('large-data', {
type: 'vector',
tiles: ['https://example.com/{z}/{x}/{y}.mvt']
});
map.addLayer({
id: 'data',
type: 'fill',
source: 'large-data',
'source-layer': 'layer-name'
});
```
**Feature State (Dynamic Styling):**
```javascript
// GeoJSON source with generateId
map.addSource('data', {
type: 'geojson',
data: data,
generateId: true // Required for feature state
});
// Update state (GeoJSON source)
map.setFeatureState({ source: 'data', id: featureId }, { hover: true });
// Vector tile source - requires sourceLayer
map.addSource('vector-data', {
type: 'vector',
tiles: ['https://example.com/{z}/{x}/{y}.mvt']
});
// Update state (vector source)
map.setFeatureState({ source: 'vector-data', id: featureId, sourceLayer: 'my-source-layer' }, { hover: true });
// Use in paint property
'fill-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ff0000',
'#0000ff'
]
```
The event binding above (`map.on('mousemove'/'mouseleave', ...)`) can be replaced with the newer `addInteraction` API; `setFeatureState` still drives the styling either way. See `mapbox-style-patterns`'s `references/interactions.md` for the full comparison.
**Client-Side Filtering:**
```javascript
// Filter without reloading data
map.setFilter('layer-id', ['>=', ['get', 'value'], threshold]);
```
**Progressive Loading:**
```javascript
map.on('moveend', () => {
const bounds = map.getBounds();
const visible = allData.features.filter((f) => bounds.contains(f.geometry.coordinates));
map.getSource('data').setData({ type: 'FeatureCollection', features: visible });
});
```
## Color Scales
**Accessible Colors (ColorBrewer):**
```javascript
// Sequential (single hue)
const sequential = ['#f0f9ff', '#bae4ff', '#7fcdff', '#0080ff', '#001f5c'];
// Diverging (two hues)
const diverging = ['#d73027', '#fc8d59', '#fee08b', '#d9ef8b', '#91cf60', '#1a9850'];
// Qualitative (distinct categories)
const qualitative = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00'];
```
## Legend Component
```html
<div class="legend">
<h4>Population Density</h4>
<div class="legend-item">
<span class="legend-color" style="background: #f0f9ff;"></span>
<span>0-500</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background: #0080ff;"></span>
<span>1000+</span>
</div>
</div>
```
## Common Use Cases
**Election Results:**
```javascript
'fill-color': [
'match',
['get', 'winner'],
'democrat', '#3b82f6',
'republican', '#ef4444',
'#94a3b8'
]
```
**COVID Cases:**
```javascript
'fill-color': [
'step',
['/', ['get', 'cases'], ['get', 'population']],
'#ffffb2',
0.001, '#fed976',
0.01, '#fc4e2a',
0.1, '#b10026'
]
```
**Real Estate:**
```javascript
'circle-radius': [
'interpolate',
['exponential', 2],
['get', 'price'],
100000, 5,
1000000, 20
],
'circle-color': [
'interpolate',
['linear'],
['get', 'price_per_sqft'],
0, '#ffffcc',
400, '#41b6c4',
800, '#253494'
]
```
## Quick Decisions
**Need to show regional statistics?**
→ Use choropleth with `fill` layer
**Need to show point density?**
→ Use `heatmap` layer (continuous) or clustering (discrete groups)
**Need to show point magnitude?**
→ Use `circle` layer with data-driven radius
**Need 3D visualization?**
→ Use `fill-extrusion` layer
**Need to animate over time?**
→ Use `setData()` with time-based filtering
**Large dataset (> 20 MB)?**
→ Use vector tiles instead of GeoJSON
**Need dynamic hover effects?**
→ Use feature state instead of updating data
**Color-blind friendly?**
→ Use blue-orange or purple-green, avoid red-green
## Expression Patterns
**Safe Property Access:**
```javascript
['case', ['has', 'property'], ['get', 'property'], defaultValue];
```
**Calculations:**
<!-- prettier-ignore -->
```javascript
// Divide
['/', ['get', 'numerator'], ['get', 'denominator']]
// Multiply
['*', ['get', 'value'], 1.5]
// Percentage
['*', ['/', ['get', 'part'], ['get', 'total']], 100]
```
## Resources
- [Mapbox Expression Reference](https://docs.mapbox.com/style-spec/reference/expressions/)
- [ColorBrewer](https://colorbrewer2.org/) - Accessible color scales
- [Turf.js](https://turfjs.org/) - Spatial analysis
SHA-256: 942f397482597ba1eed7a2646d279d5249586cfa10ed89e1a628eaf300bc30d0