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.

Why one scale for all featuresTwo districts with bar diagrams. With each diagram scaled to its own maximum, a small district's bars look as tall as a large district's, hiding the difference in magnitude. With a shared scale, the small district's bars are visibly shorter, so bars can be compared across the map as well as within each diagram.Bars only compare across features on one scalelargesmallown maximum each: misleadinglargesmallshared scale: honest

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.

Orientation and axisFour orientations: up, down, left and right. Up is the default and reads most naturally. Right suits long horizontal arrangements. An axis line at the base of the bars, available on recent releases, anchors them so readers can see where zero is; spacing between bars keeps adjacent colours distinct.Up is the default; add a base lineup + axisdownrightleft

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.

A fixed, meaningful orderBars ordered by overall size, largest first, form a descending staircase in a typical district, so a district where a later bar is taller than an earlier one stands out immediately. Random order produces jagged diagrams in every district, and nothing stands out.Order once, then exceptions stand outordered: the third bar breaks the staircaserandom: every diagram looks jagged

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 penColor and penWidth.

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.