<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Hit test by screen rectangle in 3D | 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>
--panel-chart-height: 320px;
--panel-chart-height-compact: 280px;
--panel-max-width: 440px;
--panel-viewport-width: 35vi;
--target-color: var(--calcite-color-brand);
conic-gradient(from 90deg at 4px 4px, transparent 25%, var(--target-color) 0) top left / 44px 44px no-repeat,
conic-gradient(from 180deg at 40px 4px, transparent 25%, var(--target-color) 0) top right / 44px 44px
conic-gradient(from 0deg at 4px 40px, transparent 25%, var(--target-color) 0) bottom left / 44px 44px
conic-gradient(from 270deg at 40px 40px, transparent 25%, var(--target-color) 0) bottom right / 44px 44px
border: 2px solid transparent;
filter: drop-shadow(0 0 1px white) drop-shadow(0 0 1px white) drop-shadow(0 0 2px white)
drop-shadow(0 0 4px white) drop-shadow(0 1px 1px rgb(0 0 0 / 80%));
--calcite-panel-space: 0;
width: min(var(--panel-max-width), var(--panel-viewport-width));
#charts-panel.chart-ready {
height: min(var(--panel-chart-height), calc(100dvb - var(--calcite-space-md) - var(--calcite-space-md)));
#charts-panel-instructions {
#charts-panel.chart-ready #charts-panel-instructions {
#charts-panel.chart-ready #classification-chart {
#draw-rectangle-controls {
#clear-rectangle-button {
--calcite-button-background-color: #fff;
@media (max-width: 900px) {
#charts-panel.chart-ready {
var(--panel-chart-height-compact),
calc(100dvb - var(--calcite-space-md) - var(--calcite-space-md))
<arcgis-scene item-id="21a6f805fe00491faa5102d2c4b0c5d9">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-navigation-toggle slot="top-left"></arcgis-navigation-toggle>
<arcgis-compass slot="top-left"></arcgis-compass>
<div id="selection-overlay">
<div id="selection-rectangle" hidden></div>
<calcite-panel id="charts-panel" slot="top-right" scale="s" heading="Point classifications" loading>
<div id="charts-panel-content">
<div id="charts-panel-instructions">Draw a screen rectangle to see point classifications</div>
<arcgis-chart id="classification-chart" action-mode="none" hide-loader-animation></arcgis-chart>
<div id="draw-rectangle-controls">
id="draw-rectangle-button"
appearance="outline-fill"
icon-start="cursor-selection"
<calcite-button id="clear-rectangle-button" appearance="outline-fill" icon-start="x" width="full" hidden>
const [{ createModel }, reactiveUtils, promiseUtils] = await Promise.all([
$arcgis.import("@arcgis/charts-components/model/shared/setup-utils.js"),
$arcgis.import("@arcgis/core/core/reactiveUtils.js"),
$arcgis.import("@arcgis/core/core/promiseUtils.js"),
const viewElement = document.querySelector("arcgis-scene");
const selectionRectangle = document.getElementById("selection-rectangle");
const chartsPanel = document.getElementById("charts-panel");
const drawRectangleButton = document.getElementById("draw-rectangle-button");
const clearRectangleButton = document.getElementById("clear-rectangle-button");
await viewElement.viewOnReady();
const pointCloudLayer = viewElement.map.allLayers.find((layer) => layer.title === "SONOMA AREA1 LiDAR RGB");
const pointCloudLayerView = await viewElement.whenLayerView(pointCloudLayer);
const classificationField = pointCloudLayer.renderer.field;
const classificationsByCode = new Map();
const classificationColors = {};
// Use the renderer as the source of truth for classification names and colors.
for (const rendererClassInfo of pointCloudLayer.renderer.colorUniqueValueInfos) {
const classificationName = rendererClassInfo.label;
const [red, green, blue, alpha] = rendererClassInfo.color.toRgba();
classificationColors[classificationName] = [red, green, blue, alpha * 255];
for (const classCode of rendererClassInfo.values) {
classificationsByCode.set(String(classCode), classificationName);
const classificationChart = document.getElementById("classification-chart");
classificationChart.addEventListener("arcgisLegendItemVisibilityChange", ({ detail }) => {
detail.model?.setSeriesVisibility(detail.legendItemVisibility);
let selectedScreenRectangle;
let chartUpdateAbortController;
const chartInitializationPromise = initializeClassificationChart();
// Coordinates one abortable chart update after the view and point cloud become stationary.
async function requestChartUpdate() {
if (!selectedScreenRectangle) {
chartUpdateAbortController?.abort();
const abortController = new AbortController();
const { signal } = abortController;
chartUpdateAbortController = abortController;
// Wait until navigation and point-cloud rendering stop so hitTest uses the final camera position.
await reactiveUtils.whenOnce(() => viewElement.stationary && !pointCloudLayerView.updating, { signal });
setChartStatus("Calculating statistics...");
await updateChart(signal);
if (!promiseUtils.isAbortError(error)) {
if (chartUpdateAbortController === abortController) {
chartUpdateAbortController = undefined;
// Hit tests the selected screen rectangle, then updates the chart with the matching classifications.
async function updateChart(signal) {
const screenRectangle = selectedScreenRectangle;
const hitTestResponse = await viewElement.hitTest(screenRectangle, {
include: pointCloudLayer,
const classificationCounts = await countClassifications(hitTestResponse.results, signal);
const hasRelevantPoints = Object.keys(classificationCounts).length > 0;
const pointCount = hitTestResponse.results.length.toLocaleString();
const statusText = `${pointCount} points in the screen rectangle`;
await new Promise(requestAnimationFrame);
await updateClassificationChart(classificationCounts, statusText, signal);
chartsPanel.classList.remove("chart-ready");
const pointCount = hitTestResponse.results.length.toLocaleString();
hitTestResponse.results.length > 0
? `${pointCount} points in the screen rectangle; no renderer classifications matched.`
: "No points in the screen rectangle.",
chartsPanel.loading = false;
drawRectangleButton.addEventListener("click", drawScreenRectangle);
clearRectangleButton.addEventListener("click", clearSelection);
() => !viewElement.stationary,
chartUpdateAbortController?.abort();
if (selectedScreenRectangle) {
setChartStatus("Pause navigation to update the chart.");
void requestChartUpdate();
() => viewElement.stationary,
if (chartUpdateAbortController && selectedScreenRectangle) {
setChartStatus("Calculating statistics...");
await reactiveUtils.whenOnce(() => !pointCloudLayerView.updating);
await chartInitializationPromise;
chartsPanel.loading = false;
drawRectangleButton.disabled = false;
setChartStatus("Draw a screen rectangle to see point classifications.");
// Counts renderer classifications in short chunks so navigation and rendering remain responsive.
async function countClassifications(pointHitResults, signal) {
const classificationCounts = {};
let chunkStartTime = performance.now();
for (const pointHitResult of pointHitResults) {
const hitAttributes = pointHitResult.graphic.attributes;
const classificationAttribute = Object.keys(hitAttributes).find(
(attributeName) => attributeName.toLowerCase() === classificationField.toLowerCase(),
const classificationCode = classificationAttribute ? hitAttributes[classificationAttribute] : undefined;
const classificationName = classificationsByCode.get(String(classificationCode));
if (classificationName) {
classificationCounts[classificationName] = (classificationCounts[classificationName] ?? 0) + 1;
if (performance.now() - chunkStartTime >= 8) {
await new Promise((resolve) => setTimeout(resolve));
chunkStartTime = performance.now();
return classificationCounts;
// Applies aggregate counts to the chart model and waits for its visible refresh to finish.
async function updateClassificationChart(classificationCounts, statusText, signal) {
await chartInitializationPromise;
setChartStatus(statusText);
const classificationFeatures = createClassificationFeatures(classificationCounts);
await activeChartModel.layer.applyEdits({ updateFeatures: classificationFeatures });
const isExistingChart = classificationChart.model === activeChartModel;
const refreshComplete = isExistingChart
? new Promise((resolve) =>
classificationChart.addEventListener("arcgisUpdateComplete", resolve, { once: true }),
await classificationChart.refresh({ updateData: true });
classificationChart.layer = activeChartModel.layer;
classificationChart.model = activeChartModel;
await classificationChart.loadModel();
chartsPanel.classList.add("chart-ready");
chartsPanel.loading = false;
function setChartStatus(statusText) {
document.getElementById("charts-panel-instructions").textContent = statusText;
activeChartModel.subtitleText = statusText;
function createClassificationFeatures(classificationCounts = {}) {
// arcgis-chart consumes the aggregate counts through a small inline feature collection
return Object.keys(classificationColors).map((classificationName, featureIndex) => ({
OBJECTID: featureIndex + 1,
CLASS_NAME: classificationName,
COUNT: classificationCounts[classificationName] ?? 0,
async function initializeClassificationChart() {
const classificationFeatures = createClassificationFeatures();
const classificationChartModel = await createModel({
id: "point-cloud-classification-counts",
layerType: "ArcGISFeatureLayer",
{ type: "esriFieldTypeOID", name: "OBJECTID" },
{ type: "esriFieldTypeString", name: "CLASS_NAME" },
{ type: "esriFieldTypeInteger", name: "COUNT" },
featureSet: { features: classificationFeatures },
classificationChartModel.category = "CLASS_NAME";
classificationChartModel.numericFields = ["COUNT"];
classificationChartModel.innerRadiusSize = 50;
classificationChartModel.titleText = "";
classificationChartModel.subtitleText = "Draw a screen rectangle to see point classifications.";
classificationChartModel.subtitleSymbol = {
...classificationChartModel.subtitleSymbol,
font: { ...classificationChartModel.subtitleSymbol?.font, size: 10 },
classificationChartModel.legendTextSymbol = {
...classificationChartModel.legendTextSymbol,
font: { ...classificationChartModel.legendTextSymbol?.font, size: 10 },
classificationChartModel.setLabelMaxWidth(60);
classificationChartModel.colorMatch = false;
classificationChartModel.slices = classificationFeatures.map(({ attributes: classificationAttributes }) => {
const classificationColor = classificationColors[classificationAttributes.CLASS_NAME];
sliceId: classificationAttributes.CLASS_NAME,
label: classificationAttributes.CLASS_NAME,
originalLabel: classificationAttributes.CLASS_NAME,
color: classificationColor,
outline: { type: "esriSLS", color: classificationColor },
activeChartModel = classificationChartModel;
classificationChart.layer = classificationChartModel.layer;
classificationChart.model = classificationChartModel;
await classificationChart.loadModel();
classificationChart.shadowRoot.querySelector(".chart-wrapper").style.height = "100%";
function setSelectionRectangle(screenRectangle) {
selectedScreenRectangle = screenRectangle;
selectionRectangle.hidden = !screenRectangle;
chartsPanel.classList.toggle("has-selection", Boolean(screenRectangle));
chartsPanel.classList.remove("chart-ready");
clearRectangleButton.hidden = !screenRectangle;
selectionRectangle.style.insetInlineStart = `${screenRectangle.x}px`;
selectionRectangle.style.insetBlockStart = `${screenRectangle.y}px`;
selectionRectangle.style.width = `${screenRectangle.width}px`;
selectionRectangle.style.height = `${screenRectangle.height}px`;
async function clearSelection() {
chartUpdateAbortController?.abort();
setSelectionRectangle(undefined);
await chartInitializationPromise;
const classificationFeatures = createClassificationFeatures();
await activeChartModel.layer.applyEdits({ updateFeatures: classificationFeatures });
setChartStatus("Draw a screen rectangle to see point classifications.");
chartsPanel.loading = false;
function drawScreenRectangle() {
chartUpdateAbortController?.abort();
chartsPanel.classList.remove("chart-ready");
drawRectangleButton.disabled = true;
viewElement.classList.add("selection-cursor");
setChartStatus("Drag in the scene to draw a screen rectangle.");
const dragEventListener = (event) => {
event.detail.stopPropagation();
if (event.detail.action !== "end") {
const sceneBounds = viewElement.getBoundingClientRect();
const xmin = Math.max(Math.min(event.detail.origin.x, event.detail.x), 0);
const xmax = Math.min(Math.max(event.detail.origin.x, event.detail.x), sceneBounds.width);
const ymin = Math.max(Math.min(event.detail.origin.y, event.detail.y), 0);
const ymax = Math.min(Math.max(event.detail.origin.y, event.detail.y), sceneBounds.height);
setSelectionRectangle(screenRectangle);
viewElement.removeEventListener("arcgisViewDrag", dragEventListener);
viewElement.classList.remove("selection-cursor");
drawRectangleButton.disabled = false;
if (!screenRectangle?.width || !screenRectangle.height) {
setSelectionRectangle(undefined);
setChartStatus("Draw a screen rectangle to see point classifications.");
chartsPanel.loading = true;
void requestChartUpdate();
viewElement.addEventListener("arcgisViewDrag", dragEventListener);