How to make a filled contour map using QGIS and Python

Article publication date: 11th June 2023.

Motivation

A small region of a hypsometric (colour-filled) contour map of the region around Dolgellau and Cader Idris in Wales. A portion of a map showing the topography with colour-filled contours (hypsometric tints). My cycle route is shown in pencil. I stayed at Kings Youth Hostel near Dolgellau and walked up Cader Idris. From Bartholomew Half-Inch Map Series 22 (Mid Wales), 1973. When I was 19, I had my first solo adventure: a 1,001-mile (1,611-km) bike ride around Wales. I planned and navigated with Bartholomew maps, which my father had bought for 30 pence each in the early 1970s. Even though Wales doesn’t change fast, they were a bit out of date: some towns had grown, and a few bridges had vanished. But they had one major advantage: the topography was shown in colour, using filled contours. These hypsometric colours were pioneered by John Bartholomew Junior the 1800s. They made it easy for me to visualise the landscape and plan a route that was not too hilly.

Of course, filled contours are not always the best way to display the topography. Shading the map with range of colours can make it hard for other symbols to show up, so multi-purpose maps often just use contour lines, perhaps enhanced with shaded relief. However, for a single-purpose hiking or biking map, I find that coloured contours are the most helpful base map.

Unfortunately, QGIS does not handle vector filled contours by default. However, they can be approximated with a raster, or generated with a little bit of help from Python.

What’s possible with contours in QGIS

A two-panel image. Both panels show an unlabelled raster of the topography of a square region including the crater lake of Katmai Volcano, with lighter colours indicating higher elevations. The right panel has grey elevation contours every 200 metres. Left: The topographic raster image used for the examples in this article. It shows the Katmai Volcano in Alaska with the magma colour ramp. Right: The same raster, with 200-metre contours added. We’ll start by illustrating what can be done in QGIS, using a digital elevation model (DEM) of Katmai Volcano in Alaska, downloaded with the SRTM-Downloader plugin. Entering a map extent covering the volcano will download two tiles, which can be merged into a single DEM using Raster > Miscellaneous > Merge. I clip the raster to the area of interest using Raster > Extraction > Clip to speed up the contour calculation.

We can generate contour lines with Raster > Extraction > Contour, which calls an underlying function from the GDAL library. To keep the output simple, I used a 200-metre contour interval.

A two-panel image. Both panels show the topographic raster from the previous image. The left panel applies a discrete colour ramp to the raster to give the impression of filled contours. The right panel shows an attempt to use the Lines to polygons algorithm in QGIS, leading to bad results where contours touching the edge of the map are joined up incorrectly. Left: The raster colour ramp has been changed from continuous to discrete colours, giving the impression of filled contours. Right: Attempting to use the Lines to polygons function leads to incorrect results wherever the contour lines intersect the edge of the image. This is already quite close to a filled-contour map, except that the colours between the contours are not constant. This makes it harder to distinguish between the different contour levels. To approximate filled contours, we can apply a discrete colour ramp in the raster layer’s symbology. By choosing Interpolation > Discrete, Min: 0, Max: 2000, Mode: Equal interval and Classes: 10, the raster will appear as bands of colour which align with our contours.

The raster approximation to filled contours is probably good enough for most purposes. However, in some cases it is better to have true filled contours, represented by vector polygons. These will look better under high magnification, have useful geometric properties, and in some cases lead to smaller file sizes.

The solution is to convert the contour lines into polygons, using Vector > Geometry tools >Lines to polygons. However, the resulting polygons (shown here in orange) are usually incorrect. The major problem is that the algorithm does not know how to close the contour lines when they end at the edge of the raster. We’ll now see how to deal with this using a bit of Python code.

Getting vector filled polygons by adding some Python

