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. Registering a custom projection is the one extension point that is licensed, because it is cartography beyond the 13 built-in projections rather than a data or styling extension. See Projections for ApexMaps.registerProjection() and the licensing rationale.
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