<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Interactive legend with LegendModel | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.2/"></script>
inline-size: max-content;
<arcgis-map item-id="59a3db1da7744edb9c1818b74d1e954d">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<calcite-shell-panel slot="panel-end">
<calcite-panel scale="s" heading="Legend">
<calcite-notice open closable icon="information" kind="info" scale="s" width="full">
Select one or more legend items to show only the corresponding features. If no items are selected for a
layer, all features in that layer are shown.
<div id="legend-sections"></div>
const [LegendModel, FeatureFilter, reactiveUtils, sqlUtils] = await $arcgis.import([
"@arcgis/core/legend/LegendModel.js",
"@arcgis/core/layers/support/FeatureFilter.js",
"@arcgis/core/core/reactiveUtils.js",
"@arcgis/core/core/sql.js",
// Get the map component and the container for generated legend sections.
const viewElement = document.querySelector("arcgis-map");
const legendSections = document.getElementById("legend-sections");
// Keep category selections independent for each layer.
const layerSelections = new Map();
// Wait for the view to be ready before creating its legend model.
await viewElement.viewOnReady();
const legendModel = new LegendModel({
const renderedLayerIds = new Set();
// Re-render legend when layer readiness changes; renderLegend adds each ready layer once.
() => legendModel.activeLayerInfos.map((activeLayerInfo) => activeLayerInfo.ready),
function renderLegend() {
for (const activeLayerInfo of legendModel.activeLayerInfos) {
// Skip layers whose legend is not ready or has already been rendered.
if (!activeLayerInfo.ready || renderedLayerIds.has(activeLayerInfo.layer.uid)) {
// Create a separate multi-select list for each layer.
const block = document.createElement("calcite-block");
block.heading = activeLayerInfo.title;
const list = document.createElement("calcite-list");
list.label = `${activeLayerInfo.title} legend items`;
list.selectionMode = "multiple";
// Add a heading action to clear this layer's selections.
const clearButton = document.createElement("calcite-button");
clearButton.slot = "content-end";
clearButton.appearance = "transparent";
clearButton.label = `Clear selections for ${activeLayerInfo.title}`;
clearButton.textContent = "Clear Selection";
clearButton.hidden = true;
for (const legendInfo of activeLayerInfo.legendElements) {
appendLegendInfo(legendInfo, activeLayerInfo, list, clearButton);
block.append(list, clearButton);
legendSections.appendChild(block);
renderedLayerIds.add(activeLayerInfo.layer.uid);
clearButton.addEventListener("click", async () => {
for (const listItem of list.querySelectorAll("calcite-list-item")) {
listItem.selected = false;
updateFilter(activeLayerInfo, new Set());
clearButton.hidden = true;
function appendLegendInfo(legendInfo, activeLayerInfo, list, clearButton) {
if (legendInfo.preview) {
const whereClause = getCategoryWhereClause(activeLayerInfo, legendInfo);
const listItem = document.createElement("calcite-list-item");
listItem.label = legendInfo.label ?? "Legend item";
listItem.addEventListener("calciteListItemSelect", () => {
// Copy this layer's selection, then add or remove the toggled category.
const selectedClauses = new Set(layerSelections.get(activeLayerInfo.layer.uid));
selectedClauses.add(whereClause);
selectedClauses.delete(whereClause);
updateFilter(activeLayerInfo, selectedClauses);
clearButton.hidden = selectedClauses.size === 0;
const symbol = document.createElement("div");
symbol.className = "legend-symbol";
symbol.slot = "content-start";
// Clone the preview so the model retains its original node.
symbol.appendChild(legendInfo.preview.cloneNode(true));
listItem.appendChild(symbol);
list.appendChild(listItem);
if ("infos" in legendInfo) {
// Add nested categories to the same layer list.
for (const childInfo of legendInfo.infos) {
appendLegendInfo(childInfo, activeLayerInfo, list, clearButton);
function getCategoryWhereClause(activeLayerInfo, legendInfo) {
// Use the stored category value; the label is only for display.
const fieldName = sqlUtils.sqlName(activeLayerInfo.layer.renderer.field);
// SQL equality does not match NULL, so use IS NULL for that category.
if (legendInfo.value === null) {
return `${fieldName} IS NULL`;
return `${fieldName} = ${sqlUtils.sqlLiteral(legendInfo.value)}`;
function updateFilter(activeLayerInfo, selectedClauses) {
const layerId = activeLayerInfo.layer.uid;
const layerView = activeLayerInfo.layerView;
if (!selectedClauses.size) {
// With no categories selected, show all features in this layer.
layerSelections.delete(layerId);
// A feature is included when it matches any selected category.
const where = Array.from(selectedClauses, (clause) => `(${clause})`).join(" OR ");
layerView.filter = new FeatureFilter({ where });
layerSelections.set(layerId, selectedClauses);