<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Aggregate spatial statistics | Sample | ArcGIS Maps SDK for JavaScript</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.3/chart.umd.min.js"></script>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.0/"></script>
<arcgis-map item-id="1fff20e8185f4f769a0b96f00acd97e0" id="map-element">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-expand slot="top-left" expand-tooltip="Expand legend">
<arcgis-legend></arcgis-legend>
heading="Total acreages of forests by region">
<calcite-block expanded label="chart block" id="chart-block">
<canvas id="chart" height="250" width="300" style="margin: 0 auto 1rem"></canvas>
<calcite-notice open width="full" scale="s">
<span slot="title">Instructions</span>
Hover over the chart to see facts about forests in that region.
heading="No region selected"
<calcite-action disabled id="clearAction" slot="actions-end" text="Clear" icon="blank">
<calcite-label layout="inline-space-between"
>Forests<span id="forestCount"></span
<calcite-label layout="inline-space-between"
>Acres<span id="acreageCount"></span
text="Highlight largest forest"
const [reactiveUtils] = await $arcgis.import(["@arcgis/core/core/reactiveUtils.js"]);
const viewElement = document.querySelector("arcgis-map");
viewElement.constraints = {
// Use to store the current biggest park feature
let groupField = "REGION";
let statsField = "GIS_ACRES";
let currentBiggestPark = null;
const canvasElement = document.getElementById("chart");
// Displays forest info as user interacts with the chart
const factSection = document.getElementById("factSection");
factSection.addEventListener("calciteBlockSectionToggle", (event) => {
handleParkFeatureComponent();
const resultBlock = document.getElementById("resultsHeading");
const clearAction = document.getElementById("clearAction");
// Wait for the view to be ready
await viewElement.viewOnReady();
viewElement.highlights.reverse();
// Get the forests region layer from the web map
const layer = viewElement.map.allLayers.find(
(layer) => layer.title === "Administrative Forest Regions",
const aggregateForestDataByRegions = [];
const biggestForestByRegion = {};
// Get the layer view for the forests layer to apply feature effects
const layerView = await viewElement.whenLayerView(layer);
// Wait until layer view is loaded and ready
await reactiveUtils.whenOnce(() => !layerView.updating);
// Query forest layer and set up the charts and region data only after the layer is loaded
// Runs grouped stats query on forests layerView:
// - Gets count, total acres, convex hull, largest forest per region
// - Populates chart and region data
async function runStats() {
aggregateForestDataByRegions.length = 0;
// Define the statistics to compute for each region
const consumeStatsByRegion = {
onStatisticField: statsField,
outStatisticFieldName: "totalAcresStatsField",
const forestsInRegion = {
onStatisticField: "OBJECTID",
outStatisticFieldName: "forestCountStatsField",
const aggregatedConvex = {
statisticType: "convex-hull-aggregate",
outStatisticFieldName: "aggregateConvexHull",
const query = layer.createQuery();
query.groupByFieldsForStatistics = [groupField];
query.orderByFields = [`${groupField} desc`];
query.outStatistics = [consumeStatsByRegion, forestsInRegion, aggregatedConvex];
// Execute the statistics query to use for chart and region data
const statsResults = await layer.queryFeatures(query);
// Prepare data for chart and region selection
const regions = statsResults.features.map((f) => f.attributes[groupField]);
const chartData = statsResults.features.map((f) => {
aggregateForestDataByRegions.push({
extent: f.aggregateGeometries.aggregateConvexHull,
region: f.attributes[groupField],
count: f.attributes.forestCountStatsField,
totalAcres: f.attributes.totalAcresStatsField,
return f.attributes.totalAcresStatsField;
// TopFeatureQuery parameter to get the largest forest in each region
const topFeatureQuery = {
groupByFields: [groupField],
orderByFields: [`${statsField} desc`],
const topQueryResult = await layer.queryTopFeatures(topFeatureQuery);
// Map each feature from results to its region in biggestForestByRegion
topQueryResult.features.map((feature) => {
biggestForestByRegion[feature.attributes.REGION] = feature;
// All query results are ready, update the chart UI
document.getElementById("chart-block").loading = false;
setupChart(regions, chartData);
// Add the convex hull of grouped forests for that region
// Set a featureEffect on the layerView
canvasElement.addEventListener("mousemove", async () => {
const data = await getRegionFromChart(event);
await updateMapForSelectedRegion(data);
async function updateMapForSelectedRegion(data) {
if (!data || !data.extent || !data.region) return;
// Update UI block when hovering over a new region
if (data.region !== previousRegion) {
resultBlock.heading = `Region ${data.region}`;
resultBlock.disabled = false;
resultBlock.expanded = true;
clearAction.disabled = false;
previousRegion = data.region;
// Remove previous graphics and add new region hull
viewElement.graphics.removeAll();
viewElement.graphics.add({
// Update region stats and UI using cached largest forest
const park = biggestForestByRegion[data.region];
if (park && park.attributes.REGION === data.region) {
const totalAcres = parseInt(data.totalAcres).toLocaleString("en-US");
document.getElementById("acreageCount").innerHTML = `<b>${totalAcres}</b>`;
document.getElementById("forestCount").innerHTML = `<b>${data.count}</b>`;
currentBiggestPark = park;
if (factSection.expanded) {
handleParkFeatureComponent();
createForestCard(park, data.region);
// Apply feature effect to highlight the selected region
layerView.featureEffect = {
where: `${groupField} = '${data.region}'`,
excludedEffect: "blur(0pt) opacity(0.5) grayscale(1)",
includedEffect: "drop-shadow(3pt 2pt 2pt rgba(50, 50, 50, 0.5))",
// Create a calcite card for the largest forest in the region
// Display the card under the chart as user hovers over each region
function createForestCard(park, region) {
const card = document.createElement("calcite-card");
card.text = `Highlight largest forest in region ${region}`;
const cardTitle = document.createElement("span");
cardTitle.slot = "heading";
cardTitle.textContent = park.attributes.FORESTNAME;
const cardSubtitle = document.createElement("span");
cardSubtitle.slot = "description";
cardSubtitle.textContent = `${park.attributes.GIS_ACRES.toLocaleString("en-US")} acres`;
const button = document.createElement("calcite-button");
button.textContent = `Visit ${park.attributes.FORESTNAME}`;
button.iconEnd = `launch`;
button.target = "_blank";
button.href = park.attributes.url;
button.slot = "footer-trailing";
factSection.textContent = "";
factSection.text = `Highlight largest forest in region ${region}`;
card.appendChild(button);
card.appendChild(cardTitle);
card.appendChild(cardSubtitle);
factSection.appendChild(card);
// Called once after the aggregate spatial statistics runs
// Set up the chart once after the aggregate spatial statistics runs
function setupChart(regions, chartData) {
chart = new Chart(canvasElement.getContext("2d"), {
legend: { display: false },
title: { display: false },
const acres = context.dataset.data[context.dataIndex];
parseInt(acres).toLocaleString("en-US")
const featureElement = document.createElement("arcgis-feature");
featureElement.slot = "bottom-left";
featureElement.autoDestroyDisabled = true;
// Runs when user clicks on the largest forest stats
async function handleParkFeatureComponent() {
if (currentBiggestPark && factSection.expanded) {
// Show the feature component if not visible
viewElement.append(featureElement);
// Update the graphic if needed
!featureElement.graphic ||
featureElement.graphic.attributes.OBJECTID !== currentBiggestPark.attributes.OBJECTID
featureElement.graphic = currentBiggestPark;
highlight = layerView.highlight(currentBiggestPark, { name: "temporary" });
// Hide the feature component if visible
featureElement.graphic = null;
// Clear out region related graphics and stats when user is not hovering over the chart
clearAction.addEventListener("click", () => {
// Remove feature effect and highlights
if (layerView.featureEffect) layerView.featureEffect = null;
featureElement.graphic = null;
if (viewElement.graphics) viewElement.graphics.removeAll();
resultBlock.heading = `No region selected`;
resultBlock.disabled = true;
resultBlock.expanded = false;
clearAction.icon = "blank";
clearAction.disabled = true;
factSection.expanded = false;
// Called when user hovers over the donut chart
async function getRegionFromChart(event) {
// Chart.js 4.x: getElementsAtEventForMode returns elements with index property
const activePoints = chart.getElementsAtEventForMode(
let selectedRegion = null;
if (activePoints.length > 0) {
const idx = activePoints[0].index;
const label = chart.data.labels[idx];
// Find the region object by label
selectedRegion = aggregateForestDataByRegions.find((data) => data.region === label);