Commands

Every command is a method on the handle, takes one argument object (or none) and returns a promise. This page walks through them by area with the patterns that come up in real portals. For the exact argument and result types, see the API reference.

Each area is a group the map's author can switch off. If a group is off its commands are missing from capabilities.commands and reject with denied_capability.

Camera

const view = await map.getView();
// { center: [lng, lat], zoom, bearing, pitch, bounds: [[west, south], [east, north]] }

await map.setView({ center: [-0.12, 51.5], zoom: 11 });              // jumps
await map.setView({ zoom: 13, animate: true });                      // eases; omitted fields stay put
await map.flyTo({ center: [2.35, 48.85], zoom: 9, durationMs: 2500 });
await map.fitBounds({ bounds: [[-5.4, 51.3], [-2.6, 53.5]], padding: 40 });
await map.fitLayer({ layerId: "depots", padding: 60, maxZoom: 14 }); // the layer's data as currently filtered
await map.zoomBy({ delta: 1 });
await map.setMaxBounds({ bounds: [[-8, 49], [2, 61]] });             // null removes the limit

Coordinates are always [longitude, latitude]. padding is a number of pixels or { top, right, bottom, left } - use it when part of the map sits under your own overlay.

fitBounds and fitLayer keep the camera's pitch unless you say where to land. After a pitched flyTo, pass pitch: 0, bearing: 0 for a flat, north-up frame:

await map.fitLayer({ layerId: "depots", padding: 60, pitch: 0, bearing: 0 });

If you resize the element the map lives in, tell it:

new ResizeObserver(() => map.resize()).observe(container);

Layers and visibility

const layers = await map.getLayers();
// [{ id, name, kind, visible, groupId?, legendVisible?, hasData }]

await map.setLayerVisibility({ layerId: "depots", visible: false });

Layers are only one of the things a reader can toggle. getVisibility returns all of them, and setVisibility takes any subset - what you leave out is untouched:

await map.setVisibility({
  layers: { depots: false, routes: true },
  labels: false,                       // basemap place labels
  hillshade: true,
  visuals: { "chart-sales": false },   // charts, slicers, text boxes ... by visual id
});

await map.resetVisibility();           // back to what the author published

Ids a map no longer has are skipped and returned as { skipped: [...] }, not treated as an error, so code written against one publish keeps working against the next.

:::note Hiding a slicer switches it off Visibility works the way it does in a bookmark: a hidden slicer is off, so its selection stops filtering until it is shown again. If your page supplies the filter UI and the map should show no slicer cards, filter with declared filters (setFilters) rather than by setting hidden slicers. :::

getLayerStats({ layerId }) tells you how many features a layer holds after filters and slicers - handy for an "n results" label.

:::note Layers that are switched off when the map is published A published map carries the data for layers that are switched off too, so your page can reveal one with setLayerVisibility and the map's own layer control and bookmarks work as the author designed them. The author can leave a hidden layer out - Publish > Include data for the layers that are switched off - and such a layer is published empty: turning it on shows nothing and getLayerStats reports 0.

Maps published before this was added (September 2026) carry no data for layers that were off at the time. Publishing again includes it. :::

Visibility is the whole of it. The API cannot restyle, recolour or reorder a layer. To present data differently, the author publishes it differently.

Slicers

Slicers are the reader-facing controls the author placed on the map. Driving one is exactly the same as the reader using it: everything bound to it follows.

const slicers = await map.getSlicers();
// [{ id, name, kind, field, state }]

const { values } = await map.getSlicerValues({ slicerId: "slicer-region", search: "wa" });
await map.setSlicerState({ slicerId: "slicer-region", state: { values: ["Wales"] } });
await map.clearSlicer({ slicerId: "slicer-region" });
await map.clearAllSlicers();

The shape of state depends on the slicer's kind, which getSlicers() reports. null clears a slicer.

getSlicerValues answers for a hidden slicer too, so a page that draws its own filter UI can still list the values.

Slicer kind State
singleSelect, multiSelect, buttons { values: ["Wales", "Scotland"] }
numericValue { value: 42 }
numericRange { range: [10, 50] }
date { date: "2026-03-01" }, { dateRange: ["2026-01-01", "2026-03-31"] } or { relativeDate: { count: 30, unit: "days" } }
timeRange { timeRange: ["08:00", "18:00"] }
toggle { toggled: true }

See it working: Filter with slicers.

For filters that are yours rather than the reader's, use declared filters instead.

Selection and highlight

The pattern most portals want is a list on the page and a map beside it, each driving the other.

From the map to your page - subscribe to featureClick. It carries the feature's key and the fields the layer shows in its tooltip:

map.on("featureClick", (f) => f && list.scrollTo(f.featureKey));

From your page to the map - selectByKey. With field, you pass your identifiers and the map finds the features; without it, the keys are the featureKey values featureClick gave you.

