This article was originally published on April 12, 2024, by John Nelson on the Esri ArcGIS Blog under the title Multi-Scale Contour Styling in ArcGIS Pro.
Introduction
When it comes to styling contour lines, one word matters above all: interval—and yes, it's worth repeating!
Contour lines are used to represent elevation on topographic maps, and the contour interval refers to the vertical distance between two consecutive contour lines.
Choosing the right interval is crucial and largely depends on the map's scale. On small-scale maps, using too small an interval can make the contours appear overly dense, creating visual clutter and making the map hard to read. The interval must be carefully adjusted to match both the map's scale and the terrain being represented.
There's no universal rule for selecting the perfect interval, but based on my experience mapping the terrain of Greece, I've developed a few practical guidelines:
| Map Scale | Primary Contours Interval (m) | Secondary Contours Interval (m) |
|---|---|---|
| 1:50,000 | 250 | 50 |
| 1:100,000 | 500 | 100 |
| 1:200,000 | 1000 | 200 |
From this classification, it's clear that each map scale requires two types of contour intervals: one for primary contours—which are displayed with a thicker stroke and elevation labels—and another for secondary contours, shown with thinner lines and no labels. These intervals follow a simple relationship: the secondary interval divides evenly into the primary, typically at a 1:5 ratio (e.g., 250 / 50 = 5 or 500 / 100 = 5).
By applying these straightforward rules—and with a bit of scripting using SQL and Arcade—you can generate clean, visually appealing contour lines in ArcGIS Pro with minimal effort.
Basic styling
The process begins with some basic styling. In ArcGIS Pro, I start by loading the contour lines onto the map and opening their attribute table (see Picture 1). Within the table, there's a field named "ELEV", which contains the elevation values for each contour line. By sorting this field in ascending order, I can quickly observe that the elevation values start at 50 meters and increase in consistent intervals of 50 meters (e.g., 100m, 150m, 200m, and so on).
For this layer, I open the Symbology pane and choose Unique Values as the Primary symbology type (see Picture 2). Instead of selecting a single field under the Field 1 option, I click the green Expression button to launch the Expression Builder (also shown in Picture 2). This allows for more dynamic and customized classification of the contour lines.
As shown in Picture 2, within the Expression Builder, I select Arcade as the scripting language. In the Expression field, I enter the following code:
var elevation = $feature.ELEV;
When(
elevation % 250 == 0, '250m',
elevation % 50 == 0, '50m',
null
);
This is a straightforward script. I begin by declaring a variable named "elevation", which retrieves values from the "ELEV" field in the feature layer. Then, using Arcade's When() logical function in combination with the modulus operator (%), I define two contour classes. The logic is simple: if a line has an elevation value divisible by 250, assign it to the "250m" class; if it's divisible by 50, assign it to the "50m" class; all other values are ignored by returning null.
Once I click OK to close the Expression Builder, both classes automatically appear in the Classes section of the Symbology pane, where I can style each one individually.
For the primary contours ("250m"), I apply an 80% gray color with a line width of 0.6 pt. For the secondary contours ("50m"), I use the same gray tone but reduce the line width to 0.3 pt (see Picture 3).
With the contours now styled for a 1:50,000 scale map, the next step is labeling them. I open the Labeling pane and, under the Label Expression, I simply input the "ELEV" field (see Picture 4).
However, I quickly notice that labels are appearing on every contour line, without any logical pattern making the map cluttered and difficult to read. On a proper topographic map, labels are typically shown only on the primary contours.
To fix this, I need to create an SQL query to filter the labels. In the Labeling pane, I navigate to the SQL Query tab to open the SQL Expression Builder (see Picture 5). There, I enter the following expression:
MOD(ELEV + 250, 250) = 0
This SQL statement filters the dataset to label only every nth record, which in my case is every contour line at 250-meter intervals. As shown in Picture 5, this ensures that only the primary contours are labeled, creating a cleaner and more readable map.
Finally, I refine the label appearance using the options in the Labeling pane. This includes setting the placement to "contours" under the Position tab, and adding a halo that matches the map background color. This simple styling choice provides an effective masking effect, making the labels stand out clearly against the terrain (see Picture 6).
Diving into Scales
As mentioned earlier, the current styling works well for a 1:50,000 scale map. However, when I zoom out to a smaller scale, such as 1:100,000, the contour lines begin to look dense and visually overwhelming.
To adapt the map for this new scale, I repeat the same styling process—but this time using a different interval.
I start by duplicating the existing contour layer in the Layers pane and rename the copy to "Contours 1:100,000." Then, I update its symbology by regenerating the Unique Values classification to reflect a new interval—100 meters. As shown in Picture 7, the updated expression is:
var elevation = $feature.ELEV;
When(
elevation % 500 == 0, '500m',
elevation % 100 == 0, '100m',
null
);
With this new expression, I'm instructing ArcGIS Pro as follows: whenever a contour line has an elevation divisible by 500 meters, assign it to the class "500m"; if it's divisible by 100 meters, assign it to the class "100m"; all other lines should be ignored by returning null.
After clicking OK, the updated classes appear in the Symbology pane, and the new styling is immediately applied to the 1:100,000 contours layer on the map (see Picture 7).
Next, I need to adjust the labeling interval, since the primary contours are now at 500-meter intervals instead of 250. To do this, I reuse the same SQL expression as before, but modify the interval value:
MOD(ELEV + 500, 500) = 0
This SQL statement filters the contours to label only those at 500-meter elevations. As shown in Picture 8, this ensures that only the primary contours at this scale are labeled, keeping the map clean and easy to interpret.
I follow the exact same process for a 1:200,000 scale map. In this case, the primary contours are spaced at 1,000-meter intervals, while the secondary contours are spaced at 200 meters. I update the symbology and labeling accordingly, using the appropriate expressions and styles to match the new intervals. As illustrated in Picture 9, the contours are now cleanly generalized for this smaller scale, maintaining both clarity and readability.
The Arcade expression used to define the classes for Unique Values at this scale is as follows:
var elevation = $feature.ELEV;
When(
elevation % 1000 == 0, '1000m',
elevation % 200 == 0, '200m',
null
);
With this new expression, I'm telling ArcGIS Pro: whenever a contour line has an elevation divisible by 1,000 meters, assign it to the class "1000m"; if it's divisible by 200 meters, assign it to the class "200m"; all other lines are excluded by returning null.
The same logic applies to labeling. I update the SQL query to match the new 1,000-meter interval, so that only the primary contours are labeled. As shown in Picture 10, the result is a well-balanced, legible map suited for a 1:200,000 scale.
The SQL expression used for labeling at this scale is:
MOD(ELEV + 1000, 1000) = 0
This selects contour lines at 1,000-meter intervals, ensuring that only the primary contours are labeled. As shown in Picture 10, this results in clean, purposeful labeling that matches the scale and styling of the 1:200,000 map.
Multi-scaling
Now that I've successfully created three separate contour layers, each tailored to a specific map scale, I need to control their visibility so they display appropriately as I zoom in and out.
For the "Contours 1:50,000" layer, I go to the Feature Layer contextual tab and, under the Visibility Range group, I set the "Out Beyond" scale to 1:50,000 (see Picture 11). This ensures that this layer will only appear when zoomed in to that scale or closer.
For the "Contours 1:100,000" layer, I define a visibility window between 1:50,000 and 1:100,000. Under the same Visibility Range settings, I set "In Beyond" to 1:50,000 and "Out Beyond" to 1:100,000 (see Picture 12). This way, the layer appears only when viewing the map within that scale range.
Finally, for the "Contours 1:200,000" layer, I set the "In Beyond" scale to 1:100,000 in the Visibility Range group under the Feature Layer contextual tab (see Picture 13). This ensures that this layer only becomes visible when zoomed out beyond 1:100,000, completing a seamless transition between all three contour layers across different map scales.
As a final touch, I group all three contour layers into a single group layer. This makes it easier to manage them collectively within the Table of Contents. Now, as I zoom in and out on the map, the appropriate contour layer automatically becomes visible based on the visibility ranges previously defined.
And just like that, I've created a clean, efficient, and fully functional multi-scale contour map.
Simplifying Layers, Complicating Pro
The approach described so far is entirely valid and effective for authoring a multi-scale map. However, ArcGIS Pro offers a more advanced alternative—slightly more complex to set up, but resulting in a cleaner, more efficient layer structure in the Contents pane. After all, if you're using Pro, it makes sense to take full advantage of its powerful capabilities.
Instead of creating three separate contour layers, each with its own interval and visibility range, you can achieve the same multi-scale functionality using a single contour layer.
To do this, I add the contour layer once more. But this time, under Unique Values in the Symbology pane, I enter a combined Arcade expression that includes the logic for all three scales (see Picture 14).
The Arcade expression goes like this:
var elevation = $feature.ELEV;
When(
elevation % 1000 == 0, '1000m',
elevation % 500 == 0, '500m',
elevation % 250 == 0, '250m',
elevation % 200 == 0, '200m',
elevation % 100 == 0, '100m',
elevation % 50 == 0, '50m',
null
);
This Arcade expression, using the When() function along with the modulus (%) operator, defines a total of six classes. The first three (1000m, 500m, and 250m) represent the primary contours, while the remaining three (200m, 100m, and 50m) correspond to the secondary contours.
After clicking OK, all six classes are added to the Classes section in the Symbology pane, and the layer’s symbology is updated accordingly (see Picture 15).
I style each of the six classes individually. For the primary contours—those classified as "1000m", "500m", and "250m"—I apply an 80% gray color with a line width of 0.6 pt. For the secondary contours—"200m", "100m", and "50m"—I use the same gray color but reduce the line width to 0.3 pt (see Picture 15).
Now, for the real magic to happen, I take advantage of scale-based symbol classes. But first, I need to configure the map scales and scale properties accordingly.
In the scale list of my map, I delete the default entries and add only the scales I plan to use: 1:200,000, 1:100,000, and 1:50,000. These are the predefined scales that will be available when setting visibility ranges in the Symbology pane.
Next, I open the Scales tab (located beside the Classes tab) in the Symbology pane to access the scale range settings for each symbol class (see Picture 16).
As shown in Picture 16, I adjust the visibility ranges for each class by dragging the corresponding stops on their scale range bars in the Scales tab.
- For the "250m" and "50m" classes, I set the minimum scale to 1:50,000, without assigning a maximum scale—so they will appear only when zoomed in to large scales (1:50,000 and larger).
- For the "500m" and "100m" classes, I define a scale range between 1:100,000 (minimum) and 1:50,000 (maximum), meaning they are visible only within that specific zoom range.
- For the "1000m" and "200m" classes, I assign a maximum scale of 1:100,000, allowing them to display at smaller map scales. Optionally, you can also set a minimum scale if you want these classes to disappear when zoomed too far out.
As demonstrated in Pictures 17 and 18, each class is now tied to a specific scale range. This dynamic setup ensures that as you zoom in or out, the map automatically switches contour intervals to suit the current scale—providing a seamless, multi-scale experience.
To label this contour layer across all map scales, we use label classes, creating three distinct classes—one for each target scale.
By default, the layer includes a single label class. I begin by renaming this to "Elevation 1:50,000." For this class, I set its visibility range using the Labeling contextual tab (see Picture 19), and I define an SQL query to label only the primary contours for that scale.
As shown in Picture 19, I set the minimum scale to 1:50,000, and use the following SQL query to filter the labels:
MOD(ELEV + 250, 250) = 0
This ensures that only contours with a 250-meter interval (the primary contours for this scale) are labeled when viewing the map at 1:50,000 or larger.
To label the contours for the remaining scales, I create two additional label classes: "Elevation 1:100,000" and "Elevation 1:200,000."
For each of these, I set an appropriate visibility range and define an SQL query that targets only the primary contours relevant to that scale (see Pictures 20 and 21).
For "Elevation 1:100,000," I set the visibility range between 1:100,000 and 1:50,000, and use the SQL expression:
MOD(ELEV + 500, 500) = 0
This ensures that only the 500-meter interval contours are labeled within that scale range.
For "Elevation 1:200,000," I assign a maximum scale of 1:100,000, and use the SQL expression:
MOD(ELEV + 1000, 1000) = 0
This filters the labels to only show 1,000-meter interval contours when zoomed out to smaller scales.
With these three label classes in place, the map dynamically updates its labels based on scale, just like the symbology—resulting in a fully scale-aware contour layer.
Now, as I zoom in and out on the map, this single contour layer automatically adjusts both its contour interval and labeling—seamlessly adapting to the map scale with no need for multiple layers!
Conclusion
Crafting contour lines and their labels for multiple map scales is one of those extra steps a cartographer can take to make a map not just functional, but truly clear and visually compelling. And, as with most things in GIS, there’s more than one way to achieve that goal. I hope the approaches shared here serve as a helpful reference as you take your own maps to the next interval.
Kindest regards from Crete, Greece!
Spiros