ArcGIS API for JavaScript

How and why to adjust symbol size by scale in web maps

When it comes to cartography, few things make me cringe more than overly prominent outlines that hide patterns, and icons that are either too small to see, or too large to take seriously.

With web cartography in particular, where maps often span multiple scale levels, finding the right outline widths and icon sizes can feel impossible. You may find sizes that work at one scale, but zooming in or out can quickly turn good cartographic choices into mediocre ones.

The ArcGIS API for JavaScript (ArcGIS JS API) allows developers to define renderers that work well across multiple scales. These APIs give you full control of icon, polyline, and outline sizes at specific scales, also while providing helper methods when you don’t know where to start.

The problem with static icon sizes

One icon size usually doesn’t look good at all scales. This is especially true for dense layers. For example, the points in the map below look good with a size of 10px at a national scale.

Images have been scaled down from their original sizes. Click each image to see the icons at their true sizes.

UK weather stations. Each point is clearly visible at 10px.
UK weather stations. Each point is 10px.

But when zoomed out to a worldwide extent, 10px appears too big. The large icon size obscures the underlying data, potentially misrepresenting its density.

Worldwide weather stations. At 10px each, the points appear too cluttered.
Worldwide weather stations. Each point is 10px.

With a smaller sized point symbol, many of the issues listed above can be reduced or eliminated.

Worldwide weather stations. The visual looks much better at this scale when points are 3px.
Worldwide weather stations. Each point is 3px.

But 3px makes the points almost impossible to see when you zoom to regional scales or beyond.

UK weather stations. The points are nearly impossible to discern when they are 3px at this scale.
UK weather stations. Each point is 3x.

Dynamic size by scale

By using a size visual variable and a very simple Arcade expression, you can dynamically change the size of these points based on scale.

The visualization profile in Arcade has access to the view’s scale via the $view.scale global variable. In fact, that’s all you need for your expression! Just map the scale values with specific sizes in the stops property of the size variable.

renderer.visualVariables = [
  {
    type: "size",
    valueExpression: "$view.scale",
    stops: [
      {
        size: 7.5,
        value: 1155581
      },
      {
        size: 6,
        value: 9244648
      },
      {
        size: 3,
        value: 73957190
      },
      {
        size: 1.5,
        value: 591657527
      }
    ]
  }
];

Now you get the best of both worlds: smaller icons at small scales (i.e. zoomed out) and larger icons at large scales (i.e. zoomed in).

A size variable can control icon sizes at various scales. The zoomed out view on the left shows smaller icons, while the zoomed in view on the right shows larger icons.
A size variable can control icon sizes at various scales. The zoomed out view on the left shows smaller icons, while the zoomed in view on the right shows larger icons.

Check out this app, which allows you to experiment with icons whose sizes vary by scale.

Click the >> icon to view the generated size variable and effective size for the given scale.

This same approach also works for polyline data. The following app makes roads in Florida thicker as you zoom in, and thinner as you zoom out.

Click the >> icon to view the generated size variable and effective size for the given scale.

A little help from Smart Mapping

The SizeVariable API is great because it gives you full control over configuring icon size and line width at any scale level. But you may ask “How do I know which scales matter, and the size range to use?”

That’s where Smart Mapping in the ArcGIS JS API comes in to play. When you use any createRenderer method in the esri/renderers/smartMapping/creators modules, you can use the sizeOptimizationEnabled parameter to generate a suggested size variable that adjusts icon and line sizes by scale. This is available in multiple methods, but for the purposes of this blog, I’ll use the createRenderer method in the locationRendererCreator module.