await map.selectByKey({ layerId: "assets", field: "asset_id", keys: ["41B", "41C"], zoomTo: true });
// -> { matched: 2 }

Selecting behaves as a click does: the feature is emphasised, panels and charts scoped to the selection follow, and selectionChanged fires.

:::note The key column has to be published A published map carries only the columns it uses - tooltips, styling, labels, slicers - so a key column that nothing else uses is not in it, and selectByKey with that field matches nothing ({ matched: 0 }). The map's author publishes a key by adding it to the layer's tooltip fields. The same applies to highlight. :::

Hover without selecting - highlight paints features as selected without changing the selection: no popup, no panels, no event. It is made for list hover.

row.onmouseenter = () => map.highlight({ layerId: "assets", keys: [row.dataset.key] });
row.onmouseleave = () => map.clearHighlight();

A reader's next click on the map replaces a highlight. One call may name up to 500 keys.

Charts cross-filter the map when a reader clicks a bar or slice. You can do the same:

await map.setChartSelection({ chartId: "chart-sales", datums: [{ category: "North" }] });
await map.clearChartSelection();

Scenes, bookmarks and tours

captureScene and applyScene save and restore the whole view in one object. They have their own page.

Presentation

Make the map look like part of your application.

const basemaps = await map.getBasemaps();          // the ones the author published
await map.setBasemap({ basemapId: basemaps[1].id });

await map.setTheme({ mode: "dark" });              // "light" | "dark" | "auto"
await map.setBackground({ background: "transparent" });

await map.setChrome({ legend: false, layerControl: false, bookmarksBar: false });

:::note A transparent background on a dark page background: "transparent" lets your page show through wherever the map does not paint - the space around a globe, for example. If your page declares color-scheme: dark, also give the frame the map's scheme:

#map iframe { color-scheme: light; }

When a frame's colour scheme differs from its page's, browsers paint an opaque backdrop behind the frame, so without this the background stays white and the option appears to do nothing. :::

setChrome can hide a control the author configured; it cannot add one they did not. The switches are legend, layerControl, clickPopups, selection, bookmarksBar, tourTransport and search.

If you hide the legend, build your own from the same model the map renders:

const legend = await map.getLegend();
// [{ layerId, title, sections: [{ caption, items: [{ kind, label, color, stops? ... }] }] }]

If you hide the map's own attribution, you take on showing it. getMapInfo() returns the attribution strings the map owes.

Language. setLanguage({ language: "de" }) switches the reader language. Only a language listed in capabilities.map.locales is accepted.

Full screen runs on your page rather than in the map, because browsers only allow it in response to a user gesture there. Call it from a click handler:

button.onclick = () => map.fullscreen();

Image export and print

const image = await map.exportImage({ width: 1600 });            // PNG by default
// { dataUrl, width, height, format }

const jpeg = await map.exportImage({ format: "jpeg", quality: 0.85 });
await map.print();

The image is the map exactly as the reader sees it at that moment. Height follows the frame's aspect ratio unless you give one.

If any data behind the map carries a sensitivity label that does not allow export, both commands reject with denied_capability. When a label allows export with a warning, the result carries labelVerdict: "warn" so you can show your own notice. See Security & permissions for how Icon Map treats sensitivity labels.

Popup menu commands

Add your own action to the popup a reader sees when they click a feature:

await map.addMenuCommand({ id: "raise-job", title: "Raise a job for this asset", layerIds: ["assets"] });

map.on("menuCommandTriggered", ({ commandId, layerId, featureKey, fields }) => {
  if (commandId === "raise-job") openJobForm(fields.asset_id);
});

Only the id and a plain-text title cross into the map - never HTML or script. Leave layerIds out to offer the action on every layer.

Analysis

Some maps carry an analysis engine on a layer - a water network that can trace from a burst main, for example. Where one exists, you can run it and receive its results.

const ops = await map.getAnalysisOps();
// [{ op: "traceAt", layerId: "network", kind: "waterNetwork", description, args: { lng: "number", lat: "number" } }]

const { result, truncated } = await map.runAnalysis({
  op: "traceAt",
  layerId: "network",
  args: { lng: -3.66, lat: 51.62 },
});

The operations and their arguments belong to the engine, so discover them with getAnalysisOps() rather than hard-coding them. On a map without an engine it returns an empty list.

Whether you ran the operation or the reader did it by clicking, analysisResult fires with the same payload, so one handler covers both:

map.on("analysisResult", ({ op, result, reason }) => {
  if (op === "traceAt") customerList.replace(result.affectedServiceIds);
});

Analysis is rationed so a page cannot overwhelm the map: one operation runs per layer at a time, four more may wait (busy after that), and a session gets 60 a minute (budget_exceeded). Large results are capped - 1,000 ids or 200 rows per list - and marked truncated.

Layers with a simulation clock expose it through getClock / setClock and the clockChanged event.