Extending ApexMaps
Nothing about the engine assumes the earth. ApexMaps.registerMap() puts any geometry into the registry under an id, with the same provenance fields the 26 built-in packs carry, so a floor plan, a stadium, or a wafer map works exactly the way a country does. Registering your own geometry and your own palette is a free Community feature.
Register custom geometry
ApexMaps.registerMap(id, geometry, meta) accepts a GeoJSON FeatureCollection (or a loader function returning one) and an optional metadata object.
const zone = (id, name, x, y, w, h) => ({
type: 'Feature',
properties: { id, name },
geometry: {
type: 'Polygon',
coordinates: [[[x, y], [x, y + h], [x + w, y + h], [x + w, y], [x, y]]],
},
})
ApexMaps.registerMap(
'demo/floor',
{
type: 'FeatureCollection',
features: [
zone('A', 'Assembly', 0, 0, 40, 30),
zone('B', 'Paint', 40, 0, 25, 30),
// ...
],
},
{
source: 'hand-drawn floor plan',
license: 'n/a',
vintage: '2026',
keyField: 'id',
levelName: 'Zones',
},
)
Draw it with the identity projection, since the coordinates are already a flat plan rather than longitude and latitude:
const floor = new ApexMaps(document.getElementById('floor'), {
geo: { map: 'demo/floor', projection: 'identity', keyField: 'id' },
series: [{ name: 'Utilisation', joinBy: ['id', 'key'], data: rows }],
})
await floor.render()
Metadata fields
| Field | Description |
|---|---|
source | Free text, e.g. 'Natural Earth 5.1.1'. |
license | Free text, e.g. 'public domain'. |
attribution | Text to display. Leave empty for public-domain sources. |
vintage | Boundary vintage, e.g. '2026'. |
detail | 'low', 'medium', or 'high'. |
boundaries | The disputed-territory policy this file encodes, recorded rather than decided. |
keyField | The recommended join key for this geometry, e.g. 'id'. |
Look it up later with ApexMaps.mapMeta(id), the same call the built-in registry answers.
Register a custom palette
ApexMaps.registerPalette(name, palette) adds a ramp to the registry the same way a built-in one is defined: anchor stops sampled in OkLab at render time, so a 4-class and a 9-class map of the same data stay perceptually consistent.
ApexMaps.registerPalette('demo/heat', {
kind: 'sequential',
stops: ['#f7f4ff', '#d9c7ff', '#b18aff', '#7d4bd6', '#4b1d96'],
colorblindSafe: true,
})
Use it exactly like a built-in name:
series: [{ type: 'choropleth', name: 'Adoption', data, scale: { palette: 'demo/heat' } }]
palette field | Description |
|---|---|
kind | 'sequential', 'diverging', or 'categorical'. |
stops | Anchor colors for the ramp. |
colorblindSafe | Marks the palette as colorblind-checked, for a picker UI to filter on. |
Point the registry at your own geometry source
By default, built-in packs are fetched from jsDelivr. ApexMaps.setGeoSource() redirects every future pack request, either to a base URL or through a loader function, which is the same feature whether you are self-hosting, running air-gapped, or pulling packs out of a bundler import.
// A base URL: self-hosted or a private CDN
ApexMaps.setGeoSource('https://cdn.example.com/apexmaps-geo/')
// A loader function: bundler imports, air-gapped paths, an authenticated fetch
ApexMaps.setGeoSource((file) => import(`apexmaps-geo/${file}`).then((m) => m.default))
The function form receives the pack's filename and must return the parsed geometry (or a Promise of it). Every request, from every map on the page, goes through the one function you registered:
let served = 0
ApexMaps.setGeoSource(async (file) => {
const response = await fetch(`/geo/${file}`)
if (!response.ok) throw new Error(`HTTP ${response.status} for ${file}`)
served++
return response.json()
})
The apexmaps-geo npm package ships the same 26 packs for install-time or offline resolution, versioned independently of the library, so a boundary correction does not require a library upgrade.
What stays licensed
Registering your own geometry, your own palette, and your own geo source are all free. Two extension points are licensed, and in both the gate is on rendering, not on registering: the call itself always succeeds, and drawing a map that uses what it registered is what needs a key.
- A custom projection, because it is cartography beyond the 13 built-in projections rather than a data or styling extension. See Projections for
ApexMaps.registerProjection(). - A grid layout, because the gate is on the representation rather than on whose table it came from. See Hex Tile Layouts for
ApexMaps.registerLayout().
Look up what is registered
ApexMaps.listMaps() // every registered map id, built-in and custom
ApexMaps.listPalettes() // every registered palette name, built-in and custom
ApexMaps.mapMeta('demo/floor')
ApexMaps.catalogue() // built-in packs only, with full provenance