This sample shows how to generate a chart on the client-side for an ImageryLayer. As the user clicks on or drags the pointer across the view, the app reads and processes the pixel data that is within one mile of the pointer location. The Land Cover Types chart immediately updates with new data as the pointer location changes.
This app displays National Land Cover Database (NLCD) 2001 land cover classification rasters of the conterminous US.
How it works
The land cover imagery layer is initialized with lerc format which returns the raw pixel values for the requested images.
// Initialize the imagery layer with the LERC format.
const imageryLayer = new ImageryLayer({ url, format: "lerc" });
viewElement.map.add(imageryLayer);
const layerView = await viewElement.whenLayerView(imageryLayer);
Since lerc format returns raw pixel values of images, we can access the pixel values in the browser. These raw values can be accessed via ImageryLayerView's pixelData and they are updated whenever user zooms or pans the view. The application watches the LayerView's updating property to get the updated pixel values. These values then can be used to create the Land Cover Types chart whenever the user clicks on or drags the pointer over the ImageryLayer.
const initializeLayerView = (layerView) => {
// Store the pixel data when the LayerView finishes updating.
reactiveUtils.watch(
() => layerView.updating,
(updating) => (pixelData = !updating ? layerView.pixelData : null),
);
handleEventBinding(); // Enable the land cover tool.
const graphics = imageryLayer.serviceRasterInfo.attributeTable.features;
// Extract color and class name from each graphic feature.
graphics.forEach(({ attributes }) => {
const { Red, Green, Blue, ClassName, Value } = attributes;
const hexColor = rgbToHex(Red, Green, Blue);
rasterAttributeGraphics[Value] = { ClassName, hexColor };
});
createLandCoverChart(); // Initialize the land cover doughnut chart.
};
initializeLayerView(layerView);
As user clicks on or drags the pointer over the view, the get function is called. In this function, we execute a logic to read and store pixel values that fall within one mile of the pointer location.
// Get land cover pixel info within one mile of the pointer location.
const getLandCoverPixelInfo = promiseUtils.debounce(({ x, y }) => {
if (!(pixelData && pixelData.pixelBlock && pixelData.pixelBlock.pixels)) return;
const { xmin, xmax } = pixelData.extent;
const { width, pixels } = pixelData.pixelBlock;
const center = viewElement.toMap({ x, y }); // Derive mapPoint from pixel coords.
const screenX = Math.round(x); // Store integer for screen pixel coord x.
const screenY = Math.round(y); // Store integer for screen pixel coord y.
const pixelMeterSizeX = Math.abs(xmax - xmin) / width;
const bufferDim = Math.ceil(1609 / pixelMeterSizeX); // Find pixels per mile.
// Determine the two-mile extent around the pointer location.
const bufferLeftPx = screenX - bufferDim < 0 ? 0 : screenX - bufferDim;
const bufferTopPx = screenY - bufferDim < 0 ? 0 : screenY - bufferDim;
const startPixel = bufferTopPx * width + bufferLeftPx;
const bandOne = pixels[0]; // Store the single band of land cover codes.
const radiusSq = bufferDim * bufferDim;
// Reuse existing mask; only rebuild if size change (e.g., on zoom).
if (bufferDim !== cachedBufferDim) {
cachedMask = createCircleMask(bufferDim, radiusSq);
cachedBufferDim = bufferDim;
}
const { mask, diameter } = cachedMask;
let oneMilePixelValues = [];
// Loop over bounding box, using mask to skip irrelevant pixels.
// Raster is 1D in memory but represents a 2D grid.
// (row * width) moves down; + col moves right to get pixel index.
for (let rowOffset = 0; rowOffset < diameter; rowOffset++) {
const rowStartIndex = startPixel + rowOffset * width;
for (let colOffset = 0; colOffset < diameter; colOffset++) {
if (mask[rowOffset * diameter + colOffset]) {
const pixelIndex = rowStartIndex + colOffset;
const pixelValue = bandOne[pixelIndex];
if (pixelValue !== undefined) {
oneMilePixelValues.push(pixelValue);
}
}
}
}
// Clear counts, then tally pixel values in the 1‑mile area.
pixelValueCount = {};
for (let i = 0; i < oneMilePixelValues.length; i++) {
const value = oneMilePixelValues[i];
pixelValueCount[value] = 1 + (pixelValueCount[value] || 0);
}
circleGraphic.geometry = new Circle({ center, radius: 1, radiusUnit: "miles" });
updateLandCoverChart();
});
Once the raw pixel values are processed for the chart, update is called and the chart is updated to reflect the land cover types for the new location.
// Update the chart with pixel counts and colors within one mile of the pointer.
const updateLandCoverChart = () => {
if (infoDiv.style.display !== "block") infoDiv.style.display = "block";
landCoverChart.data.datasets[0].data = [];
let landCoverTypeColors = [];
let landCoverTypeLabels = [];
// Use the counter object to set chart data, colors, and labels.
for (const index in pixelValueCount) {
if (index === "0") {
landCoverTypeColors.push("rgba(255, 255, 255, 1)");
landCoverTypeLabels.push("NoData");
} else {
const color = rasterAttributeGraphics[index].hexColor;
landCoverTypeColors.push(color);
landCoverTypeLabels.push(rasterAttributeGraphics[index].ClassName);
}
landCoverChart.data.datasets[0].data.push(pixelValueCount[index]);
}
// Apply colors and labels to the chart, then refresh without animation.
landCoverChart.data.datasets[0].backgroundColor = landCoverTypeColors;
landCoverChart.data.labels = landCoverTypeLabels;
landCoverChart.update("none");
};