Add Diagram Legends and Text Diagrams in PyQGIS
A diagram without a legend is decoration. Readers need to know which colour is which category and, when diagrams are sized, what a given size means. QGIS can generate both from the diagram renderer: category entries for the colours and a size legend for the magnitudes, shown in the Layers panel and in print layout legends. And when a chart is overkill — when the reader simply needs the numbers — the text diagram prints values inside a circle, rectangle or triangle on each feature.
This recipe belongs to Diagrams & Charts on Maps. It turns on category legends, builds size legends with sensible reference values, places diagram legends in print layouts, and configures text diagrams.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A layer with a diagram renderer configured, as in adding pie chart diagrams or sizing diagrams by attribute.
Turn on category entries
Category entries come straight from the diagram settings: each category's colour and label. They are off by default.
from qgis.core import QgsProject
constituencies = QgsProject.instance().mapLayersByName("constituencies")[0]
renderer = constituencies.diagramRenderer()
renderer.setAttributeLegend(True)
constituencies.setDiagramRenderer(renderer)
from qgis.utils import iface
iface.layerTreeView().refreshLayerSymbology(constituencies.id())
Breakdown: setAttributeLegend(True) adds one legend entry per category under the layer, using the colours and labels from QgsDiagramSettings. Because the renderer returned by diagramRenderer() is owned by the layer, setting it back with setDiagramRenderer makes sure the layer notices the change; refreshing the layer tree view updates the Layers panel immediately. Labels come from categoryLabels, so write them for readers — "public transport", not "bus" + "tram".
Build a size legend
For sized diagrams, a size legend shows what particular sizes mean. Choosing round, representative values is the main decision.
from qgis.core import QgsDataDefinedSizeLegend, QgsLinearlyInterpolatedDiagramRenderer
if isinstance(renderer, QgsLinearlyInterpolatedDiagramRenderer):
legend = QgsDataDefinedSizeLegend()
legend.setTitle("votes cast")
legend.setLegendType(QgsDataDefinedSizeLegend.LegendCollapsed)
legend.setClasses([
QgsDataDefinedSizeLegend.SizeClass(10000, "10,000"),
QgsDataDefinedSizeLegend.SizeClass(50000, "50,000"),
QgsDataDefinedSizeLegend.SizeClass(100000, "100,000"),
])
renderer.setDataDefinedSizeLegend(legend)
constituencies.setDiagramRenderer(renderer)
Breakdown: Explicit size classes with round values and formatted labels read far better than automatically chosen values such as 13,742. Pick values spanning the data: one near the typical feature, one near the largest, one small. Note that SizeClass takes the value, not the size; the renderer converts values to sizes with the same interpolation it uses on the map, so the legend is guaranteed to match. The collapsed type nests circles with leader lines, the compact convention for proportional symbols; the separated type lists each reference diagram as its own entry.
Put the legend in a print layout
A layout legend item picks up diagram entries from the layer like any other symbology. With auto-update on, it follows changes; turning it off lets you reorder and rename entries.
from qgis.core import (QgsLayoutItemLegend, QgsLayoutPoint, QgsLayoutSize,
QgsUnitTypes, QgsLegendStyle)
layout = QgsProject.instance().layoutManager().layoutByName("Election map")
legend_item = QgsLayoutItemLegend(layout)
legend_item.setTitle("")
map_item = layout.itemById("main_map")
legend_item.setLinkedMap(map_item)
legend_item.setAutoUpdateModel(False)
model = legend_item.model()
root = model.rootGroup()
root.removeAllChildren()
root.addLayer(constituencies)
legend_item.updateLegend()
legend_item.attemptMove(QgsLayoutPoint(210, 140, QgsUnitTypes.LayoutMillimeters))
layout.addLayoutItem(legend_item)
legend_item.adjustBoxSize()
Breakdown: Linking the legend to the map item lets it filter to layers visible in that map. Turning off auto-update and rebuilding the root group with only the diagram layer produces a legend that shows exactly the diagram entries and nothing else — base layers and reference layers are usually better described in a caption. adjustBoxSize fits the frame to the content. Adding a legend to a layout covers styling, columns and filtering in depth.
Print values with a text diagram
Sometimes the numbers themselves are the message: turnout in each constituency, the count of incidents per district. A text diagram writes one or more values inside a simple shape on each feature, more compact than a label with a background and positioned by the diagram engine.
from qgis.core import (QgsTextDiagram, QgsDiagramSettings,
QgsSingleCategoryDiagramRenderer, QgsDiagramLayerSettings)
from qgis.PyQt.QtGui import QColor, QFont
from qgis.PyQt.QtCore import QSizeF
settings = QgsDiagramSettings()
settings.categoryAttributes = ["format_number(100 * \"votes_total\" / \"electorate\", 0) || ' %'",
"format_number(\"votes_total\", 0)"]
settings.categoryColors = [QColor("#2563eb"), QColor("#2f3b35")]
settings.categoryLabels = ["turnout", "votes cast"]
settings.font = QFont("Noto Sans", 8)
settings.backgroundColor = QColor("#eff3ff")
settings.penColor = QColor("#2563eb")
settings.penWidth = 0.3
settings.size = QSizeF(14, 14)
settings.enabled = True
text_diagram = QgsTextDiagram()
text_diagram.setShape(QgsTextDiagram.Circle)
text_renderer = QgsSingleCategoryDiagramRenderer()
text_renderer.setDiagram(text_diagram)
text_renderer.setDiagramSettings(settings)
constituencies.setDiagramRenderer(text_renderer)
constituencies.setDiagramLayerSettings(QgsDiagramLayerSettings())
constituencies.triggerRepaint()
Breakdown: For text diagrams, each category expression produces the text to show, so formatting — percentages, thousands separators, units — happens in the expression. Two categories split the shape into two halves, each with its own value; three split it into thirds. The font, background colour and outline come from the diagram settings. Text diagrams take part in the same collision handling as other diagrams, which is their advantage over labels with backgrounds when both charts and numbers share a map.
Keep text diagrams readable
Text diagrams are only useful if every value can be read at the printed size. Three settings decide it: the font size relative to the shape, the contrast between text and background, and how many diagrams are allowed to collide.
settings.font = QFont("Noto Sans", 7)
settings.size = QSizeF(12, 12)
settings.backgroundColor = QColor(255, 255, 255, 230) # nearly opaque white
settings.opacity = 1.0
text_renderer.setDiagramSettings(settings)
ls = QgsDiagramLayerSettings()
ls.setShowAllDiagrams(False)
ls.setPriority(7)
constituencies.setDiagramLayerSettings(ls)
constituencies.triggerRepaint()
Breakdown: A font around 7 pt in a 12 mm shape leaves room for a five-digit number and a percentage on two lines; check the longest value you expect, not the typical one. An almost opaque white background guarantees contrast whatever the map colour underneath is, which matters more for numbers than for chart colours. Disallowing overlaps hides some values when the map is crowded; raising the priority of the text diagram layer above other labels makes the numbers win the space when they are the point of the map. When too many values disappear, a table beside the map, generated from the same layer, is often the better design.
Save the setup as a style
Legends, size classes, captions-to-be and text diagram settings take effort to get right. Saving the layer's style captures the diagram renderer, its legend settings and the layer settings in one QML file, so the next map — or next year's map — starts from the finished design.
from qgis.core import QgsMapLayer
ok, msg = constituencies.saveNamedStyle(
"/data/styles/constituency_diagrams.qml",
categories=QgsMapLayer.Diagrams | QgsMapLayer.Symbology | QgsMapLayer.Labeling)
print(ok, msg)
# later, on another layer with the same fields
other = QgsProject.instance().mapLayersByName("constituencies_2030")[0]
other.loadNamedStyle("/data/styles/constituency_diagrams.qml",
categories=QgsMapLayer.Diagrams | QgsMapLayer.Symbology | QgsMapLayer.Labeling)
other.triggerRepaint()
Breakdown: Restricting the saved categories to diagrams, symbology and labelling keeps unrelated properties — field aliases, forms, actions — out of the style, so it can be applied to any layer with the same field names. Size legend classes are stored with the diagram renderer, so the round reference values come along. Remember that an upper value saved in the style is a fixed number; if next year's data exceeds it, recompute it after loading. Saving and loading QML styles covers style categories in general.
Explain diagrams in the caption
Even a complete legend benefits from a sentence of explanation, especially for sized diagrams. A layout label generated from the same values the renderer uses keeps the caption correct after data updates.
from qgis.core import QgsLayoutItemLabel
upper = renderer.upperValue() if hasattr(renderer, "upperValue") else None
caption = QgsLayoutItemLabel(layout)
caption.setText(
"Pie area is proportional to votes cast"
+ (f"; the largest pie represents {upper:,.0f} votes." if upper else ".")
+ " Slices show each party's share.")
caption.setFont(QFont("Noto Sans", 7))
caption.attemptMove(QgsLayoutPoint(210, 190, QgsUnitTypes.LayoutMillimeters))
caption.attemptResize(QgsLayoutSize(80, 14, QgsUnitTypes.LayoutMillimeters))
layout.addLayoutItem(caption)
Breakdown: Reading the upper value from the renderer means the caption always states the scale actually used, even if the script recomputes it for new data. Saying explicitly that area is proportional prevents readers from comparing diameters. Captions like this are cheap and remove most of the misreadings that diagram maps invite.
QGIS version compatibility
Attribute legends, size legends for diagrams and text diagrams work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. QgsDataDefinedSizeLegend.SizeClass and the collapsed legend type exist since 3.4. On QGIS 4, enums are scoped, for example QgsTextDiagram.Shape.Circle and QgsDataDefinedSizeLegend.LegendType.LegendCollapsed; QgsUnitTypes.LayoutMillimeters becomes Qgis.LayoutUnit.Millimeters.
Troubleshooting
- No diagram entries in the legend.
setAttributeLegend(True)was not set, or the renderer was not reassigned to the layer. - Size legend values look odd. They were chosen automatically; set explicit size classes.
- Text diagrams show expressions instead of values. A field name was used without quotes or the expression has a syntax error; test it in the expression builder.
- The layout legend lists every layer. Auto-update is on; turn it off and rebuild the root group.
Conclusion
Turn on category entries with setAttributeLegend, build size legends with explicit round reference values that span the data, add a layout legend that shows only the diagram layer, use text diagrams when the numbers themselves matter, and add a caption that states what size means.
Frequently Asked Questions
Can the size legend show bars? The collapsed style is designed for circles; for bars, state the scale in a caption or draw a reference bar.
Can text diagrams show three values? Yes; with three categories the shape is divided into three parts.
Do diagram legends work in the QGIS Server legend? Yes; GetLegendGraphic includes diagram entries when the attribute legend is enabled.
Can I style legend text separately from map labels? Yes, through the legend item's text formats in the layout.