Expand/collapse the code
locationRendererCreator
  .createRenderer({
    layer: layer,
    view: view,
    // generates a Size Variable that
    // adjusts symbol size by scale
    sizeOptimizationEnabled: true
  })
  .then(function(rendererResponse) {
    // the renderer contains a size variable with stops
    // mapping icon sizes to scale values
    layer.renderer = rendererResponse.renderer;

    // Or you could reuse your existing
    // renderer and set the size variable
    // accordingly ...

    const existingRenderer = layer.renderer.clone();
    const existingVariables = existingRenderer.visualVariables;
    const scaleVariables = rendererResponse.renderer.visualVariables
      .filter(function(vv){
        return vv.valueExpression && vv.valueExpression === "$view.scale";
      });
    existingRenderer.visualVariables = existingVariables.concat(scaleVariables);

    layer.renderer = existingRenderer;
  })
  .catch(function(error) {
    console.error(error);
  });

Once you have the variable, I suggest you copy it and use it directly in the app, so you can avoid making duplicate calls to the Smart Mapping methods every time the app loads.

Graduated symbols by scale

You can even optimize visualizations of data-driven continuous size (aka graduated symbols) by scale. This will cause the entire range of symbols to grow in size as you zoom in, and shrink as you zoom out.

We’ll use this example of traffic count data in a polyline layer of Florida highways, though this principle can also be applied to point layers and polygon centroids.

Note the size range at this scale looks pretty good.

Annual average daily traffic on Florida highways. Thicker lines indicate heavier traffic.
Annual average daily traffic on Florida highways. Thicker lines indicate heavier traffic.

As you zoom in, the width of each highway increases (note the difference in the map and legend between this image and the previous one).

Annual average daily traffic on Florida highways. Thicker lines indicate heavier traffic. The thickness of each line grows as you zoom in.
Annual average daily traffic on Florida highways. Thicker lines indicate heavier traffic. The thickness of each line grows as you zoom in.

And as you zoom out, the sizes shrink.

Annual average daily traffic on Florida highways. Thicker lines indicate heavier traffic. The thickness of each line shrinks as you zoom out.
Annual average daily traffic on Florida highways. Thicker lines indicate heavier traffic. The thickness of each line shrinks as you zoom out.

Though it becomes more difficult to distinguish the varying size range as you zoom out, it’s better than the alternative – bloated static sizes that are a bit more difficult to interpret in dense areas.

Annual average daily traffic on Florida highways. This line thickness works better when zoomed in, but degrades when zoomed out.
Annual average daily traffic on Florida highways. This line thickness works better when zoomed in, but degrades when zoomed out.

The solution to make this work calls for a more complicated size variable. The size variable must reference an attribute field or Arcade expression, with two additional size variables (like the one described above) referenced by the minSize and maxSize properties.

renderer.visualVariables = [
  {
    type: "size",
    // Feature size depends on
    // the value of the AADT field
    field: "AADT",
    // Features with this value and
    // higher in AADT will be given
    // the max size
    maxDataValue: 299000,
    // The max size will vary
    // based on the view scale
    maxSize: {
      type: "size",
      valueExpression: "$view.scale",
      stops: [
        {
          size: 24,
          value: 9027.977411
        },
        {
          size: 13.5,
          value: 72223.819286
        },
        {
          size: 9,
          value: 577790.554289
        },
        {
          size: 4.5,
          value: 4622324.434309
        }
      ]
    },
    // Features with this value and
    // lower in AADT will be given
    // the min size
    minDataValue: 0,
    // The min size will vary
    // based on the view scale
    minSize: {
      type: "size",
      valueExpression: "$view.scale",
      stops: [
        {
          size: 1.5,
          value: 9027.977411
        },
        {
          size: 0.75,
          value: 72223.819286
        },
        {
          size: 0.5625,
          value: 577790.554289
        },
        {
          size: 0.375,
          value: 4622324.434309
        }
      ]
    }
  }
];

Click the >> icon to view the generated size variable and effective size for the given scale.

While this variable is more complicated to create, the sizeRendererCreator.createContinuousRenderer method has you covered. You can generate a size variable to start with and customize it later in the development process.

