Short answer

A metadata panel for a geological feature is the part of a web map that answers "what is this, where did it come from, and how much should I trust it" the moment a user clicks a polygon or line. For geology that means surfacing the map unit code, lithology, age, contact or fault type, the source map and its publication scale, and an explicit confidence value. The hard part is not the click handler; it is carrying the right attributes through tiling and presenting confidence honestly so a 1:250,000 inferred contact is never read as a surveyed boundary.

This guide covers what to put in the panel, how to wire it in MapLibre GL JS and deck.gl, and how to keep source and confidence intact from the database to the browser.

What belongs in a geological metadata panel

A geological map carries far more information than a fill color, and the panel is where that information becomes inspectable. Anchor the panel on the same fields a geologist would expect in a printed legend and a GeMS-style attribute table.

For polygon (map unit) features, show:

  • MapUnit code and the full unit name (e.g. Qal — "Quaternary alluvium").
  • Lithology and depositional/igneous origin in plain language.
  • Age, ideally both the period and a numeric range where known.
  • The DescriptionOfMapUnits text, which is the authoritative unit description.
  • Source citation: the map title, author, year, and publication scale.

For line features (contacts, faults, folds), show:

  • Feature type (depositional contact, fault, thrust, anticline axis).
  • ExistenceConfidence and IdentityConfidence — is the structure certainly there, and is its identity certain?
  • LocationConfidenceMeters — the horizontal uncertainty of the line position, a numeric field in the USGS GeMS schema that is genuinely useful in a panel.
  • Whether the line is mapped, approximate, inferred, or concealed.

The single most important discipline is to never present an interpreted boundary as if it were surveyed. A contact digitized from a 1:100,000 map has a real-world positional uncertainty on the order of a millimetre at map scale, i.e. roughly 100 m on the ground. If your panel shows the source scale and the location-confidence value, the user can reason about that; if it hides them behind a clean fill color, they cannot.

Building the panel in MapLibre GL JS

MapLibre separates sources (the data), layers (style rules over a source), and interaction (event handlers). The metadata panel is interaction logic reading the properties that the source already carries.

A click handler bound to a specific layer is the simplest pattern:

map.on('click', 'geology-units', (e) => {
  const f = e.features[0];
  const p = f.properties;
  showPanel({
    unit:        p.MapUnit,
    name:        p.Label,
    age:         p.Age,
    description: p.Description,
    source:      p.Source,
    scale:       p.SourceScale,   // e.g. "1:100,000"
  });
});

map.on('mouseenter', 'geology-units', () => {
  map.getCanvas().style.cursor = 'pointer';
});

When several layers overlap — a fault drawn over a unit polygon, say — e.features[0] only reports the top layer. Use queryRenderedFeatures to collect everything under the cursor and let the panel group them:

map.on('click', (e) => {
  const hits = map.queryRenderedFeatures(e.point, {
    layers: ['geology-units', 'contacts', 'faults'],
  });
  showPanelForFeatures(hits);
});

Prefer a docked side panel over a balloon popup for geology. Unit descriptions and source citations run long, and a side panel lets the user keep the feature highlighted while reading. Highlight the selected feature by setting a feature-state and driving the paint expression from it:

map.setPaintProperty('geology-units', 'fill-outline-color', [
  'case', ['boolean', ['feature-state', 'selected'], false],
  '#000', 'rgba(0,0,0,0.2)'
]);

This requires that your source features carry a stable id (the promoteId option on a vector source promotes an attribute to the feature id), which is also what you need to fetch full metadata on demand.

The vector tiles vs GeoJSON decision

The format you choose changes what the panel can show without a network round trip.

GeoJSON keeps every attribute available client-side. For a study area with a few hundred to a few thousand units this is the simplest path: the full Description and source fields are already in e.features[0].properties. Beyond roughly 5–10 MB of GeoJSON, initial load and memory start to hurt.

Vector tiles (Mapbox Vector Tiles, served as .pbf or packaged as PMTiles) scale to national datasets, but tiling is lossy for attributes: tile generators commonly drop long text fields, and geometry is simplified per zoom level. The robust pattern is to tile only the lightweight fields needed for styling plus a stable feature_id, then on click fetch the full record from an OGC API - Features endpoint or a small JSON API:

map.on('click', 'geology-units', async (e) => {
  const id = e.features[0].properties.feature_id;
  const r = await fetch(`/api/units/${id}`);
  showPanel(await r.json());
});

When you generate tiles with tippecanoe, keep attributes deliberately rather than letting the default drop them:

tippecanoe -o geology.pmtiles -Z6 -z14 \
  --include=MapUnit --include=feature_id \
  --drop-densest-as-needed geology.geojson

deck.gl follows the same logic. Its GeoJsonLayer or MVTLayer exposes the clicked feature through onClick: ({object}) => ..., where object.properties holds the attributes. deck.gl is the better choice when you need GPU-scale rendering (hundreds of thousands of features, extruded 3D units, or large point clouds) and can sit on top of a MapLibre base map via the interleaved or overlaid integration.

Worked example: a contact line that is honest about itself

Suppose a fault is digitized from a 1:250,000 USGS quadrangle and stored in PostGIS with GeMS attributes. The panel should render:

  • Type: Normal fault
  • Existence confidence: certain
  • Identity confidence: questionable
  • Location confidence: 250 m
  • Source: Smith, 1998, 1:250,000

Drive the line style from the confidence so the cartography and the panel agree:

'line-dasharray': [
  'case',
  ['==', ['get', 'ExistenceConfidence'], 'inferred'], ['literal', [2, 2]],
  ['literal', [1]]
]

A user who zooms to 1:10,000 now sees a dashed line, reads "location confidence 250 m" in the panel, and understands the line could be a quarter-kilometre off. That is the whole point: the interface scales the user's confidence to the data's confidence.

QA and validation

  • Round-trip a known feature. Pick a unit whose description you know from the source map and confirm the panel reproduces it exactly, including the source scale.
  • Test attribute survival through tiling. After generating tiles, click features at min and max zoom and confirm the styling fields and feature_id are present at every level.
  • Check overlap behaviour. Click where a fault crosses a contact inside a unit and confirm the panel reports all three, not just the topmost.
  • Verify null handling. Features with missing Age or LocationConfidenceMeters should show "not recorded", never a blank or undefined.

Common pitfalls and why they happen

  • Attributes vanish at high zoom. The tile generator dropped them; tiling is lossy by default, so you must explicitly include the fields you query.
  • The panel implies false precision. Source scale and confidence were left out of the schema, so the interface presents a 1:250,000 line as if it were GPS-surveyed.
  • Popups truncate descriptions. Long DescriptionOfMapUnits text needs a scrollable side panel, not a fixed-width balloon.
  • Highlighting fails silently. feature-state needs a stable feature id; without promoteId the selection never sticks.

Bathyl perspective

We treat the metadata panel as the contract between the cartographer and the reader. Every geological feature we publish carries its source citation, publication scale, and a confidence value, and the panel renders them without the user having to ask. A map that hides where a line came from is a picture; a map that exposes it is evidence.

Related reading

Sources