Short answer
A geological legend on an interactive web map is not a static image — it is a live control surface. The colours, the ordering, and the grouping must come from the same data and style expressions that paint the map, and each legend entry should be able to filter, isolate, or toggle the features it represents. Build it by driving both the map symbology and the legend from one shared classification (typically a stratigraphic unit code), generate swatches from the style's own fill-color expression so they can never drift, and keep scientific uncertainty — inferred contacts, mapping scale, source date — visible rather than polished away. MapLibre GL handles 2D vector geology well; reach for deck.gl when you have large point/line datasets or custom GPU layers, and Cesium with 3D Tiles only when true 3D terrain or subsurface context is the point.
Order and group the way a geologist reads
A geological legend has a grammar. Stratigraphic units are conventionally ordered by age — youngest at the top or oldest at the top depending on the convention you adopt, but consistently — and colours follow a scheme (often broadly the international chronostratigraphic palette, or a project-specific one) where related units share a hue family. Structures (faults, contacts, folds) form a separate block, and base/reference data (DEM hillshade, hydrography, sample sites) form another. So the legend has nested groups, not one flat list.
Encode that grammar in your data. Give each unit a stable unit_code, a unit_label, an age_rank integer for sorting, a group for the heading, and a color. Sort legend entries by age_rank, partition by group, and you get a legend that matches geological reading order automatically — no hand-curated HTML that rots the next time a unit is added.
One source of truth for colour
The most common defect in web-map legends is colour drift: the map says one thing, the legend swatch says another, because they were authored separately. Avoid it by deriving the legend from the style. In MapLibre, colour a polygon layer with a match (or step) expression keyed on the unit field:
map.addLayer({
id: 'geology',
type: 'fill',
source: 'geology',
paint: {
'fill-color': [
'match', ['get', 'unit_code'],
'Q', '#fff8b0',
'Tv', '#f4a259',
'Kg', '#e07a5f',
'Pz', '#8a6d3b',
/* default */ '#cccccc'
],
'fill-opacity': 0.8
}
});
Then build legend swatches by reading those exact stops back:
const expr = map.getPaintProperty('geology', 'fill-color');
// expr === ['match', ['get','unit_code'], 'Q','#fff8b0', ...]
const stops = [];
for (let i = 2; i < expr.length - 1; i += 2) {
stops.push({ code: expr[i], color: expr[i + 1] });
}
// render one swatch per stop
Now the legend literally cannot disagree with the map, because both read the same array. The same principle applies to deck.gl, where the getFillColor accessor and the legend should both consume one shared colour-lookup function.
Make the legend a control, not a caption
On a screen, a static legend wastes the medium. Wire each entry to the map:
- Click to isolate:
map.setFilter('geology', ['==', ['get','unit_code'], code])shows only that unit; a "show all" button restoresnullas the filter. - Toggle visibility: for grouped layers (e.g. faults),
map.setLayoutProperty('faults', 'visibility', visible ? 'visible' : 'none'). - Highlight on hover: use a feature-state hover style so pointing at a legend row brightens matching polygons.
Indicate active state clearly (a checkmark, a dimmed row for hidden units) so users never wonder why features disappeared. This turns the legend into the primary inspection tool, which is the whole reason to be on the web instead of in a PDF.
Choosing the rendering library
- MapLibre GL JS is the default for 2D vector geology: data-driven paint expressions, vector tiles, good label handling, and an open style spec. Most geological map portals need nothing more.
- deck.gl earns its place when you have hundreds of thousands of points/lines, need custom GPU shading (e.g. continuous attribute ramps on a
ScatterplotLayerorGeoJsonLayer), or want to overlay analytical layers; it interoperates with a MapLibre base map. - CesiumJS + 3D Tiles is justified only when 3D terrain, drillhole traces, or globe-scale context is intrinsic to the message. 3D adds cognitive load and occlusion, so do not add it because it looks impressive when a clear 2D comparison would serve the user better.
Prepare the data for the browser, then design the UI
Decide delivery before styling. Vectorise large polygon geology into vector tiles (e.g. with tippecanoe) so the browser fetches only the current viewport at the current zoom; keep small overlays as GeoJSON; serve raster context (hillshade) as raster tiles. Reproject everything web maps expect to EPSG:3857 for tiling, but remember the source analysis CRS for any measurement. Sending a raw multi-hundred-MB desktop GeoJSON straight to the browser is the classic way to freeze a phone.
Keep uncertainty visible
A geological map is an interpretation, and the web version must not pretend otherwise. Concretely: render inferred contacts dashed and concealed ones dotted (drive line-dasharray from a certainty field); expose mapping scale, survey date, and source in a metadata panel and in per-feature popups; and if the data carries a confidence attribute, surface it (e.g. opacity or a hatch). Generalising geometry for tile performance is fine, but label the map with its source scale so users do not over-trust smoothed boundaries.
Common pitfalls and why they happen
- Legend/map colour drift. Cause: hand-authored legend HTML separate from the style. Derive swatches from the paint expression instead.
- Static legend on an interactive map. Cause: porting a print legend unchanged. Wire entries to
setFilter/setLayoutProperty. - Raw GeoJSON to the browser. Cause: skipping the tiling step. Pre-tile large layers; the browser should never parse the whole dataset at once.
- 3D for its own sake. Cause: choosing Cesium before the question needs depth. Default to 2D; add 3D only when occlusion and terrain are the message.
- Hidden metadata. Cause: a clean UI that buries source and scale. A geological map with no visible provenance invites misuse.
QA and validation
Before publishing, confirm: every rendered unit has a legend entry and vice versa (no orphan colours); swatches match on-map colours under the same opacity; filtering a unit and resetting returns to the exact initial state; the metadata panel cites source, scale, and date; and the map stays responsive at realistic data volume on a mid-range device, not just on your workstation with a small sample.
Bathyl perspective
A Bathyl interactive geological map is built so that nothing on screen is unattributable: every colour traces to a unit code, every unit to a source, and every inferred boundary looks inferred. The interface should lower the effort of inspection while preserving the scientific caveats that make the underlying mapping trustworthy.
Related reading
- Designing Layer Toggles for Earth Data
- Map UX for Technical Decision Makers
- Preparing GIS Data for Web Visualization
- Mapping systems