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

FieldDescription
sourceFree text, e.g. 'Natural Earth 5.1.1'.
licenseFree text, e.g. 'public domain'.
attributionText to display. Leave empty for public-domain sources.
vintageBoundary vintage, e.g. '2026'.
detail'low', 'medium', or 'high'.
boundariesThe disputed-territory policy this file encodes, recorded rather than decided.
keyFieldThe 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 fieldDescription
kind'sequential', 'diverging', or 'categorical'.
stopsAnchor colors for the ramp.
colorblindSafeMarks 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