Angular
ngx-apexmaps is a standalone Angular component around the imperative ApexMaps class. It is zoneless-ready: the map is built, rendered, and updated outside the Angular zone, and its outputs re-enter the zone to emit, so the same code repaints correctly in zoned and zoneless applications alike.
Installation
npm install apexmaps ngx-apexmaps
apexmaps and @angular/core (>=20.0.0) are the only peer dependencies. ngx-apexmaps 1.2.0 needs apexmaps 1.2.0 or later. It takes the current apexmaps 1.3.0 as it is. There is no @angular/common requirement, no rxjs requirement of its own, no zone.js requirement, and no NgModule.
To follow an Ignite UI theme, import the core's add-on once in the app, import 'apexmaps/ignite-ui', not anything from the wrapper. theme: { designSystem: false } in the options turns it off for one map. See Using with Ignite UI.
Usage
import { Component, signal } from '@angular/core'
import { ApexMapsComponent } from 'ngx-apexmaps'
import 'apexmaps/apexmaps.css'
@Component({
standalone: true,
imports: [ApexMapsComponent],
template: `
<apx-map
[options]="options"
[series]="series()"
[height]="480"
(featureClick)="selected.set($event.key)"
/>
`,
})
export class CoverageComponent {
readonly options = { geo: { map: 'world' } }
readonly series = signal([
{
type: 'choropleth',
name: 'Coverage',
data: [{ key: 'IND', value: 42 }],
scale: { palette: 'blues', classes: 5 },
},
])
readonly selected = signal<string | null>(null)
}
Inputs and outputs
| Input | Type | Notes |
|---|---|---|
options | ApexMapsOptions | Required signal input. The same options object the core takes. |
series | Series[] | Optional signal input, shorthand for options.series. Takes precedence over it; both routes reach updateSeries(), so both tween. |
width / height | number | string | Signal inputs, shorthand for options.chart.width / .height. See Sizing below. |
One output per core event, under the core's own name: rendered, updated, resized, featureClick, featureHover, featureFocus, markClick, markHover, clusterClick, drilldown, drillup, selectionChange, legendToggle, zoom, panEnd, rotate, rotateEnd, viewChange. Each carries the payload the core emits, fully typed; see API: Methods and Events for every payload shape.
The zone contract
- The map runs outside the Angular zone. A map attaches pointer listeners to everything it draws, and zone.js patches
addEventListener, so a map built inside the zone would run change detection on every pointer movement over every feature. Construction, rendering, and updates all happen outside the zone. - Outputs are emitted back inside the zone. Events originate from unpatched listeners, which zone-based change detection cannot see on its own, so the component calls
NgZone.run()around each emit. A(featureClick)handler that sets component state then repaints, whether the app is zoned or zoneless. Under zoneless change detection,NgZonedegrades to a no-op and both calls collapse to plain function calls, so the same code is correct either way. viewChangeis the exception: it arrives outside the zone. It fires on every pan and zoom frame, and running it inside the zone would put a change-detection pass on every frame of every pan, whether anything is bound to it or not. Write a signal in the handler: a signal write schedules change detection on its own, so the view still follows.
@Component({
standalone: true,
imports: [ApexMapsComponent],
template: `
<apx-map [options]="options" (viewChange)="scale.set($event.k)" />
<p>Zoom: {{ scale() }}x</p>
`,
})
export class ViewReadout {
readonly options = { geo: { map: 'world' } }
readonly scale = signal(1)
}
Measured against the built package: 30 pointer moves over the map produce zero change-detection passes; one click produces one repaint.
The imperative API
Camera moves, drilldown, export, and diagnostics are methods, not options. The component exposes the live instance as a signal, reachable through a template variable on the element:
@Component({
template: `<apx-map #map [options]="options" />
<button (click)="map.map()?.frameFeature('IND')">Zoom to India</button>`,
imports: [ApexMapsComponent],
standalone: true,
})
export class ZoomDemo {
readonly options = { geo: { map: 'world' } }
}
map() is null until the first render completes and after destroy, so an effect() on it is the way to run something exactly once the map exists.
Change detection
Bindings are compared deeply, not by reference, because a template expression like [options]="build()" hands over a brand new object on every change-detection cycle. A fresh but equal tree is not a redraw. An inline formatter is compared by source rather than identity, and geo.map is compared by identity and never walked, so pass a stable reference or a registry id string for it. A series-only change routes to updateSeries(), which tweens; everything else routes to updateOptions(), which redraws.
Signal inputs do not fire on in-place mutation, so prefer replacing objects rather than mutating them. If some other input changes afterward, an earlier in-place mutation is picked up along with it rather than lost.
Sizing
Use the width and height inputs, not CSS. The map's height comes from options.chart.height, which defaults to 400; an explicit number wins over the container. Width defaults to '100%' and follows the container. height="100%" hands the height to the container:
<apx-map [options]="options" height="100%" style="height: 60vh" />
<apx-map> itself is display: block by default, and it is the element your class and style bindings land on. The element inside it belongs to the map.
SSR
Safe under Angular SSR and hydration: the component renders its container on the server and builds the map in afterNextRender, which only ever runs in the browser. There is no server-rendered map markup yet, because the core has no HTML output path.