sizeRendererCreator
  .createRenderer({
    layer: layer,
    view: view,
    field: "AADT",
    // generates a Size Variable that
    // adjusts symbol size by scale
    sizeOptimizationEnabled: true
  })
  .then(function(rendererResponse) {
    // the renderer contains a size variable with stops
    // mapping icon sizes to scale values
    layer.renderer = rendererResponse.renderer;
  })
  .catch(function(error) {
    console.error(error);
  });

See also sizeRendererCreator.createVisualVariables.

Outlines

Overly thick outlines probably make me cringe more than any other cartographic fail. Yet, outlines can be so hard to get right when multiple scales are involved.

For example, the image below shows outlines that are so thick they completely obscure the fill color of small polygons in the downtown Houston area.

Vacant housing in the Houston Area. Outlines are so thick you can't see the fill color in high density areas.
Vacant housing in the Houston Area. Outlines are so thick you can't see the fill color in high density areas.

That outline choice is clearly unacceptable. But if you zoom to a very large scale, that outline choice may actually work.

Vacant housing in the Houston Area. While thick outlines don't work well at small scales, they can be effective at large scales.
Vacant housing in the Houston Area. While thick outlines don't work well at small scales, they can be effective at large scales.

That’s why the relatively common solution of removing outlines altogether may not be the best decision. It can cause confusion for the map reader at large scales because neighboring features with the same color may appear to be a single unified feature.

Removing outlines may be appropriate at small scales, but that may cause confusion at large scales where features of the same color border each other.
Removing outlines may be appropriate at small scales, but that may cause confusion at large scales where neighboring features of the same color appear to be a single polygon.

Using the same pattern established earlier with the size variable, you can adjust outline width by scale simply by returning the view scale in an Arcade expression and mapping scale values to size stops. The only difference is that you must also set the target property to outline. That way the renderer knows the variable should apply to the outline.

renderer.visualVariables = [
  {
    type: "size",
    valueExpression: "$view.scale",
    target: "outline",
    stops: [
      {
        size: 2,
        value: 56187
      },
      {
        size: 1,
        value: 175583
      },
      {
        size: 0.5,
        value: 702332
      },
      {
        size: 0,
        value: 1404664
      }
    ]
  }
];

Now you see outline widths shrink at small scales.

Adding a size variable targeting polygon outlines makes it easier to see fill colors in smaller polygons at small scales.
Adding a size variable targeting polygon outlines makes it easier to see fill colors in smaller polygons at small scales.

And grow at larger scales.

At larger scales, polygon outlines will be thicker with some added transparency. This makes the outline visible, but not too prominent.

The same Smart Mapping methods mentioned above also have an outlineOptimizationEnabled parameter that when set to true generates this variable for you so you don’t have to worry about arbitrary scale values.

locationRendererCreator
  .createContinuousRenderer({
    layer: layer,
    view: view,
    outlineOptimizationEnabled: true
  })
  .then(function(rendererResponse) {
    // the renderer contains a size variable with stops
    // mapping polygon outline widths to scale values
    const renderer = rendererResponse.renderer;
    layer.renderer = renderer;
  })
  .catch(function(error) {
    console.error(error);
  });

The power of subtlety

Icon sizes and outlines that scale shouldn’t draw attention to themselves or be obvious to the map reader. They should feel so natural they go unnoticed.

I believe the best visualizations aren’t flashy—they look natural and are easy to understand. However, designing visualizations this way isn’t a trivial matter. They still take effort to get right.

The Smart Mapping methods aren’t intended to be the final decision for what’s best for you. In fact, in most cases you will likely tweak the suggested values until they feel right for your particular dataset. That may take a little extra time to get right, but the end result will be worth it.

About the author

Kristian Ekenes is a Product Engineer on the ArcGIS API for JavaScript team at Esri. His work focuses on mapping, visualization, and Arcade integration. Prior to joining Esri he worked as a GIS Specialist for an environmental consulting company. He enjoys cartography, GIS analysis, and building GIS applications for genealogy.

Connect:

Next Article

App Config using MDM for ArcGIS Business Analyst Mobile App

Read this article