Highlight the Atlas Feature in an Overview Map in PyQGIS
An atlas page that shows one district at large scale answers "what is here?" but not "where is this?". Two techniques fix that: an overview map — a small map of the whole area with a frame marking the current page — and a style on the main map that makes the current feature stand out from its neighbours. Both rely on atlas variables, which QGIS updates for every page, so a single style or overview definition serves the whole series.
This recipe belongs to Automating Atlas Map Series. It adds an overview map with an extent frame, highlights the current feature and dims others with a rule-based style, masks everything outside the current feature, labels only the current feature, and checks the result in exports.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A layout with an atlas and an atlas-driven main map, as set up in configuring an atlas coverage layer.
Add an overview map with an extent frame
An overview is a second map item showing the whole area at a fixed extent, with an overview frame linked to the main map. The frame moves with the main map on every page.
from qgis.core import (QgsProject, QgsLayoutItemMap, QgsLayoutItemMapOverview, QgsLayoutPoint,
QgsLayoutSize, QgsUnitTypes, QgsFillSymbol)
layout = QgsProject.instance().layoutManager().layoutByName("District atlas")
main_map = layout.itemById("main_map")
atlas = layout.atlas()
coverage = atlas.coverageLayer()
overview = QgsLayoutItemMap(layout)
overview.setId("overview_map")
overview.attemptMove(QgsLayoutPoint(222, 12, QgsUnitTypes.LayoutMillimeters))
overview.attemptResize(QgsLayoutSize(62, 48, QgsUnitTypes.LayoutMillimeters))
overview.setLayers([coverage])
overview.setKeepLayerSet(True)
overview.zoomToExtent(coverage.extent())
overview.setFrameEnabled(True)
layout.addLayoutItem(overview)
frame = QgsLayoutItemMapOverview("current page", overview)
frame.setLinkedMap(main_map)
frame.setFrameSymbol(QgsFillSymbol.createSimple(
{"color": "255,0,0,0", "outline_color": "#b91c1c", "outline_width": "0.6"}))
overview.overviews().addOverview(frame)
Breakdown: The overview shows only the coverage layer at the full extent of all districts, and is not atlas-driven, so it stays put while the main map moves. The overview frame is linked to the main map; QGIS draws the main map's current extent as a rectangle on the overview, updating it on every page. A transparent fill with a red outline is the conventional style; a semi-transparent fill can instead dim everything outside the frame by enabling the overview's "invert" option. Keeping the overview to one simple layer makes it readable at its small size.
Highlight the current feature, dim the rest
On the main map, neighbouring districts are useful context but should not compete with the current one. A rule-based renderer compares each feature with the atlas feature.
from qgis.core import QgsRuleBasedRenderer, QgsSymbol
root = QgsRuleBasedRenderer.Rule(None)
current_sym = QgsFillSymbol.createSimple(
{"color": "238,247,244,255", "outline_color": "#0f766e", "outline_width": "0.9"})
current = QgsRuleBasedRenderer.Rule(current_sym, filterExp="$id = @atlas_featureid",
label="current district")
others_sym = QgsFillSymbol.createSimple(
{"color": "240,235,221,255", "outline_color": "#b8b1a0", "outline_width": "0.2"})
others = QgsRuleBasedRenderer.Rule(others_sym, elseRule=True, label="other districts")
root.appendChild(current)
root.appendChild(others)
coverage.setRenderer(QgsRuleBasedRenderer(root))
atlas.setHideCoverage(False)
coverage.triggerRepaint()
Breakdown: @atlas_featureid holds the id of the page's feature during atlas rendering; comparing it with $id matches exactly one feature. The else rule catches everything else. Because the variable changes per page, one renderer highlights the right district on every page. Outside atlas rendering — in the normal map canvas — the variable is NULL, so every district draws with the else style, which is a reasonable default. The coverage layer must be visible on the main map for this to work, so hiding coverage is turned off. More on rules in rule-based renderers.
Mask everything outside the current feature
A stronger effect fades all data outside the current district — roads, buildings, land use — not just neighbouring boundaries. An inverted polygon renderer drawing a semi-transparent fill everywhere except the current feature does it.
from qgis.core import QgsInvertedPolygonRenderer, QgsVectorLayer
mask_layer = QgsVectorLayer(coverage.source(), "atlas mask", coverage.providerType())
mask_root = QgsRuleBasedRenderer.Rule(None)
mask_rule = QgsRuleBasedRenderer.Rule(
QgsFillSymbol.createSimple({"color": "255,255,255,190", "outline_style": "no"}),
filterExp="$id = @atlas_featureid")
mask_root.appendChild(mask_rule)
mask_layer.setRenderer(QgsInvertedPolygonRenderer(QgsRuleBasedRenderer(mask_root)))
QgsProject.instance().addMapLayer(mask_layer, False)
QgsProject.instance().layerTreeRoot().insertLayer(0, mask_layer)
main_map.setLayers([mask_layer] + [l for l in main_map.layers() if l.id() != mask_layer.id()])
Breakdown: An inverted polygon renderer fills the area outside the polygons its inner renderer draws; restricting the inner rule to the atlas feature means the white wash covers everything except the current district. A second copy of the coverage layer carries the mask so the boundary styling on the original stays independent. Putting the mask first in the map's layer list draws it on top of the data. The alpha value (190 of 255) controls how strongly the surroundings fade; context should stay faintly visible, not disappear.
Label only the current feature
District names on every neighbouring polygon clutter the page. A labelling rule restricted to the atlas feature shows only the current name, while neighbours stay unlabelled or get smaller labels.
from qgis.core import (QgsPalLayerSettings, QgsRuleBasedLabeling, QgsTextFormat)
from qgis.PyQt.QtGui import QFont
def label_settings(size, bold):
s = QgsPalLayerSettings()
s.fieldName = "name"
fmt = QgsTextFormat()
f = QFont("Noto Sans", size)
f.setBold(bold)
fmt.setFont(f)
fmt.setSize(size)
s.setFormat(fmt)
return s
lab_root = QgsRuleBasedLabeling.Rule(None)
lab_root.appendChild(QgsRuleBasedLabeling.Rule(label_settings(14, True),
filterExpression="$id = @atlas_featureid"))
lab_root.appendChild(QgsRuleBasedLabeling.Rule(label_settings(7, False),
filterExpression="$id <> @atlas_featureid"))
coverage.setLabeling(QgsRuleBasedLabeling(lab_root))
coverage.setLabelsEnabled(True)
Breakdown: Two labelling rules mirror the two style rules: a large bold label for the current district and a small label for neighbours, which gives orientation without competing. Dropping the second rule leaves neighbours unlabelled entirely. The same pattern works for any layer — label only the schools inside the current district with within($geometry, @atlas_geometry). See rule-based labels for the labelling side in detail.
Show items only on some pages
Some context is only needed sometimes: an inset for very small districts, a "continued on next page" note for districts split across sheets, an extra legend entry where a rare land use appears. Layout items can be shown or hidden per page with a data-defined exclusion expression evaluated against the atlas feature.
from qgis.core import QgsLayoutObject, QgsProperty
inset = QgsLayoutItemMap(layout)
inset.setId("detail_inset")
inset.attemptMove(QgsLayoutPoint(222, 66, QgsUnitTypes.LayoutMillimeters))
inset.attemptResize(QgsLayoutSize(62, 48, QgsUnitTypes.LayoutMillimeters))
inset.setAtlasDriven(True)
inset.setAtlasScalingMode(QgsLayoutItemMap.Auto)
inset.setAtlasMargin(0.4)
layout.addLayoutItem(inset)
inset.dataDefinedProperties().setProperty(
QgsLayoutObject.ExcludeFromExports,
QgsProperty.fromExpression("area(@atlas_geometry) > 2000000"))
Breakdown: The inset is atlas-driven with a generous margin, showing the current district with more surrounding context. The data-defined "exclude from exports" property hides it on pages where the district is larger than two square kilometres, so the inset appears only for small districts that the main map renders at a scale where context is lost. Any expression over @atlas_feature or @atlas_geometry works, which makes page-specific layout elements possible without separate layouts. Remember that the main and overview maps are unaffected; only the item carrying the property toggles.
Check the result in exports
Atlas variables only have values during atlas rendering, so the canvas cannot show the final look. Export a few pages as images to check highlighting, masking and labels.
from qgis.core import QgsLayoutExporter
atlas.beginRender()
for i in (0, atlas.count() // 2, atlas.count() - 1):
atlas.seekTo(i)
QgsLayoutExporter(layout).exportToImage(
f"/data/maps/preview_{i:03d}.png", QgsLayoutExporter.ImageExportSettings())
atlas.endRender()
print("previews written")
Breakdown: beginRender and seekTo put the layout into atlas mode for a specific page, so expressions using atlas variables evaluate as they will in the final export. Previewing the first, middle and last page catches most problems, including edge districts where the overview frame touches the overview map's border. Low-resolution PNGs are quick to write and view.
QGIS version compatibility
Overview maps, atlas variables, rule-based renderers and labelling, and the inverted polygon renderer work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. On QGIS 4, unit enums use Qgis.LayoutUnit.Millimeters; the variable names @atlas_featureid, @atlas_feature and @atlas_geometry are unchanged.
Troubleshooting
- No highlight in exports. The coverage layer is hidden by the atlas; turn
setHideCoverageoff when it carries the highlight style. - The overview frame is missing. The frame is not linked to the main map, or the overview is atlas-driven too.
- Everything is masked. The mask rule's expression does not match; check
$idagainst@atlas_featureid. - Highlight works in the designer but not from Python. Exports were made without
beginRender.
Conclusion
Add a fixed overview map with a frame linked to the main map, highlight the current feature with a rule on $id = @atlas_featureid and dim the rest with an else rule, mask the surroundings with an inverted polygon renderer on a copy of the coverage layer, label only the current feature prominently, and check sample pages from an atlas render rather than the canvas.
Frequently Asked Questions
Can the overview be another layer, such as a basemap? Yes — any layers; keep it simple enough to read at small size.
Can I highlight on a different layer than the coverage?
Yes: compare with @atlas_geometry, for example intersects($geometry, @atlas_geometry).
Does this work with QGIS Server atlas requests? Yes, atlas variables are available wherever atlases render.
Can the overview zoom to the region around each page instead of the whole area? Make the overview atlas-driven with a large margin, or a fixed scale several times smaller than the main map; it then follows each page while still showing wider context.
How do I fade instead of mask completely? Lower the mask fill's alpha so surroundings stay visible.