Building Geographic Visualizations (Mapbox GL/Deck.gl) for Websites
Mapbox GL JS and Deck.gl enable creation of interactive maps with custom data layers: activity heatmaps, delivery routes, choropleth maps by regions, scatter plots by coordinates.
Mapbox GL JS — Basic Map
npm install mapbox-gl react-map-gl
import Map, { Source, Layer, Popup } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
function DeliveryMap({ orders }) {
const [popup, setPopup] = useState(null);
const geojsonData = {
type: 'FeatureCollection',
features: orders.map(order => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [order.lng, order.lat]
},
properties: {
id: order.id,
status: order.status,
address: order.address
}
}))
};
return (
<Map
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
initialViewState={{ longitude: 37.6, latitude: 55.75, zoom: 10 }}
style={{ width: '100%', height: 500 }}
mapStyle="mapbox://styles/mapbox/light-v11"
>
<Source id="orders" type="geojson" data={geojsonData}>
<Layer
id="orders-circles"
type="circle"
paint={{
'circle-radius': 8,
'circle-color': [
'match', ['get', 'status'],
'delivered', '#22c55e',
'in-transit', '#f59e0b',
'failed', '#ef4444',
'#6b7280'
],
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}}
onClick={(e) => {
const feature = e.features?.[0];
if (feature) {
setPopup({
lng: e.lngLat.lng,
lat: e.lngLat.lat,
...feature.properties
});
}
}}
/>
</Source>
{popup && (
<Popup longitude={popup.lng} latitude={popup.lat}
onClose={() => setPopup(null)} closeButton={false}>
<div>
<strong>Order #{popup.id}</strong>
<p>{popup.address}</p>
<StatusBadge status={popup.status} />
</div>
</Popup>
)}
</Map>
);
}
Deck.gl — Large Dataset Visualizations
npm install deck.gl @deck.gl/react @deck.gl/layers @deck.gl/aggregation-layers
import DeckGL from '@deck.gl/react';
import { Map } from 'react-map-gl';
import { HeatmapLayer, ScatterplotLayer, ArcLayer } from 'deck.gl';
function HeatmapDashboard({ events, routes }) {
const layers = [
new HeatmapLayer({
id: 'heatmap',
data: events,
getPosition: d => [d.longitude, d.latitude],
getWeight: d => d.weight,
radiusPixels: 30,
intensity: 1,
threshold: 0.1,
colorRange: [
[0, 25, 0, 0],
[0, 85, 0, 85],
[0, 170, 0, 170],
[0, 255, 0, 255]
]
}),
new ArcLayer({
id: 'arcs',
data: routes,
getSourcePosition: d => d.from,
getTargetPosition: d => d.to,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: d => d.volume / 100,
pickable: true,
onClick: ({ object }) => console.log('Route:', object)
})
];
return (
<DeckGL
initialViewState={{ longitude: 37.6, latitude: 55.75, zoom: 4, pitch: 40 }}
controller={true}
layers={layers}
getTooltip={({ object }) => object && `${object.from_city} → ${object.to_city}`}
>
<Map mapboxAccessToken={MAPBOX_TOKEN} mapStyle="mapbox://styles/mapbox/dark-v11" />
</DeckGL>
);
}
Choropleth Map by Regions
const russiaRegions = await fetch('/api/geodata/russia-regions.geojson').then(r => r.json());
function RegionChoropleth({ revenueByRegion }) {
const choroplethData = {
...russiaRegions,
features: russiaRegions.features.map(feature => ({
...feature,
properties: {
...feature.properties,
revenue: revenueByRegion[feature.properties.regionCode] ?? 0
}
}))
};
return (
<Map initialViewState={{ longitude: 65, latitude: 65, zoom: 2 }}>
<Source type="geojson" data={choroplethData}>
<Layer
type="fill"
paint={{
'fill-color': [
'interpolate', ['linear'],
['get', 'revenue'],
0, '#f0f9ff',
100000, '#3b82f6',
1000000, '#1e40af'
],
'fill-opacity': 0.8
}}
/>
<Layer
type="line"
paint={{ 'line-color': '#ffffff', 'line-width': 0.5 }}
/>
</Source>
</Map>
);
}
Timeline
Basic map with markers and popups — 3–5 days. Deck.gl heatmaps and arc layers — 1–2 weeks. Choropleth with GeoJSON regions — another 3–5 days.







