Skip to content

Adding Layers

Your data layers live in app/layers.ts, which exports a layers array that settings.ts imports as settings.layers. (It's a separate file purely to keep settings.ts readable — everything downstream treats it as one array.)

Each entry is a typed object with a type discriminator that selects the loader:

import type { LayerConfig } from '@/core/types'

export const layers: LayerConfig[] = [
  {
    id: 'parks',
    type: 'geojson',
    name: 'National Parks',
    geometryType: 'polygon',
    url: './layers/parks.geojson',
    symbology: { kind: 'simple', color: '#2c8a3e' },
  },
  // …
]

Order matters: it sets the layer-list display order, and groups are positioned by the first appearance of their group name.

Common fields (every layer type)

Field Type Default Description
id string — (required) Unique id (must be unique across all layers)
name string — (required) Display name in the layer list and legend
type 'geojson' \| 'pmtiles' \| 'flatgeobuf' \| 'esri' \| 'geoparquet' — (required) Selects the loader
symbology Symbology How to draw it (see Symbology). Required for most types; optional for Esri
group string none Group layers under a shared header in the layer list. Same name = same group
visible boolean true Initial visibility
opacity number (0–1) 1 Initial opacity
minZoom / maxZoom number MapLibre defaults Zoom range at which the layer renders (distinct from the camera clamp in Map & Camera)
geometryType 'point' \| 'line' \| 'polygon' none Drives the legend swatch shape; required for vector-tile types
label LabelConfig none Text labels (see Labels)
popup PopupConfig none (off) Feature popups (see Popups)
hideFromLegend boolean false Keep the layer on the map + in the layer list, but out of the legend (works for any symbology kind)
actions { opacity?, labels? } global default Per-layer override of the layer-list row action buttons

Where files live

  • Local files go in app/layers/. Reference them as './layers/<file>'. Vite resolves the path at build time. Supported extensions: .geojson, .pmtiles, .fgb, .parquet / .geoparquet.
  • Remote URLs (http:// / https://) are used as-is. Remote sources must be CORS-enabled.

GeoJSON file-size reality

A geojson source loads and parses the entire file into memory on the main thread — there's no streaming. Rough thresholds: under 5 MB is fine; 5–20 MB is noticeable startup lag; over ~50 MB isn't viable. minZoom controls rendering, not the parse. For large data, use PMTiles, FlatGeobuf (dynamic), or a tiled Esri service.


geojson

The simplest type. One source, automatically split into fill / line / circle sublayers filtered by geometry — so a single file can mix points, lines, and polygons.

Field Type Description
url string Path to a .geojson file (./layers/…) or a remote URL
{
  id: 'airports',
  type: 'geojson',
  name: 'US Major Airports',
  geometryType: 'point',
  url: 'https://example.com/airports.geojson',
  symbology: { kind: 'ref', name: 'airports-by-passengers' },
}

pmtiles

PMTiles vector tiles — a single cloud-native archive that streams only the tiles in view. Ideal for large datasets.

Field Type Description
url string Path to a .pmtiles archive (local or remote)
sourceLayer string The layer name inside the archive (required)
geometryType 'point' \| 'line' \| 'polygon' Required (vector tiles carry no geometry hint)
{
  id: 'counties',
  type: 'pmtiles',
  name: 'US Counties',
  geometryType: 'polygon',
  url: './layers/US_counties.pmtiles',
  sourceLayer: 'US_counties',
  symbology: { kind: 'simple', color: '#838291', strokeColor: '#fff', strokeWidth: 1 },
}

flatgeobuf

FlatGeobuf (.fgb) — a streaming vector format. Decoded into a GeoJSON source, so it flows through the same sublayer / popup / legend pipeline.

Field Type Default Description
url string Path to a .fgb file (local or remote)
bbox 'all' \| 'dynamic' 'all' Loading strategy
  • 'all' reads the whole file once into memory — fine for small/medium files (same ceiling as GeoJSON).
  • 'dynamic' fetches only the features in the current viewport via the file's spatial index (HTTP Range requests), refetched on map move — the way to use a large remote .fgb (hundreds of MB / GB) without downloading it whole.

Dynamic mode needs an uncompressed, Range-friendly host

dynamic reads byte ranges, so the host must serve the file uncompressed over Range requests. Hosts that gzip responses (e.g. GitHub Pages) return a 206 the browser can't decode, and dynamic mode fails. S3 / object stores / most CDNs are fine.

esri

ArcGIS REST services via esri-gl. A service discriminator picks the family. esri-gl creates the source; the kit adds the styled layer(s).

service Source kind Capabilities
'feature' GeoJSON (tile-indexed, queried per-viewport) Full client styling + popups + legend
'dynamic' Server-rendered raster (export image) Visibility + opacity only; no legend, no popups
'tiled' Pre-cached raster tiles Visibility + opacity only

Feature service

{
  id: 'buoys',
  type: 'esri',
  service: 'feature',
  name: 'NDBC Buoys',
  geometryType: 'point',
  url: 'https://services5.arcgis.com/.../FeatureServer/0',
  minZoom: 4,
  symbology: { kind: 'simple', color: '#a5d7f7', radius: 5, strokeColor: '#fff', strokeWidth: 1 },
  popup: { displayType: 'rich' },
}

For feature services, symbology is optional: provide it for kit styling, or omit it to use the service's own ArcGIS renderer. Extra fields: where (an ArcGIS where clause, default '1=1').

Raster services

{
  id: 'usa-dynamic', type: 'esri', service: 'dynamic',
  name: 'USA States/Counties',
  url: 'https://sampleserver6.arcgisonline.com/.../USA/MapServer',
  layers: [2, 3],   // dynamic only: which MapServer sublayers to draw
  opacity: 0.7,
}

dynamic takes an optional layers (sublayer ids). tiled takes only the common fields. Both are visibility + opacity only.

geoparquet

GeoParquet — parsed in a Web Worker via parquet-wasm + apache-arrow. The worker decodes the WKB geometry to GeoJSON and feeds a GeoJSON source.

Field Type Description
url string Path to a .parquet / .geoparquet file (CORS-enabled if remote)
limit number Optional cap on features to load (the reader stops early). Omit to read the whole file

Optional dependencies

parquet-wasm and apache-arrow are optional dependencies — only loaded when a geoparquet layer is used. If they're missing, the loader logs a clear npm install parquet-wasm apache-arrow message and the rest of the map keeps working.

There's no viewport bbox filtering for GeoParquet yet — for large files, set a limit or pre-filter the file.


Labels

Any vector layer can carry text labels via a label block. Rendered as a companion MapLibre symbol sublayer.

Field Type Default Description
field string Column to label by (compiles to ['get', field])
expression ExpressionSpecification A raw MapLibre expression for text-field (wins over field). Combine columns, e.g. ['concat', ['get', 'name'], ' (', ['get', 'code'], ')']
font string 'Noto Sans Regular' Must be served by the basemap's glyphs URL (see note)
fontSize number 12 Text size in px
color string '#333333' Text color
haloColor string '#ffffff' Outline color for legibility
haloWidth number 1 Halo width (0 = none)
minZoom number layer's range Zoom floor for labels alone, so geometry can show before its labels
anchor 'center' \| 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' 'center' Label position relative to the feature
allowOverlap boolean false Allow labels to overlap (MapLibre drops colliding labels by default)

Fonts must be served by the basemap

MapLibre draws labels on the WebGL canvas, so the font must be in the active basemap style's glyphs URL. All OpenFreeMap styles (the defaults) serve Noto Sans Regular / Bold / Italic. The raw OSM raster basemap has no glyphs URL, so labels won't render over it.

Set app-wide label defaults once via ui.layerDefaults.label.

Hiding a layer from the legend

Set hideFromLegend: true on any layer to keep it on the map and in the layer list but omit it from the legend. (This works for every symbology kind, unlike the raw-symbology legend: { kind: 'hidden' } which is raw-only — see Symbology.)