A two-panel image. The left panel shows the result of applying the contouring algorithm from QGIS to a tapered raster, which successfully generates closed contours, except that contour holes are not captured correctly. The right panel shows the contours once the contour holes have been detected and subtracted. Left: Filled contours generated with Lines to polygons, after tapering the DEM (the tapered region is outside the image frame). Right: The filled contours after processing to remove the holes. Note the volcano’s crater lake now has the correct colour. The solution is to ‘dig a moat’ around the edge of our digital elevation model, so that all of the contours are closed. We taper the outer part of the raster down to some arbitrary low value. I wrote a Python function to do this called taper_geotiff(), which is part of my GitHub repository geotiff_helper. You must have the module osgeo installed. Although the concept is not very complex, care is required to ensure that there is a smooth transition down to the edge. The terminal command looks like python3 taper_geotiff.py raster.tif raster_tapered.tif 50 500 where 50 is the width of the taper zone (in pixels), and 500 is the depth of the taper (in the same units as the raster).

We then use the same steps as before to generate contour lines and turn them into polygons (Raster > Extraction > Contour, Vector > Geometry tools > Lines to polygons). Since the polygons will be opaque, you need to make sure that the lower contours are rendered first, by going to Properties > Symbology > Layer rendering > Control feature rendering order and selecting Ascending sorting of variable ELEV. You can then style the polygons as you would any QGIS polygon. The easiest way to apply a colour ramp is to set Properties > Symbology > Categorized. Choose a colour ramp and click Classify. You may want to delete any lower contours that are not visible, as well as the all other values style, to make full use of the range of colours (reset the colour ramp to ensure this). The result should be beautiful vector polygons representing filled contours.

Filled contours with holes

For some unusual landscapes, such as volcanic craters, some of the contour lines have holes (enclosed regions of lower elevation). Holes can also be created by the tapering procedure described above. When contours have holes, the filled contours will not render properly, as the inner contour line is displayed simply as another filled polygon.

This problem can also be fixed in Python, by detecting the holes (contour lines completely enclosed by other contour lines of the same height) and ‘subtracting’ the inner contour from the outer one. The algorithm uses the areas of the polygons, so it is best to put the contours into a projected coordinate system (if they are not already) using Vector > Data Management Tools > Reproject Layer. For Katmai Volcano, UTM Zone 5N is a suitable projection.

The function to do this, find_contour_holes() can be found in my GitHub repository contour_helper, and used as follows: python3 find_contour_holes.py contours_filled.gpkg contours_filled_no_holes.gpkg

Finishing touches

The final result of the steps described here, showing the topography as discrete colour-filled contours, with the tapered region trimmed off and contour holes removed. The contour lines use varying shades of grey to achieve constant contrast with the fill colours. The final result of the steps described in this tutorial. The topographic raster has been converted to contours: discrete colour-filled vector polygons. The tapered region has been trimmed off, the contour holes have been subtracted, and the grey colours of the contour lines are adjusted to make the lower ones darker, achieving a constant contrast level even as the colours change. If you tapered the raster, don’t forget to clip this region of the contours. You can do this by creating a new layer with a rectangle feature and applying Vector > Geoprocessing tools > Clip.

Controlling the symbology of the polygons from the QGIS menus can be tedious and restrictive. I sometimes use Python scripts to achieve a more automatic or fine-tuned result. For example, I like to adjust the brightness of contour lines (polygon outlines) to be similar to the brightness of the adjacent contour colours. This leads to contours that are always visible but never too heavy.

If you’d like to apply this kind of styling to your own maps, or see how it works, you can download and modify the Python scripts from my GitHub repository qgis_style. Here I used the function make_style_files() with the following input file:task make_filled_contours z_min 0.0 d_z 200.0 n_bins 10 colour_ramp_type pyplot_linear pyplot_ramp_name magma outline_contrast 0.15 outline_style solid outline_width 2.0 outline_width_unit Pixel dir_out styles/ qgis_version 3.22.9-Białowieża style_file_name filled_contours

Final thoughts

In this article, I’ve shown how to make a hypsometric map illustrating the topography with discrete colour-shaded contours, either as a raster layer with vector contour lines, or as a vector layer of filled polygons. This approach is not limited to topography: any raster that can be processed with Raster > Extraction > Contour could be treated in the same way, for example a map of surface temperature.

In future, as I learn more about the inner workings of QGIS, I hope to add the filled contour functionality to the software, and extend it to global rasters. Please feel free to contribute to these goals!