[{"data":1,"prerenderedAt":1458},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis":3},{"id":4,"title":5,"body":6,"description":1447,"extension":1448,"meta":1449,"navigation":220,"path":1454,"seo":1455,"stem":1456,"__hash__":1457},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis\u002Findex.md","Data-Defined Symbol Size with an Expression in PyQGIS",{"type":7,"value":8,"toc":1434},"minimark",[9,13,17,36,145,150,176,180,187,308,334,338,353,398,417,420,424,430,497,513,516,686,705,709,712,801,874,897,901,911,1032,1158,1183,1187,1193,1258,1267,1271,1337,1341,1359,1363,1373,1384,1396,1402,1406,1430],[10,11,5],"h1",{"id":12},"data-defined-symbol-size-with-an-expression-in-pyqgis",[14,15,16],"p",{},"A proportional-symbol map — bigger circle, bigger value — is one of the few thematic techniques that reads instantly without a legend. In PyQGIS it is a data-defined property: instead of setting a fixed size on the symbol, you bind an expression that QGIS re-evaluates for every feature at render time. Nothing is written to the layer, so the same data can carry three different sizings in three different projects.",[14,18,19,20,25,26,30,31,35],{},"This page is a focused recipe within ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","Working with QGIS Expressions in PyQGIS",", and it pairs with ",[21,27,29],{"href":28},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002F","Programmatic Layer Styling"," on the cartography side. It covers binding the property, choosing between linear and area-true scaling, handling ",[32,33,34],"code",{},"NULL"," and outliers, and producing a legend that matches what the map actually draws.",[14,37,38],{},[39,40,45,49,53,60,69,79,85,91,99,104,108,111,115,118,122,126,130,134,137,139,141,143],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 268","img","Linear radius scaling compared with area-true scaling: the same three population values produce very different perceived magnitudes depending on which quantity is scaled","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"Scaling the radius versus scaling the area",[50,51,52],"desc",{},"Three towns with populations of four thousand, twenty-eight thousand and ninety-six thousand are drawn twice. In the upper row scale_linear maps population onto the radius, so the largest circle looks far more than twenty-four times the smallest because its area grows with the square. In the lower row scale_exp with an exponent of one half maps population onto the area, so the visual magnitudes match the data.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","268","#f6f3ea",[61,62,68],"text",{"x":63,"y":64,"style":65,"fill":66,"textAnchor":67},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The eye reads area, not radius",[54,70],{"x":71,"y":72,"width":73,"height":74,"rx":75,"fill":76,"stroke":77,"style":78},"16","42","728","96","10","#fffdf7","#b45309","stroke-width:2.5",[61,80,84],{"x":81,"y":82,"style":83,"fill":77},"40","66","font-size:12px;font-weight:bold;font-family:sans-serif","scale_linear → radius",[61,86,90],{"x":81,"y":87,"style":88,"fill":89},"84","font-size:11px;font-family:sans-serif","#2f3b35","exaggerates large values",[92,93],"circle",{"cx":94,"cy":95,"r":96,"fill":77,"fillOpacity":97,"stroke":77,"style":98},"300","94","7",0.4,"stroke-width:2",[61,100,103],{"x":94,"y":101,"style":102,"fill":89,"textAnchor":67},"126","text-anchor:middle;font-size:11px;font-family:sans-serif","4 200",[92,105],{"cx":106,"cy":95,"r":107,"fill":77,"fillOpacity":97,"stroke":77,"style":98},"420","19",[61,109,110],{"x":106,"y":101,"style":102,"fill":89,"textAnchor":67},"28 500",[92,112],{"cx":113,"cy":95,"r":114,"fill":77,"fillOpacity":97,"stroke":77,"style":98},"580","34",[61,116,117],{"x":113,"y":101,"style":102,"fill":89,"textAnchor":67},"96 000",[54,119],{"x":71,"y":120,"width":73,"height":74,"rx":75,"fill":76,"stroke":121,"style":78},"152","#15803d",[61,123,125],{"x":81,"y":124,"style":83,"fill":121},"176","scale_exp(…, 0.5) → area",[61,127,129],{"x":81,"y":128,"style":88,"fill":89},"194","visual size matches the data",[92,131],{"cx":94,"cy":132,"r":133,"fill":121,"fillOpacity":97,"stroke":121,"style":98},"204","8",[61,135,103],{"x":94,"y":136,"style":102,"fill":89,"textAnchor":67},"236",[92,138],{"cx":106,"cy":132,"r":71,"fill":121,"fillOpacity":97,"stroke":121,"style":98},[61,140,110],{"x":106,"y":136,"style":102,"fill":89,"textAnchor":67},[92,142],{"cx":113,"cy":132,"r":64,"fill":121,"fillOpacity":97,"stroke":121,"style":98},[61,144,117],{"x":113,"y":136,"style":102,"fill":89,"textAnchor":67},[146,147,149],"h2",{"id":148},"prerequisites","Prerequisites",[151,152,153,161,168],"ul",{},[154,155,156,160],"li",{},[157,158,159],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[154,162,163,164,167],{},"A ",[157,165,166],{},"point layer"," with a numeric attribute to size by. The technique works on line width and polygon outline too, but proportional circles are the canonical case.",[154,169,170,171,175],{},"A single-symbol renderer on the layer. If it is currently categorized or graduated, the symbol lookup below changes — see ",[21,172,174],{"href":173},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated and Categorized Renderers",".",[146,177,179],{"id":178},"bind-the-expression-to-symbol-size","Bind the expression to symbol size",[14,181,182,183,186],{},"The property lives on a ",[157,184,185],{},"symbol layer",", not the symbol: a symbol can stack several layers, and each has its own overridable properties.",[188,189,194],"pre",{"className":190,"code":191,"language":192,"meta":193,"style":193},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsProperty, QgsSymbolLayer\n\nlayer = QgsProject.instance().mapLayersByName(\"towns\")[0]\nsymbol_layer = layer.renderer().symbol().symbolLayer(0)\n\nsymbol_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertySize,\n    QgsProperty.fromExpression('scale_linear(\"population\", 0, 100000, 2, 12)'),\n)\n\nlayer.triggerRepaint()\n","python","",[32,195,196,215,222,247,263,268,274,280,292,297,302],{"__ignoreMap":193},[197,198,201,205,209,212],"span",{"class":199,"line":200},"line",1,[197,202,204],{"class":203},"snl16","from",[197,206,208],{"class":207},"s95oV"," qgis.core ",[197,210,211],{"class":203},"import",[197,213,214],{"class":207}," QgsProject, QgsProperty, QgsSymbolLayer\n",[197,216,218],{"class":199,"line":217},2,[197,219,221],{"emptyLinePlaceholder":220},true,"\n",[197,223,225,228,231,234,238,241,244],{"class":199,"line":224},3,[197,226,227],{"class":207},"layer ",[197,229,230],{"class":203},"=",[197,232,233],{"class":207}," QgsProject.instance().mapLayersByName(",[197,235,237],{"class":236},"sU2Wk","\"towns\"",[197,239,240],{"class":207},")[",[197,242,56],{"class":243},"sDLfK",[197,245,246],{"class":207},"]\n",[197,248,250,253,255,258,260],{"class":199,"line":249},4,[197,251,252],{"class":207},"symbol_layer ",[197,254,230],{"class":203},[197,256,257],{"class":207}," layer.renderer().symbol().symbolLayer(",[197,259,56],{"class":243},[197,261,262],{"class":207},")\n",[197,264,266],{"class":199,"line":265},5,[197,267,221],{"emptyLinePlaceholder":220},[197,269,271],{"class":199,"line":270},6,[197,272,273],{"class":207},"symbol_layer.setDataDefinedProperty(\n",[197,275,277],{"class":199,"line":276},7,[197,278,279],{"class":207},"    QgsSymbolLayer.PropertySize,\n",[197,281,283,286,289],{"class":199,"line":282},8,[197,284,285],{"class":207},"    QgsProperty.fromExpression(",[197,287,288],{"class":236},"'scale_linear(\"population\", 0, 100000, 2, 12)'",[197,290,291],{"class":207},"),\n",[197,293,295],{"class":199,"line":294},9,[197,296,262],{"class":207},[197,298,300],{"class":199,"line":299},10,[197,301,221],{"emptyLinePlaceholder":220},[197,303,305],{"class":199,"line":304},11,[197,306,307],{"class":207},"layer.triggerRepaint()\n",[14,309,310,313,314,317,318,321,322,325,326,329,330,333],{},[157,311,312],{},"Breakdown:"," ",[32,315,316],{},"renderer().symbol()"," works for a single-symbol renderer; a categorized renderer has one symbol per category and needs ",[32,319,320],{},"renderer().symbols(QgsRenderContext())"," instead. ",[32,323,324],{},"symbolLayer(0)"," is the bottom-most layer of the symbol, which for a plain marker is the only one. ",[32,327,328],{},"scale_linear(value, in_min, in_max, out_min, out_max)"," clamps outside the input range, so a population of 200 000 still renders at 12 mm rather than running off the map. ",[32,331,332],{},"triggerRepaint()"," is what makes the canvas redraw — without it, the change is applied but invisible until something else forces a refresh.",[146,335,337],{"id":336},"scale-the-area-not-the-radius","Scale the area, not the radius",[14,339,340,343,344,348,349,352],{},[32,341,342],{},"scale_linear"," on size maps the value onto the marker's ",[345,346,347],"em",{},"diameter",". Because a circle's area grows with the square of its diameter, a value that is four times larger looks sixteen times bigger. For an honest proportional-symbol map, scale so that ",[157,350,351],{},"area"," is proportional — which means taking a square root.",[188,354,356],{"className":190,"code":355,"language":192,"meta":193,"style":193},"from qgis.core import QgsProperty, QgsSymbolLayer\n\nsymbol_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertySize,\n    QgsProperty.fromExpression('scale_exp(\"population\", 0, 100000, 2, 12, 0.5)'),\n)\nlayer.triggerRepaint()\n",[32,357,358,369,373,377,381,390,394],{"__ignoreMap":193},[197,359,360,362,364,366],{"class":199,"line":200},[197,361,204],{"class":203},[197,363,208],{"class":207},[197,365,211],{"class":203},[197,367,368],{"class":207}," QgsProperty, QgsSymbolLayer\n",[197,370,371],{"class":199,"line":217},[197,372,221],{"emptyLinePlaceholder":220},[197,374,375],{"class":199,"line":224},[197,376,273],{"class":207},[197,378,379],{"class":199,"line":249},[197,380,279],{"class":207},[197,382,383,385,388],{"class":199,"line":265},[197,384,285],{"class":207},[197,386,387],{"class":236},"'scale_exp(\"population\", 0, 100000, 2, 12, 0.5)'",[197,389,291],{"class":207},[197,391,392],{"class":199,"line":270},[197,393,262],{"class":207},[197,395,396],{"class":199,"line":276},[197,397,307],{"class":207},[14,399,400,313,402,405,406,409,410,413,414,416],{},[157,401,312],{},[32,403,404],{},"scale_exp()"," takes the same five arguments as ",[32,407,408],{},"scale_linear()"," plus an exponent. An exponent of ",[32,411,412],{},"0.5"," is a square root, which makes the rendered ",[345,415,351],{}," proportional to the value — the standard cartographic convention for proportional symbols. Exponents above 1 exaggerate large values further, which is occasionally useful for emphasis but should be declared in the caption if you do it.",[14,418,419],{},"The choice matters more than it sounds. With linear radius scaling, a reader comparing the largest and smallest circles on the map above would judge the ratio at roughly 24:1 by area when the underlying data says 23:1 by value — coincidentally close. Push the range wider and the distortion becomes severe.",[146,421,423],{"id":422},"handle-null-and-outliers","Handle NULL and outliers",[14,425,426,427,429],{},"Real data has empty cells and a handful of values far outside the useful range. Both need an explicit decision, because the defaults are unhelpful: a ",[32,428,34],{}," produces a zero-size marker that is invisible, and a single extreme value flattens everything else.",[188,431,433],{"className":190,"code":432,"language":192,"meta":193,"style":193},"expression = \"\"\"\nscale_exp(\n  coalesce(\"population\", 0),\n  0,\n  coalesce(maximum(\"population\"), 1),\n  2, 14, 0.5\n)\n\"\"\"\n\nsymbol_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertySize, QgsProperty.fromExpression(expression)\n)\n",[32,434,435,445,450,455,460,465,470,474,479,483,487,492],{"__ignoreMap":193},[197,436,437,440,442],{"class":199,"line":200},[197,438,439],{"class":207},"expression ",[197,441,230],{"class":203},[197,443,444],{"class":236}," \"\"\"\n",[197,446,447],{"class":199,"line":217},[197,448,449],{"class":236},"scale_exp(\n",[197,451,452],{"class":199,"line":224},[197,453,454],{"class":236},"  coalesce(\"population\", 0),\n",[197,456,457],{"class":199,"line":249},[197,458,459],{"class":236},"  0,\n",[197,461,462],{"class":199,"line":265},[197,463,464],{"class":236},"  coalesce(maximum(\"population\"), 1),\n",[197,466,467],{"class":199,"line":270},[197,468,469],{"class":236},"  2, 14, 0.5\n",[197,471,472],{"class":199,"line":276},[197,473,262],{"class":236},[197,475,476],{"class":199,"line":282},[197,477,478],{"class":236},"\"\"\"\n",[197,480,481],{"class":199,"line":294},[197,482,221],{"emptyLinePlaceholder":220},[197,484,485],{"class":199,"line":299},[197,486,273],{"class":207},[197,488,489],{"class":199,"line":304},[197,490,491],{"class":207},"    QgsSymbolLayer.PropertySize, QgsProperty.fromExpression(expression)\n",[197,493,495],{"class":199,"line":494},12,[197,496,262],{"class":207},[14,498,499,313,501,504,505,508,509,512],{},[157,500,312],{},[32,502,503],{},"coalesce(\"population\", 0)"," turns an empty cell into a zero, which renders at the minimum size rather than vanishing — a visible small dot is far better than a silently missing town. ",[32,506,507],{},"maximum(\"population\")"," is an aggregate function evaluated across the layer, so the upper bound adapts to the data instead of being hard-coded; wrapping it in ",[32,510,511],{},"coalesce(..., 1)"," prevents a divide-by-zero on an empty layer. The triple-quoted Python string keeps the expression readable, and the engine ignores the newlines.",[14,514,515],{},"For genuine outliers, clamping at a percentile reads better than clamping at the maximum. There is no percentile function in the expression engine, so compute the bound in Python and format it into the expression:",[188,517,519],{"className":190,"code":518,"language":192,"meta":193,"style":193},"from qgis.core import QgsExpression\nimport statistics\n\nvalues = sorted(f[\"population\"] for f in layer.getFeatures() if f[\"population\"] is not None)\ncap = values[int(len(values) * 0.95)] if values else 1\n\nexpression = (\n    f'scale_exp(coalesce(\"population\", 0), 0, {cap}, 2, 14, 0.5)'\n)\nsymbol_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertySize, QgsProperty.fromExpression(expression)\n)\n",[32,520,521,532,539,543,595,637,641,650,670,674,678,682],{"__ignoreMap":193},[197,522,523,525,527,529],{"class":199,"line":200},[197,524,204],{"class":203},[197,526,208],{"class":207},[197,528,211],{"class":203},[197,530,531],{"class":207}," QgsExpression\n",[197,533,534,536],{"class":199,"line":217},[197,535,211],{"class":203},[197,537,538],{"class":207}," statistics\n",[197,540,541],{"class":199,"line":224},[197,542,221],{"emptyLinePlaceholder":220},[197,544,545,548,550,553,556,559,562,565,568,571,574,577,580,582,584,587,590,593],{"class":199,"line":249},[197,546,547],{"class":207},"values ",[197,549,230],{"class":203},[197,551,552],{"class":243}," sorted",[197,554,555],{"class":207},"(f[",[197,557,558],{"class":236},"\"population\"",[197,560,561],{"class":207},"] ",[197,563,564],{"class":203},"for",[197,566,567],{"class":207}," f ",[197,569,570],{"class":203},"in",[197,572,573],{"class":207}," layer.getFeatures() ",[197,575,576],{"class":203},"if",[197,578,579],{"class":207}," f[",[197,581,558],{"class":236},[197,583,561],{"class":207},[197,585,586],{"class":203},"is",[197,588,589],{"class":203}," not",[197,591,592],{"class":243}," None",[197,594,262],{"class":207},[197,596,597,600,602,605,608,611,614,617,620,623,626,628,631,634],{"class":199,"line":265},[197,598,599],{"class":207},"cap ",[197,601,230],{"class":203},[197,603,604],{"class":207}," values[",[197,606,607],{"class":243},"int",[197,609,610],{"class":207},"(",[197,612,613],{"class":243},"len",[197,615,616],{"class":207},"(values) ",[197,618,619],{"class":203},"*",[197,621,622],{"class":243}," 0.95",[197,624,625],{"class":207},")] ",[197,627,576],{"class":203},[197,629,630],{"class":207}," values ",[197,632,633],{"class":203},"else",[197,635,636],{"class":243}," 1\n",[197,638,639],{"class":199,"line":270},[197,640,221],{"emptyLinePlaceholder":220},[197,642,643,645,647],{"class":199,"line":276},[197,644,439],{"class":207},[197,646,230],{"class":203},[197,648,649],{"class":207}," (\n",[197,651,652,655,658,661,664,667],{"class":199,"line":282},[197,653,654],{"class":203},"    f",[197,656,657],{"class":236},"'scale_exp(coalesce(\"population\", 0), 0, ",[197,659,660],{"class":243},"{",[197,662,663],{"class":207},"cap",[197,665,666],{"class":243},"}",[197,668,669],{"class":236},", 2, 14, 0.5)'\n",[197,671,672],{"class":199,"line":294},[197,673,262],{"class":207},[197,675,676],{"class":199,"line":299},[197,677,273],{"class":207},[197,679,680],{"class":199,"line":304},[197,681,491],{"class":207},[197,683,684],{"class":199,"line":494},[197,685,262],{"class":207},[14,687,688,690,691,693,694,697,698,701,702,175],{},[157,689,312],{}," Reading the values once in Python and taking the 95th percentile gives a bound that ignores the handful of extreme outliers. Because ",[32,692,404],{}," clamps, the five per cent above the cap all render at the maximum size — which is exactly the intent, and should be mentioned in the legend. Formatting a computed ",[345,695,696],{},"number"," into an expression is safe; formatting a computed ",[345,699,700],{},"string"," is not, which is why the earlier example used ",[32,703,704],{},"QgsExpression.quotedValue()",[146,706,708],{"id":707},"make-the-legend-match","Make the legend match",[14,710,711],{},"A data-defined size renders correctly but produces a legend showing a single arbitrary symbol, because QGIS has no way to know which sizes are meaningful. A data-defined size legend fixes that.",[14,713,714],{},[39,715,718,721,724,727,730,736,742,747,752,756,759,763,768,771,774,780,785,788,792,794,797],{"viewBox":716,"role":42,"ariaLabel":717,"xmlns":44},"0 0 760 232","A default legend showing one arbitrary symbol beside a data-defined size legend showing three nested circles annotated with their values",[46,719,720],{},"Default legend versus a data-defined size legend",[50,722,723],{},"On the left, the default legend entry for a data-defined symbol shows a single medium circle labelled only with the layer name, giving the reader no way to interpret sizes. On the right, a data-defined size legend shows three nested circles of increasing size, each annotated with the population value it represents.",[54,725],{"x":56,"y":56,"width":57,"height":726,"fill":59},"232",[61,728,729],{"x":63,"y":64,"style":65,"fill":66,"textAnchor":67},"Without a size legend the reader cannot decode the map",[54,731],{"x":81,"y":732,"width":733,"height":734,"rx":75,"fill":76,"stroke":735,"style":78},"46","316","164","#b91c1c",[61,737,741],{"x":738,"y":739,"style":740,"fill":735,"textAnchor":67},"198","72","text-anchor:middle;font-size:12.5px;font-weight:bold;font-family:sans-serif","default legend entry",[92,743],{"cx":744,"cy":745,"r":71,"fill":746,"fillOpacity":97,"stroke":746,"style":98},"120","132","#0f766e",[61,748,751],{"x":120,"y":749,"style":750,"fill":89},"137","font-size:12px;font-family:sans-serif","towns",[61,753,755],{"x":738,"y":754,"style":102,"fill":89,"textAnchor":67},"186","one size, no scale reference",[54,757],{"x":758,"y":732,"width":733,"height":734,"rx":75,"fill":76,"stroke":121,"style":78},"404",[61,760,762],{"x":761,"y":739,"style":740,"fill":121,"textAnchor":67},"562","data-defined size legend",[92,764],{"cx":765,"cy":766,"r":64,"fill":767,"stroke":746,"style":98},"484","150","none",[92,769],{"cx":765,"cy":770,"r":71,"fill":767,"stroke":746,"style":98},"160",[92,772],{"cx":765,"cy":773,"r":133,"fill":767,"stroke":746,"style":98},"168",[199,775],{"x1":765,"y1":776,"x2":777,"y2":776,"stroke":778,"style":779},"124","560","#59645f","stroke-width:1;stroke-dasharray:3 3",[61,781,784],{"x":782,"y":783,"style":88,"fill":89},"566","128","100 000",[199,786],{"x1":765,"y1":787,"x2":777,"y2":787,"stroke":778,"style":779},"144",[61,789,791],{"x":782,"y":790,"style":88,"fill":89},"148","40 000",[199,793],{"x1":765,"y1":770,"x2":777,"y2":770,"stroke":778,"style":779},[61,795,796],{"x":782,"y":734,"style":88,"fill":89},"10 000",[61,798,800],{"x":761,"y":799,"style":102,"fill":121,"textAnchor":67},"196","nested circles the reader can measure against",[188,802,804],{"className":190,"code":803,"language":192,"meta":193,"style":193},"from qgis.core import QgsDataDefinedSizeLegend, QgsProperty\n\nsize_legend = QgsDataDefinedSizeLegend()\nsize_legend.setLegendType(QgsDataDefinedSizeLegend.LegendCollapsed)\nsize_legend.setTitle(\"Population\")\n\nrenderer = layer.renderer()\nrenderer.setDataDefinedSizeLegend(size_legend)\nlayer.triggerRepaint()\nlayer.emitStyleChanged()\n",[32,805,806,817,821,831,836,846,850,860,865,869],{"__ignoreMap":193},[197,807,808,810,812,814],{"class":199,"line":200},[197,809,204],{"class":203},[197,811,208],{"class":207},[197,813,211],{"class":203},[197,815,816],{"class":207}," QgsDataDefinedSizeLegend, QgsProperty\n",[197,818,819],{"class":199,"line":217},[197,820,221],{"emptyLinePlaceholder":220},[197,822,823,826,828],{"class":199,"line":224},[197,824,825],{"class":207},"size_legend ",[197,827,230],{"class":203},[197,829,830],{"class":207}," QgsDataDefinedSizeLegend()\n",[197,832,833],{"class":199,"line":249},[197,834,835],{"class":207},"size_legend.setLegendType(QgsDataDefinedSizeLegend.LegendCollapsed)\n",[197,837,838,841,844],{"class":199,"line":265},[197,839,840],{"class":207},"size_legend.setTitle(",[197,842,843],{"class":236},"\"Population\"",[197,845,262],{"class":207},[197,847,848],{"class":199,"line":270},[197,849,221],{"emptyLinePlaceholder":220},[197,851,852,855,857],{"class":199,"line":276},[197,853,854],{"class":207},"renderer ",[197,856,230],{"class":203},[197,858,859],{"class":207}," layer.renderer()\n",[197,861,862],{"class":199,"line":282},[197,863,864],{"class":207},"renderer.setDataDefinedSizeLegend(size_legend)\n",[197,866,867],{"class":199,"line":294},[197,868,307],{"class":207},[197,870,871],{"class":199,"line":299},[197,872,873],{"class":207},"layer.emitStyleChanged()\n",[14,875,876,313,878,881,882,885,886,889,890,893,894,896],{},[157,877,312],{},[32,879,880],{},"LegendCollapsed"," produces the nested-circle form shown above; ",[32,883,884],{},"LegendSeparated"," lists each class on its own row, which suits a narrow legend column. ",[32,887,888],{},"setTitle()"," labels the group so the reader knows what the sizes mean. ",[32,891,892],{},"emitStyleChanged()"," is what tells the layer tree to rebuild the legend — ",[32,895,332],{}," alone redraws the map but leaves the old legend in place, which is a confusing half-update.",[146,898,900],{"id":899},"beyond-size-width-rotation-and-offset","Beyond size: width, rotation and offset",[14,902,903,906,907,910],{},[32,904,905],{},"PropertySize"," is one slot among many. The same ",[32,908,909],{},"QgsProperty"," mechanism drives every overridable symbol property, and a few of them solve problems that would otherwise need a separate layer.",[14,912,913],{},[39,914,917,920,923,926,929,958,981,1008],{"viewBox":915,"role":42,"ariaLabel":916,"xmlns":44},"0 0 760 254","Four overridable symbol properties illustrated: line width driven by traffic volume, marker rotation driven by a bearing field, fill colour driven by a colour ramp, and offset driven by a side field",[46,918,919],{},"Four properties an expression can drive",[50,921,922],{},"Four panels each show a different data-defined property. Line width renders three road segments at increasing thickness from a volume field. Rotation renders three arrow markers pointing in different directions from a bearing field. Fill colour renders three polygons in a ramp from a value field. Offset renders two parallel lines shifted to either side of a centreline from a side field.",[54,924],{"x":56,"y":56,"width":57,"height":925,"fill":59},"254",[61,927,928],{"x":63,"y":64,"style":65,"fill":66,"textAnchor":67},"Every overridable property takes the same QgsProperty",[930,931,932,937,943,948,951,955],"g",{},[54,933],{"x":934,"y":935,"width":124,"height":734,"rx":75,"fill":76,"stroke":936,"style":98},"12","44","#2563eb",[61,938,942],{"x":939,"y":940,"style":941,"fill":936,"textAnchor":67},"100","68","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","PropertyStrokeWidth",[199,944],{"x1":945,"y1":946,"x2":734,"y2":946,"stroke":936,"style":947},"36","102","stroke-width:2;stroke-linecap:round",[199,949],{"x1":945,"y1":745,"x2":734,"y2":745,"stroke":936,"style":950},"stroke-width:6;stroke-linecap:round",[199,952],{"x1":945,"y1":953,"x2":734,"y2":953,"stroke":936,"style":954},"166","stroke-width:12;stroke-linecap:round",[61,956,957],{"x":939,"y":799,"style":102,"fill":89,"textAnchor":67},"width ← \"volume\"",[930,959,960,963,967,972,975,978],{},[54,961],{"x":962,"y":935,"width":124,"height":734,"rx":75,"fill":76,"stroke":77,"style":98},"200",[61,964,966],{"x":965,"y":940,"style":941,"fill":77,"textAnchor":67},"288","PropertyAngle",[968,969],"path",{"d":970,"fill":767,"stroke":77,"style":971},"M240 150 L240 106 M232 116 L240 104 L248 116","stroke-width:2.5;stroke-linejoin:round",[968,973],{"d":974,"fill":767,"stroke":77,"style":971},"M288 150 L318 120 M306 116 L320 118 L318 132",[968,976],{"d":977,"fill":767,"stroke":77,"style":971},"M336 150 L336 106 M328 138 L336 152 L344 138",[61,979,980],{"x":965,"y":799,"style":102,"fill":89,"textAnchor":67},"angle ← \"bearing\"",[930,982,983,986,990,997,1001,1005],{},[54,984],{"x":985,"y":935,"width":124,"height":734,"rx":75,"fill":76,"stroke":121,"style":98},"388",[61,987,989],{"x":988,"y":940,"style":941,"fill":121,"textAnchor":67},"476","PropertyFillColor",[54,991],{"x":992,"y":993,"width":72,"height":994,"fill":121,"fillOpacity":995,"stroke":121,"style":996},"410","98","60",0.2,"stroke-width:1.5",[54,998],{"x":999,"y":993,"width":72,"height":994,"fill":121,"fillOpacity":1000,"stroke":121,"style":996},"456",0.5,[54,1002],{"x":1003,"y":993,"width":72,"height":994,"fill":121,"fillOpacity":1004,"stroke":121,"style":996},"502",0.85,[61,1006,1007],{"x":988,"y":799,"style":102,"fill":89,"textAnchor":67},"ramp_color(…, \"score\")",[930,1009,1010,1014,1018,1023,1027,1029],{},[54,1011],{"x":1012,"y":935,"width":1013,"height":734,"rx":75,"fill":76,"stroke":746,"style":98},"576","172",[61,1015,1017],{"x":1016,"y":940,"style":941,"fill":746,"textAnchor":67},"662","PropertyOffset",[199,1019],{"x1":1020,"y1":745,"x2":1021,"y2":745,"stroke":778,"style":1022},"600","724","stroke-width:1.5;stroke-dasharray:4 3",[199,1024],{"x1":1020,"y1":1025,"x2":1021,"y2":1025,"stroke":746,"style":1026},"116","stroke-width:4;stroke-linecap:round",[199,1028],{"x1":1020,"y1":766,"x2":1021,"y2":766,"stroke":746,"style":1026},[61,1030,1031],{"x":1016,"y":799,"style":102,"fill":89,"textAnchor":67},"offset ← \"side\"",[188,1033,1035],{"className":190,"code":1034,"language":192,"meta":193,"style":193},"from qgis.core import QgsProperty, QgsSymbolLayer\n\nroads = QgsProject.instance().mapLayersByName(\"roads\")[0]\nline_layer = roads.renderer().symbol().symbolLayer(0)\n\nline_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertyStrokeWidth,\n    QgsProperty.fromExpression('scale_linear(\"daily_volume\", 0, 40000, 0.3, 4)'),\n)\nline_layer.setDataDefinedProperty(\n    QgsSymbolLayer.PropertyStrokeColor,\n    QgsProperty.fromExpression(\n        \"ramp_color('Reds', scale_linear(\\\"congestion\\\", 0, 1, 0, 1))\"\n    ),\n)\nroads.triggerRepaint()\n",[32,1036,1037,1047,1051,1069,1083,1087,1092,1097,1106,1110,1114,1119,1124,1141,1147,1152],{"__ignoreMap":193},[197,1038,1039,1041,1043,1045],{"class":199,"line":200},[197,1040,204],{"class":203},[197,1042,208],{"class":207},[197,1044,211],{"class":203},[197,1046,368],{"class":207},[197,1048,1049],{"class":199,"line":217},[197,1050,221],{"emptyLinePlaceholder":220},[197,1052,1053,1056,1058,1060,1063,1065,1067],{"class":199,"line":224},[197,1054,1055],{"class":207},"roads ",[197,1057,230],{"class":203},[197,1059,233],{"class":207},[197,1061,1062],{"class":236},"\"roads\"",[197,1064,240],{"class":207},[197,1066,56],{"class":243},[197,1068,246],{"class":207},[197,1070,1071,1074,1076,1079,1081],{"class":199,"line":249},[197,1072,1073],{"class":207},"line_layer ",[197,1075,230],{"class":203},[197,1077,1078],{"class":207}," roads.renderer().symbol().symbolLayer(",[197,1080,56],{"class":243},[197,1082,262],{"class":207},[197,1084,1085],{"class":199,"line":265},[197,1086,221],{"emptyLinePlaceholder":220},[197,1088,1089],{"class":199,"line":270},[197,1090,1091],{"class":207},"line_layer.setDataDefinedProperty(\n",[197,1093,1094],{"class":199,"line":276},[197,1095,1096],{"class":207},"    QgsSymbolLayer.PropertyStrokeWidth,\n",[197,1098,1099,1101,1104],{"class":199,"line":282},[197,1100,285],{"class":207},[197,1102,1103],{"class":236},"'scale_linear(\"daily_volume\", 0, 40000, 0.3, 4)'",[197,1105,291],{"class":207},[197,1107,1108],{"class":199,"line":294},[197,1109,262],{"class":207},[197,1111,1112],{"class":199,"line":299},[197,1113,1091],{"class":207},[197,1115,1116],{"class":199,"line":304},[197,1117,1118],{"class":207},"    QgsSymbolLayer.PropertyStrokeColor,\n",[197,1120,1121],{"class":199,"line":494},[197,1122,1123],{"class":207},"    QgsProperty.fromExpression(\n",[197,1125,1127,1130,1133,1136,1138],{"class":199,"line":1126},13,[197,1128,1129],{"class":236},"        \"ramp_color('Reds', scale_linear(",[197,1131,1132],{"class":243},"\\\"",[197,1134,1135],{"class":236},"congestion",[197,1137,1132],{"class":243},[197,1139,1140],{"class":236},", 0, 1, 0, 1))\"\n",[197,1142,1144],{"class":199,"line":1143},14,[197,1145,1146],{"class":207},"    ),\n",[197,1148,1150],{"class":199,"line":1149},15,[197,1151,262],{"class":207},[197,1153,1155],{"class":199,"line":1154},16,[197,1156,1157],{"class":207},"roads.triggerRepaint()\n",[14,1159,1160,313,1162,1164,1165,1167,1168,1171,1172,1175,1176,1178,1179,1182],{},[157,1161,312],{},[32,1163,942],{}," is the line-width slot, and here ",[32,1166,342],{}," is the right choice rather than ",[32,1169,1170],{},"scale_exp"," — the eye reads a line's thickness as a single dimension, so no area correction is needed. ",[32,1173,1174],{},"ramp_color()"," takes a named ramp and a position between 0 and 1, which is why the inner ",[32,1177,342],{}," normalises the congestion value into that range. Two properties on one symbol layer is perfectly normal; they are evaluated independently per feature. Which slots a symbol layer exposes varies by type, and ",[32,1180,1181],{},"symbol_layer.dataDefinedProperties().propertyKeys()"," lists the ones currently set.",[146,1184,1186],{"id":1185},"qgis-version-compatibility","QGIS version compatibility",[14,1188,1189,1190,1192],{},"The examples target ",[157,1191,159],{}," (Python 3.12).",[1194,1195,1196,1212],"table",{},[1197,1198,1199],"thead",{},[1200,1201,1202,1206,1209],"tr",{},[1203,1204,1205],"th",{},"QGIS version",[1203,1207,1208],{},"Python",[1203,1210,1211],{},"Notes",[1213,1214,1215,1234,1245],"tbody",{},[1200,1216,1217,1221,1224],{},[1218,1219,1220],"td",{},"3.28 LTR",[1218,1222,1223],{},"3.9",[1218,1225,1226,1227,1230,1231,1233],{},"Identical API. ",[32,1228,1229],{},"QgsSymbolLayer.PropertySize"," and ",[32,1232,404],{}," both long-standing.",[1200,1235,1236,1239,1242],{},[1218,1237,1238],{},"3.34 LTR",[1218,1240,1241],{},"3.12",[1218,1243,1244],{},"Baseline for this page.",[1200,1246,1247,1250,1252],{},[1218,1248,1249],{},"3.40 \u002F 3.44",[1218,1251,1241],{},[1218,1253,1254,1257],{},[32,1255,1256],{},"QgsSymbolLayer.Property.Size"," is the newer enum spelling; the old form still resolves.",[14,1259,1260,1262,1263,1266],{},[32,1261,909],{},", ",[32,1264,1265],{},"QgsDataDefinedSizeLegend"," and the scaling functions are unchanged across the 3.x line.",[146,1268,1270],{"id":1269},"troubleshooting","Troubleshooting",[151,1272,1273,1282,1294,1304,1315,1325],{},[154,1274,1275,1278,1279,1281],{},[157,1276,1277],{},"All markers render at the same size."," The property was set on the symbol rather than a symbol layer, or the renderer is not single-symbol so ",[32,1280,316],{}," returned the wrong object.",[154,1283,1284,1287,1288,1290,1291,175],{},[157,1285,1286],{},"Some markers are invisible."," Their value is ",[32,1289,34],{},", which evaluates to zero size. Wrap the field in ",[32,1292,1293],{},"coalesce()",[154,1295,1296,1299,1300,1303],{},[157,1297,1298],{},"Every marker is the maximum size."," The input range is too narrow — ",[32,1301,1302],{},"scale_linear(\"population\", 0, 100, ...)"," clamps everything above 100 to the top. Check the actual data range first.",[154,1305,1306,313,1309,1311,1312,1314],{},[157,1307,1308],{},"The change does not appear.",[32,1310,332],{}," was not called, or the layer is cached. Add ",[32,1313,892],{}," too when the legend should update.",[154,1316,1317,1320,1321,1324],{},[157,1318,1319],{},"The map reads as more extreme than the data."," Radius is being scaled linearly. Switch to ",[32,1322,1323],{},"scale_exp(..., 0.5)"," so area carries the value.",[154,1326,1327,313,1330,1333,1334,1336],{},[157,1328,1329],{},"The legend still shows one symbol.",[32,1331,1332],{},"setDataDefinedSizeLegend()"," was applied but ",[32,1335,892],{}," was not, so the layer tree is showing a stale legend.",[146,1338,1340],{"id":1339},"conclusion","Conclusion",[14,1342,1343,1344,1346,1347,1349,1350,1352,1353,1355,1356,1358],{},"A data-defined size is two objects — a ",[32,1345,909],{}," holding the expression and the ",[32,1348,1229],{}," slot it binds to — plus one cartographic decision. Scale the area rather than the radius, guard ",[32,1351,34],{}," with ",[32,1354,1293],{},", cap the range at a percentile rather than the maximum, and attach a ",[32,1357,1265],{}," so the reader can decode what they are looking at.",[146,1360,1362],{"id":1361},"frequently-asked-questions","Frequently Asked Questions",[14,1364,1365,1368,1369,1372],{},[157,1366,1367],{},"Why do all my symbols come out the same size?","\nThe property was almost certainly applied to the symbol instead of one of its symbol layers, or the renderer is categorized rather than single-symbol. Fetch the symbol layer with ",[32,1370,1371],{},"renderer().symbol().symbolLayer(0)"," and set the property there.",[14,1374,1375,1378,1380,1381,1383],{},[157,1376,1377],{},"Should I use scale_linear or scale_exp?",[32,1379,1170],{}," with an exponent of 0.5 for proportional symbols, because it makes the rendered area proportional to the value, which is what a reader actually perceives. ",[32,1382,342],{}," is fine for line width, where the eye reads the single dimension directly.",[14,1385,1386,1389,1390,1392,1393,1395],{},[157,1387,1388],{},"How do I stop NULL values disappearing from the map?","\nWrap the field in ",[32,1391,503],{},". A ",[32,1394,34],{}," otherwise evaluates to a zero-size marker, which draws nothing at all and looks like missing data rather than a missing value.",[14,1397,1398,1401],{},[157,1399,1400],{},"Does a data-defined size change my data?","\nNo. The expression is evaluated at render time and nothing is written to the layer. Removing the property restores the fixed symbol size immediately.",[146,1403,1405],{"id":1404},"related","Related",[151,1407,1408,1413,1418,1424],{},[154,1409,1410,1412],{},[21,1411,24],{"href":23}," — the guide this recipe belongs to",[154,1414,1415],{},[21,1416,1417],{"href":28},"Programmatic Layer Styling in PyQGIS",[154,1419,1420],{},[21,1421,1423],{"href":1422},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","Set a Vector Layer's Symbol Colour in PyQGIS",[154,1425,1426],{},[21,1427,1429],{"href":1428},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","Evaluate a QGIS Expression in PyQGIS",[1431,1432,1433],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":193,"searchDepth":217,"depth":217,"links":1435},[1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446],{"id":148,"depth":217,"text":149},{"id":178,"depth":217,"text":179},{"id":336,"depth":217,"text":337},{"id":422,"depth":217,"text":423},{"id":707,"depth":217,"text":708},{"id":899,"depth":217,"text":900},{"id":1185,"depth":217,"text":1186},{"id":1269,"depth":217,"text":1270},{"id":1339,"depth":217,"text":1340},{"id":1361,"depth":217,"text":1362},{"id":1404,"depth":217,"text":1405},"Bind a QGIS expression to symbol size in PyQGIS with QgsProperty. Scale markers by an attribute, use scale_exp for area-true circles, and keep the legend honest.","md",{"slug":1450,"type":1451,"breadcrumb":1452,"datePublished":1453,"dateModified":1453},"data-defined-symbol-size-expression-pyqgis","article","Data-Defined Symbol Size","2026-08-01","\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis",{"title":5,"description":1447},"pyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis\u002Findex","qiRBvlLVbTP89LUQ1yEFZX7OjrcebsKn1kZLhBxO9-k",1785585990473]