Data-Defined Symbol Size and Colour in PyQGIS
Drive QGIS symbol size and colour from attribute values with QgsProperty and setDataDefinedProperty: which level the override attaches to, the per-feature…
TL;DR: attach a QgsProperty built from an expression to a specific property of a specific symbol layer with setDataDefinedProperty() — the expression is evaluated once per feature per render, so use it for genuinely continuous variation and precompute the value when the layer is rendered often. This page is part of the layer styling and symbology automation guide.
Complete Runnable Code
"""Drive marker size and fill colour from attribute values."""
from qgis.core import QgsProperty, QgsSymbolLayer, QgsVectorLayer
def size_by_field(layer: QgsVectorLayer, field: str,
lo_mm: float = 2.0, hi_mm: float = 12.0) -> None:
"""Scale marker size linearly with `field`, clamped between `lo_mm` and `hi_mm`."""
symbol_layer = layer.renderer().symbol().symbolLayer(0)
expression = (
'coalesce(scale_linear("{f}", minimum("{f}"), maximum("{f}"), {lo}, {hi}), {lo})'
).format(f=field, lo=lo_mm, hi=hi_mm)
symbol_layer.setDataDefinedProperty(
QgsSymbolLayer.PropertySize, QgsProperty.fromExpression(expression))
layer.triggerRepaint()
def colour_by_threshold(layer: QgsVectorLayer, field: str, threshold: float) -> None:
"""Colour features red above `threshold`, blue below it, grey when unknown."""
expression = (
'CASE WHEN "{f}" IS NULL THEN \'#9e9e9e\' '
"WHEN \"{f}\" > {t} THEN '#c62828' ELSE '#1565c0' END"
).format(f=field, t=threshold)
symbol_layer = layer.renderer().symbol().symbolLayer(0)
symbol_layer.setDataDefinedProperty(
QgsSymbolLayer.PropertyFillColor, QgsProperty.fromExpression(expression))
layer.triggerRepaint()
The coalesce in the size expression is not defensive clutter. scale_linear returns NULL when its input is NULL, and a NULL size renders as nothing at all — so without it, features with no value silently vanish from the map.
Architecture Breakdown
QgsProperty — an expression bound to one property
QgsProperty.fromExpression() wraps an expression string in an object the symbol layer can evaluate. There is also fromField(), which is a faster path when the value needs no arithmetic at all: it reads the column directly instead of running the expression engine.
Every property has an enum constant — PropertySize, PropertyFillColor, PropertyStrokeWidth, PropertyAngle and so on — and the constants belong to QgsSymbolLayer. Setting a property that the symbol layer type does not support is silently ignored, which is the second most common reason an override appears to do nothing.
Why it must go on the symbol layer
A symbol is a container; the properties that are painted belong to the symbol layers inside it. Code that reaches for symbol.setDataDefinedProperty() does not fail — that method exists for a small set of symbol-level properties — but the size and colour overrides you want are one level down, on symbol.symbolLayer(0).
For a symbol with several layers, choose deliberately: an override on layer 0 changes the fill and leaves the outline alone, which is usually right, but not always what a reader expects.
The cost model
Every data-defined property is an expression evaluated once per feature, on every render pass. On a canvas the user is panning around, that is several times a second.
The last bar is the important one. An expression containing an aggregate — maximum("value"), sum("area") — is re-evaluated per feature, and each evaluation scans the layer. The example above avoids this by computing the bounds once in Python and interpolating them into the expression as literals, which turns a quadratic cost into a linear one.
Choosing Between an Override and Classes
Before reaching for an override it is worth asking whether the property really varies continuously, because the cheaper answers cover more cases than they get credit for.
Overrides and renderer classes solve overlapping problems, and the choice is mostly about how many distinct outcomes there are and how often the layer is drawn. Half a dozen size steps read perfectly well as a graduated renderer, which costs one classification lookup per feature instead of an expression evaluation. Genuinely continuous variation — a proportional symbol map — is what overrides are for.
The third option deserves more use than it gets: precompute the derived value into a real column with the field calculation pattern, then bind the override to that column with QgsProperty.fromField(). The arithmetic happens once in a batch rather than on every repaint, and the map behaves like an unstyled one.
Testing an Override Before Shipping It
An override is easy to get subtly wrong in a way that only shows up on real data, so it is worth a deliberate check rather than a glance at the canvas. Evaluate the expression in the field calculator over the actual layer and look at the distribution of results: how many rows produced NULL, what the minimum and maximum came out as, and whether the range makes sense in millimetres or as a colour.
Then render the full extent once and time it. A property that costs three milliseconds per thousand features is invisible on a test file and unusable on a production one, and the only way to find out which you have is to draw all of it.
Production Best Practices
- Wrap size expressions in
coalesce. A NULL size draws nothing, with no warning. - Never put an aggregate in an override. It scans the layer once per feature.
- Interpolate bounds as literals rather than recomputing minimum and maximum per feature.
- Set the property on a symbol layer, not on the symbol.
- Prefer
fromField()tofromExpression()when no arithmetic is needed. - Check the render time after adding an override, on a full extent, before shipping it. The cost is invisible on a test file of a thousand features and obvious on a real one.
Frequently Asked Questions
Which properties can be overridden?
Most of the ones you would expect, and the list depends on the symbol layer type. A simple marker exposes size, angle, fill and stroke colour, stroke width and offset; a simple fill exposes fill and stroke colour, stroke width and style; a line symbol layer adds dash pattern and offset. The enum constants all live on QgsSymbolLayer, and the safest way to discover what a given symbol layer supports is to inspect it in the interface, where unsupported properties simply do not appear.
Setting a constant the symbol layer does not support is not an error — it is stored and ignored, which makes it one of the harder styling mistakes to spot.
Why did my override have no effect?
Three usual causes: it was set on the symbol rather than the symbol layer, the property constant does not apply to that symbol layer type, or the expression returns NULL for every feature. None of the three raises — the map simply looks unchanged.
Test the expression in the field calculator first. If it produces sensible values there, the problem is where the override was attached rather than what it computes.
Can an override read a field that is not in the renderer?
Yes. The expression is evaluated against a context with the feature scope, so any field on the layer is available regardless of what the renderer classifies on. Bear in mind that QGIS must therefore fetch that attribute for every feature it draws, which is a cost worth knowing about on wide tables.
How do I make symbol size proportional to a value rather than linear with it?
For proportional symbols the area, not the diameter, should be proportional to the value — so the size expression needs a square root. scale_exp with an exponent of 0.5 does this directly, and using a linear scale instead is the classic mistake that makes a map exaggerate large values.
Can I combine an override with a categorized renderer?
Yes, and it is a common combination: the renderer picks a colour per class, and an override on each class’s symbol scales the size by a value. Set the override on the symbol layer of each category’s symbol, not once on the renderer, because each category owns its own symbol.
That is also the moment to consider whether the classification is still earning its place. Two overrides on a single symbol will sometimes express the same map more simply than a dozen classes each carrying its own override.
Do overrides survive a QML export?
Yes. Data-defined properties are part of the symbol layer definition and are written into the QML along with everything else. They do not survive an SLD export, which has no vocabulary for them — another reason to precompute values into columns when the style must reach a map server.
Related
- Layer Styling and Symbology Automation — the parent guide covering the renderer chain
- Adding a Field and Populating It with an Expression — precomputing the value an override reads
- Saving and Loading Layer Styles as QML — persisting a style that carries overrides