Size Diagrams by Attribute in PyQGIS
A diagram that only shows composition treats every feature as equally important. Most maps need magnitude too: a pie of party votes is more informative when large constituencies get large pies, a bar of energy sources more honest when a city's bar dwarfs a village's. Size is a powerful visual variable and an easy one to misuse — the same numbers can look like a small difference or a dramatic one depending on how size is computed.
This recipe belongs to Diagrams & Charts on Maps. It configures the linearly interpolated diagram renderer, explains the choice between scaling by area and by diameter, handles outliers, keeps one scale across several maps, and shows when a data-defined size expression is the better tool.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A layer with a diagram already configured — a pie, bar or stacked bar — and a numeric field or expression to size by.
Configure the interpolated renderer
The linearly interpolated renderer maps a value range to a size range. Four numbers define it: lower and upper value, lower and upper size.
from qgis.core import (QgsProject, QgsPieDiagram, QgsDiagramSettings,
QgsLinearlyInterpolatedDiagramRenderer, QgsDiagramLayerSettings)
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtCore import QSizeF
constituencies = QgsProject.instance().mapLayersByName("constituencies")[0]
settings = QgsDiagramSettings()
settings.categoryAttributes = ['"votes_a"', '"votes_b"', '"votes_c"', '"votes_other"']
settings.categoryColors = [QColor(c) for c in ("#2563eb", "#b91c1c", "#15803d", "#59645f")]
settings.categoryLabels = ["A", "B", "C", "other"]
settings.scaleByArea = True
settings.penColor = QColor("#ffffff")
settings.enabled = True
renderer = QgsLinearlyInterpolatedDiagramRenderer()
renderer.setDiagram(QgsPieDiagram())
renderer.setClassificationAttributeExpression('"votes_total"')
renderer.setLowerValue(0)
renderer.setUpperValue(max(f["votes_total"] or 0 for f in constituencies.getFeatures()))
renderer.setLowerSize(QSizeF(0, 0))
renderer.setUpperSize(QSizeF(18, 18))
renderer.setDiagramSettings(settings)
constituencies.setDiagramRenderer(renderer)
constituencies.setDiagramLayerSettings(QgsDiagramLayerSettings())
constituencies.triggerRepaint()
Breakdown: The classification can be a field or any expression — a sum of parts, a value converted to thousands. Starting both the value and the size range at zero keeps sizes proportional: a constituency with half the votes gets half the area. With scaleByArea set, QGIS interpolates the diagram's area rather than its diameter, which is what readers perceive for circles. Sizes are in millimetres by default and stay constant as the map zooms.
Choose the ranges
The upper value and upper size together set the scale; the lower values decide whether small features remain visible.
A practical approach: set the upper size so the largest diagram fits comfortably between its neighbours at the intended scale — often 15–25 mm on a printed map — and the upper value to the data's maximum. If the smallest features become invisible dots, that is information: they really are much smaller. If small features must remain readable as pies, a modest lower size of 2–3 mm is a reasonable compromise, but note it in the legend, because sizes near the bottom are no longer proportional.
values = sorted(f["votes_total"] or 0 for f in constituencies.getFeatures())
ratio = values[-1] / max(values[0], 1)
print(f"min {values[0]:,}, median {values[len(values) // 2]:,}, max {values[-1]:,}, ratio {ratio:.0f}:1")
Breakdown: The ratio of largest to smallest value tells you whether proportional sizing will work. Up to about 50:1, area scaling gives readable results; beyond a few hundred to one, the smallest diagrams vanish next to the largest, and you need one of the strategies below.
Handle outliers
One huge value — a capital city among small towns — sets the upper value, shrinking every other diagram. Three strategies help, each with a cost.
import math
# 1. cap at a high percentile; the outlier is drawn at the maximum size
p95 = values[int(len(values) * 0.95)]
renderer.setUpperValue(p95)
# 2. scale by a transformed value (state this clearly in the legend)
renderer.setClassificationAttributeExpression('sqrt("votes_total")')
renderer.setUpperValue(math.sqrt(values[-1]))
# 3. keep proportional sizing but show outliers differently
# (e.g. a separate inset map at a smaller scale)
constituencies.triggerRepaint()
Breakdown: Capping at the 95th percentile keeps most diagrams well sized; anything above the cap is drawn at the maximum size, which understates the outliers — label them with their value. Scaling by the square root compresses the range, but readers assume proportionality, so a transformed scale must be stated clearly. Often the best answer is cartographic rather than numeric: an inset for the dense city, where diagrams can be drawn at a larger scale, with proportional sizing everywhere. Choose one strategy and be explicit about it.
Share one scale across maps
A series of maps — the same constituencies in three elections, the same districts in several months — must use the same scale, or readers will compare sizes that do not mean the same thing.
series = {year: QgsProject.instance().mapLayersByName(f"constituencies_{year}")[0]
for year in (2018, 2022, 2026)}
series_max = max(f["votes_total"] or 0 for lyr in series.values() for f in lyr.getFeatures())
for year, lyr in series.items():
r = renderer.clone()
r.setUpperValue(series_max)
lyr.setDiagramRenderer(r)
lyr.setDiagramLayerSettings(QgsDiagramLayerSettings())
lyr.triggerRepaint()
print("shared upper value:", series_max)
Breakdown: Cloning the renderer gives each layer its own copy with identical settings, and the upper value comes from the maximum across every layer in the series. Recording the shared value in each map's caption or legend lets a reader of any single map understand its scale. When the series is an atlas over one layer filtered by year, a single renderer with a fixed upper value achieves the same thing automatically.
Use stepped sizes from an expression
Continuous scaling shows every difference but is hard to read precisely. Size classes — three or four fixed sizes for value bands — are easier to read and to explain in a legend, at the cost of hiding differences within a class. The interpolated renderer produces them when its classification expression returns a class number instead of the raw value.
stepped = renderer.clone()
stepped.setClassificationAttributeExpression(
'CASE WHEN "votes_total" < 20000 THEN 1 '
'WHEN "votes_total" < 50000 THEN 2 '
'WHEN "votes_total" < 100000 THEN 3 ELSE 4 END')
stepped.setLowerValue(0)
stepped.setUpperValue(4)
stepped.setLowerSize(QSizeF(0, 0))
stepped.setUpperSize(QSizeF(16, 16))
constituencies.setDiagramRenderer(stepped)
constituencies.triggerRepaint()
Breakdown: The expression maps each feature to a class from 1 to 4, and the renderer maps 0–4 to 0–16 mm, so the classes get four distinct sizes. With scaleByArea still on, each step adds the same area, which keeps the classes visually even. Choose class breaks that mean something to the reader — round numbers, policy thresholds — rather than letting an algorithm pick them. The legend must then show the classes with their bounds, not a continuous scale, because sizes no longer correspond to exact values. For point layers where you only need sized symbols rather than charts, the symbol-level approach in proportional symbol maps is often simpler.
Add a size legend
Readers need a key to translate size back into numbers. The interpolated renderer supports a size legend that draws reference diagrams for chosen values.
from qgis.core import QgsDataDefinedSizeLegend
legend = QgsDataDefinedSizeLegend()
legend.setTitle("votes cast")
legend.setLegendType(QgsDataDefinedSizeLegend.LegendCollapsed)
renderer.setDataDefinedSizeLegend(legend)
renderer.setAttributeLegend(True)
constituencies.setDiagramRenderer(renderer)
constituencies.triggerRepaint()
Breakdown: A collapsed size legend draws nested circles for representative values, the familiar compact key for proportional symbols. The attribute legend adds the category colours. Both appear in the Layers panel and in a print layout legend. Adding diagram legends covers choosing the reference values and styling the result.
Check sizes at print scale
Diagram sizes are in millimetres on the page, so their relationship to the features underneath changes with map scale. A layout at 1:50,000 and one at 1:250,000 show the same 18 mm pies over very different areas. Before finalising sizes, export a test page at the real scale and look for two things: diagrams overlapping so much that collision handling drops many of them, and diagrams so small that slices or segments disappear. Adjust the upper size first, since it scales every diagram at once; only then revisit the lower size or outlier handling. Keeping the final values in a small dictionary at the top of the script — upper value, upper size, lower size — makes it easy to produce variants of the same map at different print scales without hunting through code.
QGIS version compatibility
The interpolated renderer and scaleByArea work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Size legends for diagrams (setDataDefinedSizeLegend) exist since 3.4. On QGIS 4, enum members are scoped, for example QgsDataDefinedSizeLegend.LegendType.LegendCollapsed.
Troubleshooting
- Large values look enormous. Area scaling is off; set
scaleByArea = True. - Small features have no diagram. Their size interpolates to nearly zero; accept it, or set a small lower size and say so.
- Maps in a series do not compare. Each was scaled to its own maximum; share the upper value.
- Diagrams exceed the maximum size. Values above the upper value are drawn at the upper size or beyond; recompute or cap deliberately.
Conclusion
Use the linearly interpolated renderer with zero-based ranges and area scaling, choose the upper size from the space available and the upper value from the data, handle outliers by capping, transforming or insetting — and say which — share one upper value across a series, and add a size legend so readers can read values back.
Frequently Asked Questions
Should bars scale by area too? No. Bars encode values by length; area scaling applies to pies and other two-dimensional diagrams.
What maximum size works for print? Typically 15–25 mm at the map's print scale, depending on feature density.
Can I size by one field and colour by others? Yes — that is the normal case: size by the total, slices or segments by the parts.
Is there a minimum readable pie? Around 4 mm; below that slices are hard to see. Hide or aggregate features that small.