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.

What a diagram legend containsA diagram legend has two parts. Category entries list each colour with its label, generated from the diagram settings. A size legend shows reference diagrams for chosen values, either as separate items or collapsed into nested circles with leader lines. Both appear under the layer in the Layers panel and in print layout legends.Colours, then sizescategory entriesparty Aparty Bparty Cothersize legend (collapsed)100,00050,00010,000

Prerequisites

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.

Choosing reference valuesAutomatic legend values come from the data range and produce awkward numbers such as 13,742 and 87,219. Chosen round values such as 10,000, 50,000 and 100,000 are easy to read and compare, and should span the data from a typical feature to the largest.Round numbers that span the dataautomatic13,742 · 47,903 · 87,219precise, unreadablechosen10,000 · 50,000 · 100,000round, comparable

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.

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.

Text diagram shapesA text diagram draws one to three values inside a circle, rectangle or triangle. With two categories, the shape is split by a line and each value sits in its own half, each optionally coloured by its category colour. Font and background colours follow the diagram settings.Values in a shape72 %41,2001,28438circle, two valuesrectangletriangle

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.