Add Histogram Bar Diagrams in PyQGIS
Bars are the chart people read most accurately. Comparing the lengths of two bars is far easier than comparing two pie slices, which is why a small bar chart on each feature often communicates better than a pie: monthly rainfall at each station, the number of jobs in each sector per district, emissions by source in each city. QGIS calls this diagram type a histogram, though it draws one bar per category rather than a frequency distribution.
This recipe belongs to Diagrams & Charts on Maps. It builds a histogram diagram renderer, fixes a common scale so bars are comparable across features, controls orientation, width and spacing, adds axes, and keeps the result readable when printed.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A layer with numeric fields in the same unit — counts, amounts or rates — that should be compared both within and across features.
Build a histogram diagram
The structure is the same as for pies: a diagram type, settings with categories, a renderer and layer settings. For bars, the linearly interpolated renderer is the natural choice, because it is what maps values to bar lengths on a shared scale.
from qgis.core import (QgsProject, QgsHistogramDiagram, QgsDiagramSettings,
QgsLinearlyInterpolatedDiagramRenderer, QgsDiagramLayerSettings)
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtCore import QSizeF
regions = QgsProject.instance().mapLayersByName("regions")[0]
fields = ["jobs_industry", "jobs_services", "jobs_public", "jobs_agri"]
settings = QgsDiagramSettings()
settings.categoryAttributes = [f'"{f}"' for f in fields]
settings.categoryColors = [QColor(c) for c in ("#b45309", "#2563eb", "#0f766e", "#15803d")]
settings.categoryLabels = ["industry", "services", "public", "agriculture"]
settings.barWidth = 2.0 # mm
settings.penColor = QColor("#2f3b35")
settings.penWidth = 0.1
settings.enabled = True
renderer = QgsLinearlyInterpolatedDiagramRenderer()
renderer.setDiagram(QgsHistogramDiagram())
renderer.setDiagramSettings(settings)
regions.setDiagramRenderer(renderer)
regions.setDiagramLayerSettings(QgsDiagramLayerSettings())
Breakdown: Categories are expressions, so each field name is wrapped in double quotes. barWidth is the width of each bar in millimetres; the diagram's total width follows from the number of bars. A thin dark outline keeps light-coloured bars visible against a pale map. Nothing is scaled yet — that is the next step, and the most important one.
Set a shared scale
Bar length is set by mapping a value range to a length range. The upper value should be the largest single category value across all features, so the tallest bar on the map has the maximum length and everything else is proportional to it.
max_value = max(max(f[name] or 0 for name in fields) for f in regions.getFeatures())
renderer.setClassificationAttributeExpression(
"max(" + ", ".join(f'coalesce("{f}", 0)' for f in fields) + ")")
renderer.setLowerValue(0)
renderer.setUpperValue(max_value)
renderer.setLowerSize(QSizeF(0, 0))
renderer.setUpperSize(QSizeF(0, 18)) # tallest bar: 18 mm
renderer.setDiagramSettings(settings)
regions.setDiagramRenderer(renderer)
regions.triggerRepaint()
print("tallest bar represents", max_value)
Breakdown: For histogram diagrams, the interpolated size governs bar length: a bar's length is its value divided by the upper value, times the upper size. Starting both value and size at zero keeps lengths proportional — a bar half as long means half the value. Setting the upper value from the data's true maximum, computed across every category of every feature, is what makes bars comparable across the map. Hard-code the upper value instead when several maps must share one scale, for example the same regions in different years.
Orientation, spacing and axes
Bars can grow up, down, left or right. Upward bars read most naturally; horizontal bars suit features arranged in a north–south band where vertical bars would collide.
settings.diagramOrientation = QgsDiagramSettings.Up
settings.setSpacing(0.4) # mm between bars
settings.setSpacingUnit(settings.sizeType) # same unit as sizes
settings.setShowAxis(True)
from qgis.core import QgsLineSymbol
settings.setAxisLineSymbol(QgsLineSymbol.createSimple({"color": "#2f3b35", "width": "0.25"}))
renderer.setDiagramSettings(settings)
regions.triggerRepaint()
Breakdown: A small gap between bars separates adjacent colours, which matters most when two categories have similar hues. The axis line draws a base under the bars, so a missing bar reads as zero rather than as an omission; it was added in the 3.12 series along with bar spacing. Use the same orientation for every diagram on a map; mixing them defeats comparison.
Make values readable
Bars show relative size; a reader still needs to know what the tallest bar means. A legend entry for size, consistent units and a note on the map close the gap.
renderer.setAttributeLegend(True) # categories in the legend
print(f"tallest bar = {max_value:,.0f} jobs; bars scale linearly from zero")
Breakdown: With the attribute legend enabled, the layer's legend entry lists each category with its colour, which is all a print legend needs for the colours. For magnitude, the simplest honest device is a sentence in the map's caption — "tallest bar = 48,200 jobs" — or a small reference diagram drawn in the layout. Adding legends for diagrams builds a proper size legend.
Keep diagrams readable at print size
Diagrams are sized in millimetres, so they look the same on screen and on paper — and what fits on a monitor at one zoom level may be crowded on an A4 export at another scale.
from qgis.core import QgsDiagramLayerSettings
ls = QgsDiagramLayerSettings()
ls.setPlacement(QgsDiagramLayerSettings.OverPoint)
ls.setShowAllDiagrams(False)
regions.setDiagramLayerSettings(ls)
settings.scaleBasedVisibility = True
settings.minimumScale = 1_500_000
renderer.setDiagramSettings(settings)
regions.triggerRepaint()
Breakdown: Hiding colliding diagrams keeps the map legible; scale-based visibility stops diagrams appearing when the map is zoomed out too far for them to fit. For a print layout at a fixed scale, check the export rather than the canvas — a layout map item renders diagrams at its own scale. If too many diagrams are dropped, reduce the bar width and upper size rather than allowing overlaps.
Order and colour categories deliberately
The order of bars is the order of categoryAttributes, and it is the same in every diagram on the map. A deliberate order — by size across the whole dataset, or by a natural sequence such as months or age bands — lets readers learn the pattern once and then scan the map for departures from it.
totals = {f: sum((feat[f] or 0) for feat in regions.getFeatures()) for f in fields}
ordered = sorted(fields, key=lambda f: totals[f], reverse=True)
colour_of = dict(zip(fields, settings.categoryColors))
label_of = dict(zip(fields, settings.categoryLabels))
settings.categoryAttributes = [f'"{f}"' for f in ordered]
settings.categoryColors = [colour_of[f] for f in ordered]
settings.categoryLabels = [label_of[f] for f in ordered]
renderer.setDiagramSettings(settings)
regions.triggerRepaint()
Breakdown: Ordering by each category's total across all features makes the typical diagram a descending staircase, so any region whose bars break the staircase — services larger than industry where industry usually dominates — is visible at a glance. Colours travel with their categories through the reordering, so the same sector keeps the same colour on every map in a series. For naturally ordered categories, such as months or age groups, keep the natural order instead.
Bars versus other diagram types
Histogram diagrams are the right choice when readers should compare the same category between features — industrial jobs here versus there — or compare categories with each other in absolute terms. When the whole matters as much as the parts, stacked bar diagrams show the total as one bar divided into segments. When only proportions matter, pies are more compact. With more than five or six categories, no diagram type remains readable at map size — group categories, or move the detail into a chart beside the map.
QGIS version compatibility
QgsHistogramDiagram and the interpolated renderer work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Axis lines and bar spacing require 3.12 or newer. On QGIS 4, orientation is QgsDiagramSettings.DiagramOrientation.Up and placement is QgsDiagramLayerSettings.Placement.OverPoint.
Troubleshooting
- All bars have the same length. The renderer is single-category with a fixed size; use the interpolated renderer with a shared upper value.
- Bars exceed the upper size. A value is larger than
setUpperValue; recompute the maximum across all categories. - Diagrams disappear when zoomed out. Scale-based visibility is hiding them, or collisions are dropping them.
- Bars are invisible on a pale base. Add an outline with
penColorandpenWidth.
Conclusion
Use QgsHistogramDiagram with the linearly interpolated renderer, set the upper value to the largest category value across all features so bars share one scale, keep zero as the base, add spacing and an axis line, state what the tallest bar represents, and manage collisions and scale ranges for print.
Frequently Asked Questions
Why is it called a histogram? QGIS's naming; it draws one bar per category, which is a bar chart rather than a frequency histogram.
Can bars show negative values? Not meaningfully; bar length is derived from the value's ratio to the upper value. Show change with a diverging choropleth instead.
Can each bar have a label with its value? Not directly. Use a text diagram or labels with an expression next to the diagram.
Can I use the same scale on maps of different years? Yes — set the upper value to the maximum over all years and reuse it.