ImageryLayer - client side chart

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.

Use dark colors for code blocksCopy
1
2
3
4
const imageryLayer = new ImageryLayer({
  url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/NLCDLandCover2001/ImageServer",
  format: "lerc"
});

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.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const layerLoaded = (layerView) => {
  // watch for the imagery layer view's updating property
  // to get the updated pixel values
  reactiveUtils.when(
    () => !layerView.updating,
    () => {
        pixelData = layerView.pixelData;
      }
    }
  );

  // when the layer loads, listen to the view's drag and click
  // events to update the land cover types chart to reflect an
  // area within 1 mile of the pointer location.
  removeChartEvents = view.on(["drag", "click"], (event) => {
    if (pixelData) {
      event.stopPropagation();
      getLandCoverPixelInfo(event);
    }
  });
  // raster attributes table returns categorical mapping of pixel values such as class and group
  const attributesData = imageryLayer.serviceRasterInfo.attributeTable.features;

  // rasterAttributeFeatures will be used to add legend labels and colors for each
  // land use type
  for (let index in attributesData) {
    if (attributesData) {
      const hexColor = rgbToHex(
        attributesData[index].attributes.Red,
        attributesData[index].attributes.Green,
        attributesData[index].attributes.Blue
      );
      rasterAttributeFeatures[attributesData[index].attributes.Value] = {
        ClassName: attributesData[index].attributes.ClassName,
        hexColor: hexColor
      };
    }
  }
  // initialize the land cover pie chart
  createLandCoverChart();
};

const layerView = await view.whenLayerView(imageryLayer);
layerLoaded(layerView);

As user clicks on or drags the pointer over the view, the getLandCoverPixelInfo() 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.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// This function is called as user drags the pointer over or clicks on the view.
// Here we figure out which pixels fall within one mile of the
// pointer location and update the chart accordingly
const getLandCoverPixelInfo = (event) => {
  const currentExtent = pixelData.extent;
  const pixelBlock = pixelData.pixelBlock;
  const height = pixelBlock.height;
  const width = pixelBlock.width;

  // map point for the pointer location.
  const point = view.toMap({
    x: event.x,
    y: event.y
  });
  // pointer x, y in pixels
  const reqX = Math.ceil(event.x);
  const reqY = Math.ceil(event.y);

  // calculate how many meters are represented by 1 pixel.
  const pixelSizeX = Math.abs(currentExtent.xmax - currentExtent.xmin) / width;

  // calculate how many pixels represent one mile
  const bufferDim = Math.ceil(1609 / pixelSizeX);

  // figure out 2 mile extent around the pointer location
  const xmin = reqX - bufferDim < 0 ? 0 : reqX - bufferDim;
  const ymin = reqY - bufferDim < 0 ? 0 : reqY - bufferDim;
  const startPixel = ymin * width + xmin;
  const bufferlength = bufferDim * 2;
  const pixels = pixelBlock.pixels[0];
  const radius2 = bufferDim * bufferDim;
  let oneMilePixelValues = [];

  // cover pixels within to 2 mile rectangle
  if (bufferlength) {
    for (let i = 0; i <= bufferlength; i++) {
      for (let j = 0; j <= bufferlength; j++) {
        // check if the given pixel location is in within one mile of the pointer
        // add its value to pixelValue.
        let pixelValue;
        if (Math.pow(i - bufferDim, 2) + Math.pow(j - bufferDim, 2) <= radius2) {
          pixelValue = pixels[Math.floor(startPixel + i * width + j)];
        }
        if (pixelValue !== undefined) {
          oneMilePixelValues.push(pixelValue);
        }
      }
    }
  } else {
    oneMilePixelValues.push(pixels[startPixel]);
  }
  pixelValCount = {};
  // get the count of each land type returned within one mile raduis
  for (let i = 0; i < oneMilePixelValues.length; i++) {
    pixelValCount[oneMilePixelValues[i]] = 1 + (pixelValCount[oneMilePixelValues[i]] || 0);
  }
  const circle = new Circle({
    center: point,
    radius: bufferDim * pixelSizeX
  });

  graphic.geometry = circle;
  updateLandCoverChart();
};

Once the raw pixel values are processed for the chart, updateLandCovertChart() is called and the chart is updated to reflect the land cover types for the new location.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// This function is called once pixel values within one mile of the pointer
// location are processed and ready for the chart update.
const updateLandCoverChart = () => {
  if (infoDiv.style.display != "block") {
    infoDiv.style.display = "block";
  }
  landCoverChart.data.datasets[0].data = [];
  let dataset = [];
  let landCoverTypeColors = [];
  let landCoverTypeLabels = [];

  // pixelValCount object contains land cover types and count of pixels
  // that represent that type in within one mile.
  for (let index in pixelValCount) {
    if (index == 0) {
      landCoverTypeColors.push("rgba(255,255,255,1");
      landCoverTypeLabels.push("NoData");
    } else {
      const color = rasterAttributeFeatures[index].hexColor;
      landCoverTypeColors.push(color);
      landCoverTypeLabels.push(rasterAttributeFeatures[index].ClassName);
    }
    landCoverChart.data.datasets[0].data.push(pixelValCount[index]);
  }
  landCoverChart.data.datasets[0].backgroundColor = landCoverTypeColors;
  landCoverChart.data.labels = landCoverTypeLabels;
  landCoverChart.update(0);
  document.getElementById("chartLegend").innerHTML = landCoverChart.generateLegend();
};

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.