Choropleth Map
Color regions by data values for thematic visualization Each library has its own approach: here's how they compare.
Quick Recommendations
Leaflet — Best for simple choropleths with GeoJSON data where you style each feature with a JavaScript function and need interactive hover/click.
MapLibre GL JS — Makes the most sense when choropleth colors should update dynamically using match or interpolate expressions without re-rendering features.
OpenLayers — A strong fit when choropleth data comes from WFS or other OGC sources and needs integration with OpenLayers' style function pipeline.
MapTiler SDK — The MapTiler SDK take: MapLibre's expression-based choropleth, with simplified style configuration.
At a Glance
| Leaflet | MapLibre | OpenLayers | SDK | |
|---|---|---|---|---|
| Coordinates | [lat, lng] | [lng, lat] | fromLonLat([lng, lat]) | [lng, lat] |
| Rendering | DOM + Canvas (2D) | WebGL (GPU, 60fps) | Canvas + WebGL | WebGL (GPU, 60fps) |
| Bundle size | ~42 KB gzipped | ~250 KB gzipped | ~290 KB gzipped (tree-shakeable) | ~280 KB gzipped (includes MapLibre) |
| Learning curve | Easiest (simple API, great tutorials) | Moderate (source/layer model, expressions) | Steepest (projections, OGC standards, verbose API) | Easy (MapLibre API with convenience helpers) |
| 3D support | No native 3D (2D only) | Full: terrain, fill-extrusion, globe, sky | Limited: WebGL layers, no native globe | Full (same as MapLibre) |
| Code lines | 18 | 24 | 30 | 25 |
Code Snippets
Click a library to expand its code. Each snippet shows only the core logic: the scaffold handles HTML boilerplate.
Leaflet18 linesShow code
const map = L.map('map').setView([50, 10], 4);
L.tileLayer('https://api.maptiler.com/maps/streets-v4/256/{z}/{x}/{y}.png?key=YOUR_MAPTILER_KEY', {
tileSize: 256,
attribution: '© <a href="https://www.maptiler.com/">MapTiler</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}).addTo(map);
const getColor = d => d > 200 ? '#800026' : d > 150 ? '#BD0026' : d > 100 ? '#FC4E2A' : d > 50 ? '#FED976' : '#FFEDA0';
const countries = { type: 'FeatureCollection', features: [
{ type: 'Feature', properties: { name: 'France', density: 119 }, geometry: { type: 'Polygon', coordinates: [[[2.5,51.1],[6.2,49],[8.2,48.9],[7.5,47.5],[6.8,46.4],[3.6,43.7],[-1.8,43.4],[-1.2,46],[1.4,48.6],[2.5,51.1]]] }},
{ type: 'Feature', properties: { name: 'Germany', density: 232 }, geometry: { type: 'Polygon', coordinates: [[[6,51.8],[7,53.5],[10.4,54.8],[14.1,53.9],[14.7,51],[13.6,48.9],[12.5,47.6],[7.5,47.5],[6.2,49],[6,51.8]]] }},
{ type: 'Feature', properties: { name: 'Spain', density: 93 }, geometry: { type: 'Polygon', coordinates: [[[-9.3,43.1],[-1.8,43.4],[3.2,42.3],[3.3,39.5],[0.1,38.7],[-5.6,36.5],[-7.4,37],[-8.9,41.8],[-9.3,43.1]]] }},
{ type: 'Feature', properties: { name: 'Italy', density: 201 }, geometry: { type: 'Polygon', coordinates: [[[6.8,46.4],[12.4,46.8],[13.8,46.5],[12.3,41.8],[15.5,40],[16.5,38.1],[12.5,37.5],[9,39.2],[8.1,43.9],[6.8,46.4]]] }},
{ type: 'Feature', properties: { name: 'Poland', density: 124 }, geometry: { type: 'Polygon', coordinates: [[[14.1,53.9],[18.8,54.7],[23.5,54.2],[24,51.6],[23.5,50],[18.8,49.5],[14.7,51],[14.1,53.9]]] }},
{ type: 'Feature', properties: { name: 'Sweden', density: 25 }, geometry: { type: 'Polygon', coordinates: [[[11.1,58.8],[12.3,56],[16.3,56.5],[19.1,59.4],[20.5,63.5],[24.2,65.8],[20,69],[14,68.4],[11.9,63.1],[11.1,58.8]]] }}
]};
L.geoJSON(countries, {
style: f => ({ fillColor: getColor(f.properties.density), weight: 2, color: '#fff', fillOpacity: 0.7 }),
onEachFeature: (f, layer) => layer.bindPopup(`<strong>${f.properties.name}</strong><br>Density: ${f.properties.density}/km²`)
}).addTo(map);MapLibre GL JS24 linesShow code
const map = new maplibregl.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/streets-v4/style.json?key=YOUR_MAPTILER_KEY',
center: [10, 50],
zoom: 4
});
map.on('load', () => {
const data = { type: 'FeatureCollection', features: [
{ type: 'Feature', properties: { name: 'France', value: 67 }, geometry: { type: 'Polygon', coordinates: [[[-5,42],[8,42],[8,51],[-5,51],[-5,42]]] } },
{ type: 'Feature', properties: { name: 'Germany', value: 83 }, geometry: { type: 'Polygon', coordinates: [[[6,47],[15,47],[15,55],[6,55],[6,47]]] } },
{ type: 'Feature', properties: { name: 'Spain', value: 47 }, geometry: { type: 'Polygon', coordinates: [[[-10,36],[4,36],[4,44],[-10,44],[-10,36]]] } },
{ type: 'Feature', properties: { name: 'Italy', value: 60 }, geometry: { type: 'Polygon', coordinates: [[[7,37],[19,37],[19,47],[7,47],[7,37]]] } }
] };
map.addSource('countries', { type: 'geojson', data });
const labels = map.getStyle().layers.find(l => l.type === 'symbol' && /label/.test(l.id));
map.addLayer({ id: 'choropleth', type: 'fill', source: 'countries',
paint: { 'fill-color': ['interpolate', ['linear'], ['get', 'value'], 40, '#fee5d9', 60, '#fcae91', 80, '#fb6a4a', 100, '#cb181d'], 'fill-opacity': 0.6 }
}, labels ? labels.id : undefined);
map.addLayer({ id: 'choropleth-border', type: 'line', source: 'countries', paint: { 'line-color': '#333', 'line-width': 1 } });
map.on('click', 'choropleth', (e) => {
const f = e.features[0];
new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`<b>${f.properties.name}</b><br>Pop: ${f.properties.value}M`).addTo(map);
});
});OpenLayers30 linesShow code
function getColor(value) {
return value > 80 ? '#b5179e' : value > 60 ? '#e63946' : value > 40 ? '#f4a261' : value > 20 ? '#e9c46a' : '#2a9d8f';
}
var geojson = {
type: 'FeatureCollection',
features: [
{ type: 'Feature', properties: { name: 'Zone A', value: 85 }, geometry: { type: 'Polygon', coordinates: [[[2.30, 48.87], [2.34, 48.87], [2.34, 48.85], [2.30, 48.85], [2.30, 48.87]]] } },
{ type: 'Feature', properties: { name: 'Zone B', value: 45 }, geometry: { type: 'Polygon', coordinates: [[[2.34, 48.87], [2.38, 48.87], [2.38, 48.85], [2.34, 48.85], [2.34, 48.87]]] } },
{ type: 'Feature', properties: { name: 'Zone C', value: 15 }, geometry: { type: 'Polygon', coordinates: [[[2.30, 48.85], [2.34, 48.85], [2.34, 48.83], [2.30, 48.83], [2.30, 48.85]]] } }
]
};
var vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
features: new ol.format.GeoJSON().readFeatures(geojson, { featureProjection: 'EPSG:3857' })
}),
style: function(feature) {
return new ol.style.Style({
fill: new ol.style.Fill({ color: getColor(feature.get('value')) + '99' }),
stroke: new ol.style.Stroke({ color: '#333', width: 1 })
});
}
});
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({ source: new ol.source.XYZ({ url: 'https://api.maptiler.com/maps/streets-v4/256/{z}/{x}/{y}.png?key=YOUR_MAPTILER_KEY', attributions: '© <a href="https://www.maptiler.com/copyright/">MapTiler</a>' }) }),
vectorLayer
],
view: new ol.View({ center: ol.proj.fromLonLat([2.34, 48.855]), zoom: 14 })
});MapTiler SDK25 linesShow code
maptilersdk.config.apiKey = 'YOUR_MAPTILER_KEY';
const map = new maptilersdk.Map({
container: 'map',
style: maptilersdk.MapStyle.STREETS,
center: [15.0, 50.0],
zoom: 4
});
map.on('load', () => {
map.addSource('countries', {
type: 'geojson',
data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_countries.geojson'
});
map.addLayer({
id: 'countries-fill', type: 'fill', source: 'countries',
paint: {
'fill-color': ['interpolate', ['linear'], ['get', 'pop_est'],
0, '#e8d5f5', 10000000, '#ce93d8', 100000000, '#9C27B0', 1000000000, '#4a148c'],
'fill-opacity': 0.7
}
});
map.addLayer({
id: 'countries-border', type: 'line', source: 'countries',
paint: { 'line-color': '#2a2a2a', 'line-width': 1 }
});
});Things to Know
Implementation-specific details that matter for choropleth map.
Style function runs per-feature and returns { fillColor, fillOpacity, ... }; updating colors requires re-setting style on each feature.
Uses match or step expressions in the fill-color paint property to map data values to colors declaratively.
Style function receives each feature and returns an ol.style.Style; supports complex multi-pass rendering.
Same expression-based approach as MapLibre for fill-color; identical GeoJSON source and fill layer configuration.
Which Library to Use?
Create a data-driven choropleth map that colors regions by value using inline GeoJSON and Leaflet.
Best for simple choropleths with GeoJSON data where you style each feature with a JavaScript function and need interactive hover/click.
Create a data-driven choropleth map using GeoJSON polygons and match expressions to color regions by value.
Makes the most sense when choropleth colors should update dynamically using match or interpolate expressions without re-rendering features.
Create a data-driven choropleth map using a style function to color polygons based on property values.
A strong fit when choropleth data comes from WFS or other OGC sources and needs integration with OpenLayers' style function pipeline.
Create a data-driven choropleth map using fill layer paint expressions with MapTiler SDK.
The MapTiler SDK take: MapLibre's expression-based choropleth, with simplified style configuration.
Build Maps with AI
Generate code like these examples using mapping library skills in your IDE.