Add Stacked Bar Diagrams in PyQGIS
Pies show composition but not size; bar charts show size but make the whole hard to see. A stacked bar does both: one bar per feature whose length is the total, divided into segments for each part. Housing stock by tenure, energy use by source, waste by stream — whenever readers need to see both "how much" and "made of what", a stacked bar diagram is usually the clearest choice that QGIS can draw on a map.
This recipe belongs to Diagrams & Charts on Maps. It builds a stacked bar diagram renderer, orders and colours segments, scales bar length by the total, switches to 100 % bars when only composition matters, and shows how QGIS 3.40's stacked diagram combines several diagram types on one feature.
Prerequisites
- QGIS 3.34 LTR or newer for
QgsStackedBarDiagram(available since 3.12); 3.40 or newer for the combinedQgsStackedDiagram. - A layer with numeric fields that are additive parts of a total, all in the same unit.
Build a stacked bar diagram
The setup mirrors other diagram types. With a single-category renderer every bar has the same length, which shows composition only; the next section scales length by the total.
from qgis.core import (QgsProject, QgsStackedBarDiagram, QgsDiagramSettings,
QgsSingleCategoryDiagramRenderer, QgsDiagramLayerSettings)
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtCore import QSizeF
wards = QgsProject.instance().mapLayersByName("wards")[0]
parts = [("owner_occ", "owner occupied", "#0f766e"),
("private_rent", "private rented", "#2563eb"),
("social_rent", "social rented", "#b45309")]
settings = QgsDiagramSettings()
settings.categoryAttributes = [f'coalesce("{f}", 0)' for f, _, _ in parts]
settings.categoryLabels = [label for _, label, _ in parts]
settings.categoryColors = [QColor(c) for _, _, c in parts]
settings.diagramOrientation = QgsDiagramSettings.Right
settings.barWidth = 2.5
settings.size = QSizeF(20, 2.5)
settings.penColor = QColor("#ffffff")
settings.penWidth = 0.2
settings.enabled = True
renderer = QgsSingleCategoryDiagramRenderer()
renderer.setDiagram(QgsStackedBarDiagram())
renderer.setDiagramSettings(settings)
wards.setDiagramRenderer(renderer)
wards.setDiagramLayerSettings(QgsDiagramLayerSettings())
wards.triggerRepaint()
Breakdown: coalesce keeps a NULL part from removing the whole bar, since expression arithmetic with NULL gives NULL. A horizontal orientation (Right) suits stacked bars on maps because labels and features are usually wider than tall and horizontal bars sit comfortably under a place name. The bar width is the thickness of the bar; with a fixed-size renderer, size sets the full bar length. White separators between segments keep adjacent colours distinct.
Scale bar length by the total
With the linearly interpolated renderer, bar length follows the sum of the parts, so the diagram shows magnitude as well as composition.
from qgis.core import QgsLinearlyInterpolatedDiagramRenderer
total_expr = " + ".join(f'coalesce("{f}", 0)' for f, _, _ in parts)
max_total = max(sum((feat[f] or 0) for f, _, _ in parts) for feat in wards.getFeatures())
scaled = QgsLinearlyInterpolatedDiagramRenderer()
scaled.setDiagram(QgsStackedBarDiagram())
scaled.setDiagramSettings(settings)
scaled.setClassificationAttributeExpression(total_expr)
scaled.setLowerValue(0)
scaled.setUpperValue(max_total)
scaled.setLowerSize(QSizeF(0, 2.5))
scaled.setUpperSize(QSizeF(24, 2.5))
wards.setDiagramRenderer(scaled)
wards.triggerRepaint()
print("longest bar =", max_total, "dwellings")
Breakdown: The classification expression is the total, and the interpolation maps zero to zero length and the largest total to 24 mm. Width (the bar's thickness) stays fixed at 2.5 mm through the size pairs — only the length changes. Starting at zero keeps lengths proportional, which is what makes bars comparable; a non-zero lower value would exaggerate differences. Use the same upper value on every map in a series so bars remain comparable between maps.
Percentage bars for pure composition
When totals differ wildly — a city ward and a rural ward — scaled bars make the small ward's composition unreadable. Bars of equal length showing percentages put the focus entirely on composition.
shares = [f'100 * coalesce("{f}", 0) / nullif({total_expr}, 0)' for f, _, _ in parts]
settings.categoryAttributes = shares
settings.size = QSizeF(20, 2.5)
renderer.setDiagramSettings(settings)
wards.setDiagramRenderer(renderer) # the fixed-size renderer from above
wards.triggerRepaint()
Breakdown: Each segment's expression computes its percentage of the ward total; nullif turns a zero total into NULL, so empty wards produce no bar instead of a division error. With the fixed-size renderer every bar is 20 mm long and segments show shares directly — the map equivalent of a 100 % stacked bar chart. Pair this map with a separate choropleth or proportional symbol map of totals if magnitude still matters.
Choose segment order and colours
Segment order is the same in every bar. Putting the category of most interest at the start — the left end of horizontal bars — gives it a common baseline, so it can be compared precisely across features; segments in the middle float and are harder to compare.
focus = "social_rent"
ordered = [p for p in parts if p[0] == focus] + [p for p in parts if p[0] != focus]
settings.categoryAttributes = [f'coalesce("{f}", 0)' for f, _, _ in ordered]
settings.categoryLabels = [label for _, label, _ in ordered]
settings.categoryColors = [QColor(c) for _, _, c in ordered]
scaled.setDiagramSettings(settings)
wards.triggerRepaint()
Breakdown: Only the first segment shares a baseline across bars; every later segment starts wherever the previous one ended. Moving the focus category to the front therefore turns a hard comparison into an easy one. Use a strong colour for the focus category and quieter, related colours for the rest, so the eye goes where the story is.
Label bars with their totals
A scaled bar shows relative magnitude, but readers often want the actual number for a few places they care about. Labels on the same layer can carry the total, placed next to the diagram, while the diagram itself stays uncluttered.
from qgis.core import (QgsPalLayerSettings, QgsTextFormat, QgsVectorLayerSimpleLabeling,
QgsTextBufferSettings)
from qgis.PyQt.QtGui import QFont
label = QgsPalLayerSettings()
label.isExpression = True
label.fieldName = f"\"ward_name\" || '\\n' || format_number({total_expr}, 0)"
fmt = QgsTextFormat()
fmt.setFont(QFont("Noto Sans", 8))
fmt.setSize(8)
buf = QgsTextBufferSettings()
buf.setEnabled(True)
buf.setSize(0.8)
fmt.setBuffer(buf)
label.setFormat(fmt)
label.placement = QgsPalLayerSettings.AroundPoint
label.dist = 2.0
wards.setLabeling(QgsVectorLayerSimpleLabeling(label))
wards.setLabelsEnabled(True)
wards.triggerRepaint()
Breakdown: The label expression joins the ward name and its formatted total on two lines, reusing the same total expression that sizes the bars, so the number always matches the bar. format_number adds thousands separators. Placing labels around the point with a small offset, and letting the labelling engine treat diagrams as obstacles, keeps text off the bars. A text buffer keeps the label legible over the base map. Label placement and collisions covers fine-tuning when labels and diagrams compete for space.
Combine diagrams on one feature
QGIS 3.40 introduced QgsStackedDiagram, which draws several diagrams — of any type — side by side or one above another for each feature. That allows, for example, a bar for dwellings by tenure next to a pie of household size.
from qgis.core import QgsStackedDiagram, QgsPieDiagram
stacked = QgsStackedDiagram()
stacked.setStackedDiagramMode(QgsStackedDiagram.Horizontal)
stacked.setStackedDiagramSpacing(1.0)
pie_settings = QgsDiagramSettings()
pie_settings.categoryAttributes = ['"hh_1"', '"hh_2"', '"hh_3plus"']
pie_settings.categoryColors = [QColor("#b45309"), QColor("#d97706"), QColor("#92400e")]
pie_settings.categoryLabels = ["1 person", "2 people", "3+"]
pie_settings.size = QSizeF(6, 6)
pie_settings.enabled = True
stacked.addSubDiagram(QgsStackedBarDiagram(), settings)
stacked.addSubDiagram(QgsPieDiagram(), pie_settings)
combo = QgsSingleCategoryDiagramRenderer()
combo.setDiagram(stacked)
combo.setDiagramSettings(settings)
wards.setDiagramRenderer(combo)
wards.triggerRepaint()
Breakdown: Each sub-diagram keeps its own settings, so the bar and the pie have independent categories, colours and sizes. The container arranges them horizontally or vertically with a spacing in the diagram's size unit. Use this sparingly: two small charts per feature is about the limit before a map becomes a dashboard. Check the exact method names with help(QgsStackedDiagram) on your version, as the class is new.
Check the result with a reader's eyes
Before publishing, look at the map the way a reader will. Can you find the ward with the most social housing without reading any numbers? Can you tell which wards are large and which small? Does any bar run into a neighbour or off the edge of the page? Stacked bars reward this check because their two messages — total and mix — can compete: if every bar is long, composition is easy to read but totals blur together; if lengths vary enormously, small wards' segments become slivers. When one message clearly matters more, favour it — percentage bars for composition, a separate proportional symbol map for totals — rather than forcing one diagram to do both badly. A quick export of the layout to PDF at the final size, viewed at 100 %, is the most reliable test of all.
QGIS version compatibility
QgsStackedBarDiagram requires QGIS 3.12 or newer and is available on 3.34 LTR, 3.40 LTR and QGIS 4. QgsStackedDiagram requires 3.40. On QGIS 4, orientation and mode enums are scoped, for example QgsDiagramSettings.DiagramOrientation.Right.
Troubleshooting
- Bars all have the same length. A single-category renderer is in use; switch to the interpolated renderer for magnitude.
- Some wards have no bar. A part is NULL and was not wrapped in
coalesce, or the total is zero in a percentage bar. - Segments look different in the legend. Colours and labels were not reordered together with the attributes.
AttributeErroronQgsStackedDiagram. QGIS is older than 3.40.
Conclusion
Use QgsStackedBarDiagram when both totals and composition matter, scale length by the total from zero with the interpolated renderer, switch to equal-length percentage bars when totals vary too much, put the focus category first for a common baseline, and combine diagrams with QgsStackedDiagram on 3.40 or newer only when two charts genuinely help.
Frequently Asked Questions
Vertical or horizontal bars? Horizontal usually fits better under labels and between features; vertical suits point features in sparse areas.
Can segments have value labels? Not inside the diagram. Add labels with an expression that formats the values, placed next to the bar.
How many segments are too many? More than five or six become hard to tell apart at map size. Group categories first.
Can a stacked bar show negative parts? No. Diagrams assume non-negative values.