[{"data":1,"prerenderedAt":1358},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-proportional-symbol-map-pyqgis":3},{"id":4,"title":5,"body":6,"description":1347,"extension":1348,"meta":1349,"navigation":218,"path":1354,"seo":1355,"stem":1356,"__hash__":1357},"docs\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-proportional-symbol-map-pyqgis\u002Findex.md","Create a Proportional Symbol Map in PyQGIS",{"type":7,"value":8,"toc":1333},"minimark",[9,13,17,26,151,156,183,187,190,302,312,316,319,569,591,598,602,605,674,695,764,768,771,870,890,893,897,900,1055,1071,1083,1087,1090,1093,1097,1103,1188,1192,1243,1247,1250,1254,1268,1279,1285,1295,1299,1329],[10,11,5],"h1",{"id":12},"create-a-proportional-symbol-map-in-pyqgis",[14,15,16],"p",{},"A proportional symbol map encodes a quantity in the size of a mark rather than the colour of an area, and it is the honest choice whenever the value is a count rather than a rate. Population, sales, casualties, tonnes shipped — all of these belong in a circle whose area is the number, not in a choropleth where a large sparse district shouts louder than a small dense one.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated & Categorized Renderers in PyQGIS",". It covers the square-root rule that makes the encoding truthful, applying it as a data-defined override, the graduated-size alternative when a legend must show discrete classes, and how to keep the largest symbols from swallowing the map.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,81,86,91,95,98,101,104,109,113,118,122,126,130,133,136,138,141,143,147],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 316","img","Two rows of circles sized from the same values, one scaled by radius which exaggerates large values and one scaled by the square root so that area is proportional to the value","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Why the radius must follow the square root",[40,41,42],"desc",{},"Scaling the radius directly by the value makes a circle for one hundred sixteen times the area of a circle for twenty five, so the reader sees a difference far larger than the data. Scaling the radius by the square root of the value makes the area proportional, so the visual difference matches the numeric one.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","316","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Readers compare area, so area must carry the number",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","52","712","112","10","#fdf2e2","#b45309","stroke-width:2.5",[51,70,74],{"x":71,"y":72,"style":73,"fill":67,"textAnchor":57},"122","82","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","radius = value",[51,76,80],{"x":71,"y":77,"style":78,"fill":79,"textAnchor":57},"102","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","wrong",[82,83],"circle",{"cx":84,"cy":64,"r":85,"fill":67},"260","7",[51,87,90],{"x":84,"y":88,"style":89,"fill":79,"textAnchor":57},"150","text-anchor:middle;font-size:10px;font-family:sans-serif","25",[82,92],{"cx":93,"cy":64,"r":94,"fill":67},"360","14",[51,96,97],{"x":93,"y":88,"style":89,"fill":79,"textAnchor":57},"100",[82,99],{"cx":100,"cy":64,"r":54,"fill":67},"500",[51,102,103],{"x":100,"y":88,"style":89,"fill":79,"textAnchor":57},"400",[51,105,108],{"x":106,"y":107,"style":78,"fill":67,"textAnchor":57},"640","106","16× the value looks",[51,110,112],{"x":106,"y":111,"style":78,"fill":67,"textAnchor":57},"126","like 256× the quantity",[44,114],{"x":61,"y":115,"width":63,"height":64,"rx":65,"fill":116,"stroke":117,"style":68},"180","#edf8e9","#15803d",[51,119,121],{"x":71,"y":120,"style":73,"fill":117,"textAnchor":57},"210","radius = √value",[51,123,125],{"x":71,"y":124,"style":78,"fill":79,"textAnchor":57},"230","right",[82,127],{"cx":84,"cy":128,"r":129,"fill":117},"238","9",[51,131,90],{"x":84,"y":132,"style":89,"fill":79,"textAnchor":57},"278",[82,134],{"cx":93,"cy":128,"r":135,"fill":117},"18",[51,137,97],{"x":93,"y":132,"style":89,"fill":79,"textAnchor":57},[82,139],{"cx":100,"cy":128,"r":140,"fill":117},"36",[51,142,103],{"x":100,"y":132,"style":89,"fill":79,"textAnchor":57},[51,144,146],{"x":106,"y":145,"style":78,"fill":117,"textAnchor":57},"232","16× the value covers",[51,148,150],{"x":106,"y":149,"style":78,"fill":117,"textAnchor":57},"252","16× the area",[152,153,155],"h2",{"id":154},"prerequisites","Prerequisites",[157,158,159,167,175],"ul",{},[160,161,162,166],"li",{},[163,164,165],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[160,168,169,170,174],{},"A point layer with a numeric count field. Polygons work too — apply the symbol to a ",[21,171,173],{"href":172},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fgeometry-generator-symbol-layer-pyqgis\u002F","centroid geometry generator"," so the circle sits on the polygon.",[160,176,177,178,182],{},"A count, not a rate. Population per square kilometre belongs in a ",[21,179,181],{"href":180},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","choropleth","; total population belongs here.",[152,184,186],{"id":185},"the-scaling-rule","The scaling rule",[14,188,189],{},"A circle's area grows with the square of its radius. If the radius is set directly to the value, a place with four times the count looks sixteen times as big, and every reader over-reads the large values. The fix is one square root.",[191,192,197],"pre",{"className":193,"code":194,"language":195,"meta":196,"style":196},"language-python shiki shiki-themes github-dark","import math\n\ndef radius_mm(value, max_value, max_radius_mm=8.0):\n    if value is None or value \u003C= 0:\n        return 0.0\n    return max_radius_mm * math.sqrt(value \u002F max_value)\n","python","",[198,199,200,213,220,243,272,281],"code",{"__ignoreMap":196},[201,202,205,209],"span",{"class":203,"line":204},"line",1,[201,206,208],{"class":207},"snl16","import",[201,210,212],{"class":211},"s95oV"," math\n",[201,214,216],{"class":203,"line":215},2,[201,217,219],{"emptyLinePlaceholder":218},true,"\n",[201,221,223,226,230,233,236,240],{"class":203,"line":222},3,[201,224,225],{"class":207},"def",[201,227,229],{"class":228},"svObZ"," radius_mm",[201,231,232],{"class":211},"(value, max_value, max_radius_mm",[201,234,235],{"class":207},"=",[201,237,239],{"class":238},"sDLfK","8.0",[201,241,242],{"class":211},"):\n",[201,244,246,249,252,255,258,261,263,266,269],{"class":203,"line":245},4,[201,247,248],{"class":207},"    if",[201,250,251],{"class":211}," value ",[201,253,254],{"class":207},"is",[201,256,257],{"class":238}," None",[201,259,260],{"class":207}," or",[201,262,251],{"class":211},[201,264,265],{"class":207},"\u003C=",[201,267,268],{"class":238}," 0",[201,270,271],{"class":211},":\n",[201,273,275,278],{"class":203,"line":274},5,[201,276,277],{"class":207},"        return",[201,279,280],{"class":238}," 0.0\n",[201,282,284,287,290,293,296,299],{"class":203,"line":283},6,[201,285,286],{"class":207},"    return",[201,288,289],{"class":211}," max_radius_mm ",[201,291,292],{"class":207},"*",[201,294,295],{"class":211}," math.sqrt(value ",[201,297,298],{"class":207},"\u002F",[201,300,301],{"class":211}," max_value)\n",[14,303,304,307,308,311],{},[163,305,306],{},"Breakdown:"," Anchoring on the largest value keeps the biggest symbol at a size you chose rather than one the data chose. Returning zero for a null or non-positive value is deliberate: a missing count is not a small count, and drawing nothing is more honest than drawing a dot that reads as \"nearly none\". ",[198,309,310],{},"max_radius_mm"," is the single knob that controls how busy the map is, and eight millimetres is a reasonable starting point for a national map at A3.",[152,313,315],{"id":314},"apply-it-as-a-data-defined-size","Apply it as a data-defined size",[14,317,318],{},"Rather than computing per feature in Python, hand the expression to the renderer so it evaluates during drawing and stays correct when the data changes.",[191,320,322],{"className":193,"code":321,"language":195,"meta":196,"style":196},"from qgis.core import (\n    QgsProject, QgsMarkerSymbol, QgsProperty, QgsSymbolLayer,\n    QgsSingleSymbolRenderer,\n)\n\nlayer = QgsProject.instance().mapLayersByName(\"towns\")[0]\nmaximum = layer.maximumValue(layer.fields().indexOf(\"population\"))\n\nsymbol = QgsMarkerSymbol.createSimple({\n    \"name\": \"circle\",\n    \"color\": \"#2563eb\",\n    \"outline_color\": \"#1e40af\",\n    \"outline_width\": \"0.3\",\n})\nsymbol.setOpacity(0.75)\n\nmarker = symbol.symbolLayer(0)\nmarker.setDataDefinedProperty(\n    QgsSymbolLayer.PropertySize,\n    QgsProperty.fromExpression(\n        f'8.0 * sqrt(coalesce(\"population\", 0) \u002F {maximum})'\n    ),\n)\n\nlayer.setRenderer(QgsSingleSymbolRenderer(symbol))\nlayer.triggerRepaint()\n",[198,323,324,337,342,347,352,356,378,395,400,411,426,439,452,465,471,482,487,502,508,514,520,541,547,552,557,563],{"__ignoreMap":196},[201,325,326,329,332,334],{"class":203,"line":204},[201,327,328],{"class":207},"from",[201,330,331],{"class":211}," qgis.core ",[201,333,208],{"class":207},[201,335,336],{"class":211}," (\n",[201,338,339],{"class":203,"line":215},[201,340,341],{"class":211},"    QgsProject, QgsMarkerSymbol, QgsProperty, QgsSymbolLayer,\n",[201,343,344],{"class":203,"line":222},[201,345,346],{"class":211},"    QgsSingleSymbolRenderer,\n",[201,348,349],{"class":203,"line":245},[201,350,351],{"class":211},")\n",[201,353,354],{"class":203,"line":274},[201,355,219],{"emptyLinePlaceholder":218},[201,357,358,361,363,366,370,373,375],{"class":203,"line":283},[201,359,360],{"class":211},"layer ",[201,362,235],{"class":207},[201,364,365],{"class":211}," QgsProject.instance().mapLayersByName(",[201,367,369],{"class":368},"sU2Wk","\"towns\"",[201,371,372],{"class":211},")[",[201,374,46],{"class":238},[201,376,377],{"class":211},"]\n",[201,379,381,384,386,389,392],{"class":203,"line":380},7,[201,382,383],{"class":211},"maximum ",[201,385,235],{"class":207},[201,387,388],{"class":211}," layer.maximumValue(layer.fields().indexOf(",[201,390,391],{"class":368},"\"population\"",[201,393,394],{"class":211},"))\n",[201,396,398],{"class":203,"line":397},8,[201,399,219],{"emptyLinePlaceholder":218},[201,401,403,406,408],{"class":203,"line":402},9,[201,404,405],{"class":211},"symbol ",[201,407,235],{"class":207},[201,409,410],{"class":211}," QgsMarkerSymbol.createSimple({\n",[201,412,414,417,420,423],{"class":203,"line":413},10,[201,415,416],{"class":368},"    \"name\"",[201,418,419],{"class":211},": ",[201,421,422],{"class":368},"\"circle\"",[201,424,425],{"class":211},",\n",[201,427,429,432,434,437],{"class":203,"line":428},11,[201,430,431],{"class":368},"    \"color\"",[201,433,419],{"class":211},[201,435,436],{"class":368},"\"#2563eb\"",[201,438,425],{"class":211},[201,440,442,445,447,450],{"class":203,"line":441},12,[201,443,444],{"class":368},"    \"outline_color\"",[201,446,419],{"class":211},[201,448,449],{"class":368},"\"#1e40af\"",[201,451,425],{"class":211},[201,453,455,458,460,463],{"class":203,"line":454},13,[201,456,457],{"class":368},"    \"outline_width\"",[201,459,419],{"class":211},[201,461,462],{"class":368},"\"0.3\"",[201,464,425],{"class":211},[201,466,468],{"class":203,"line":467},14,[201,469,470],{"class":211},"})\n",[201,472,474,477,480],{"class":203,"line":473},15,[201,475,476],{"class":211},"symbol.setOpacity(",[201,478,479],{"class":238},"0.75",[201,481,351],{"class":211},[201,483,485],{"class":203,"line":484},16,[201,486,219],{"emptyLinePlaceholder":218},[201,488,490,493,495,498,500],{"class":203,"line":489},17,[201,491,492],{"class":211},"marker ",[201,494,235],{"class":207},[201,496,497],{"class":211}," symbol.symbolLayer(",[201,499,46],{"class":238},[201,501,351],{"class":211},[201,503,505],{"class":203,"line":504},18,[201,506,507],{"class":211},"marker.setDataDefinedProperty(\n",[201,509,511],{"class":203,"line":510},19,[201,512,513],{"class":211},"    QgsSymbolLayer.PropertySize,\n",[201,515,517],{"class":203,"line":516},20,[201,518,519],{"class":211},"    QgsProperty.fromExpression(\n",[201,521,523,526,529,532,535,538],{"class":203,"line":522},21,[201,524,525],{"class":207},"        f",[201,527,528],{"class":368},"'8.0 * sqrt(coalesce(\"population\", 0) \u002F ",[201,530,531],{"class":238},"{",[201,533,534],{"class":211},"maximum",[201,536,537],{"class":238},"}",[201,539,540],{"class":368},")'\n",[201,542,544],{"class":203,"line":543},22,[201,545,546],{"class":211},"    ),\n",[201,548,550],{"class":203,"line":549},23,[201,551,351],{"class":211},[201,553,555],{"class":203,"line":554},24,[201,556,219],{"emptyLinePlaceholder":218},[201,558,560],{"class":203,"line":559},25,[201,561,562],{"class":211},"layer.setRenderer(QgsSingleSymbolRenderer(symbol))\n",[201,564,566],{"class":203,"line":565},26,[201,567,568],{"class":211},"layer.triggerRepaint()\n",[14,570,571,573,574,577,578,582,583,586,587,590],{},[163,572,306],{}," ",[198,575,576],{},"layer.maximumValue()"," reads the maximum straight from the provider, which is far cheaper than iterating features and is exact for a file-based layer. Interpolating it into the expression pins the scale, so panning does not rescale the symbols — the same discipline that a fixed maximum brings to ",[21,579,581],{"href":580},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fcreate-heatmap-renderer-pyqgis\u002F","a heatmap",". ",[198,584,585],{},"coalesce()"," protects against nulls, which would otherwise fall back silently to the symbol's static size and produce a medium circle where there is no data. ",[198,588,589],{},"setOpacity(0.75)"," on the symbol lets overlapping circles show through each other, which matters because they will overlap.",[14,592,593,594,597],{},"Note that ",[198,595,596],{},"PropertySize"," on a marker sets the diameter, not the radius, so the expression above produces a maximum diameter of 8 mm. Doubling the constant if you were thinking in radii is the most common off-by-two in this recipe.",[152,599,601],{"id":600},"draw-the-big-ones-underneath","Draw the big ones underneath",[14,603,604],{},"Overlapping circles hide each other, and by default QGIS draws features in provider order — which is usually the order they were written, i.e. arbitrary. Sorting so the largest are painted first puts the small ones on top where they remain visible.",[191,606,608],{"className":193,"code":607,"language":195,"meta":196,"style":196},"renderer = layer.renderer()\nrenderer.setOrderBy(\n    QgsFeatureRequest.OrderBy([\n        QgsFeatureRequest.OrderByClause(\"population\", ascending=False)\n    ])\n)\nrenderer.setOrderByEnabled(True)\nlayer.triggerRepaint()\n",[198,609,610,620,625,630,651,656,660,670],{"__ignoreMap":196},[201,611,612,615,617],{"class":203,"line":204},[201,613,614],{"class":211},"renderer ",[201,616,235],{"class":207},[201,618,619],{"class":211}," layer.renderer()\n",[201,621,622],{"class":203,"line":215},[201,623,624],{"class":211},"renderer.setOrderBy(\n",[201,626,627],{"class":203,"line":222},[201,628,629],{"class":211},"    QgsFeatureRequest.OrderBy([\n",[201,631,632,635,637,640,644,646,649],{"class":203,"line":245},[201,633,634],{"class":211},"        QgsFeatureRequest.OrderByClause(",[201,636,391],{"class":368},[201,638,639],{"class":211},", ",[201,641,643],{"class":642},"s9osk","ascending",[201,645,235],{"class":207},[201,647,648],{"class":238},"False",[201,650,351],{"class":211},[201,652,653],{"class":203,"line":274},[201,654,655],{"class":211},"    ])\n",[201,657,658],{"class":203,"line":283},[201,659,351],{"class":211},[201,661,662,665,668],{"class":203,"line":380},[201,663,664],{"class":211},"renderer.setOrderByEnabled(",[201,666,667],{"class":238},"True",[201,669,351],{"class":211},[201,671,672],{"class":203,"line":397},[201,673,568],{"class":211},[14,675,676,573,678,681,682,685,686,689,690,694],{},[163,677,306],{},[198,679,680],{},"setOrderBy()"," attaches a sort to the feature request the renderer issues, so it costs one ordered read rather than a Python loop. ",[198,683,684],{},"ascending=False"," draws the largest first, meaning they end up at the bottom of the pile. Forgetting ",[198,687,688],{},"setOrderByEnabled(True)"," stores the clause and ignores it — the same two-part pattern as ",[21,691,693],{"href":692},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fapply-map-theme-to-layout-map-pyqgis\u002F","following a map theme",", and the same easy mistake.",[14,696,697],{},[29,698,701,704,707,710,713,718,724,729,732,734,737,741,744,748,751,755,758,761],{"viewBox":699,"role":32,"ariaLabel":700,"xmlns":34},"0 0 760 288","Overlapping proportional circles drawn in arbitrary order with small symbols hidden behind large ones, compared with the same symbols drawn largest first so the small ones remain visible on top",[36,702,703],{},"Draw order decides which towns disappear",[40,705,706],{},"In arbitrary provider order a large circle can be painted over several small ones, hiding them completely. Sorting descending by the mapped value paints the largest first, so every smaller symbol lands on top of it and stays readable.",[44,708],{"x":46,"y":46,"width":47,"height":709,"fill":49},"288",[51,711,712],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Sorting is the difference between six towns and one",[44,714],{"x":715,"y":62,"width":716,"height":717,"rx":65,"fill":66,"stroke":67,"style":68},"26","330","204",[51,719,723],{"x":720,"y":721,"style":722,"fill":67,"textAnchor":57},"191","78","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","provider order",[82,725],{"cx":88,"cy":726,"r":94,"fill":727,"fillOpacity":728},"170","#2563eb",0.75,[82,730],{"cx":731,"cy":88,"r":65,"fill":727,"fillOpacity":728},"196",[82,733],{"cx":115,"cy":726,"r":62,"fill":727,"fillOpacity":728},[82,735],{"cx":149,"cy":717,"r":736,"fill":727,"fillOpacity":728},"8",[51,738,740],{"x":720,"y":739,"style":78,"fill":79,"textAnchor":57},"240","two towns are gone",[44,742],{"x":743,"y":62,"width":716,"height":717,"rx":65,"fill":116,"stroke":117,"style":68},"404",[51,745,747],{"x":746,"y":721,"style":722,"fill":117,"textAnchor":57},"569","largest painted first",[82,749],{"cx":750,"cy":726,"r":62,"fill":727,"fillOpacity":728},"558",[82,752],{"cx":753,"cy":726,"r":94,"fill":727,"fillOpacity":754},"528",0.9,[82,756],{"cx":757,"cy":88,"r":65,"fill":727,"fillOpacity":754},"574",[82,759],{"cx":760,"cy":717,"r":736,"fill":727,"fillOpacity":754},"630",[51,762,763],{"x":746,"y":739,"style":78,"fill":79,"textAnchor":57},"all four readable",[152,765,767],{"id":766},"the-graduated-size-alternative","The graduated-size alternative",[14,769,770],{},"A continuous size gives the most faithful encoding and the worst legend — a reader cannot read a value off a circle. Graduated size classes trade a little accuracy for a legend that works.",[191,772,774],{"className":193,"code":773,"language":195,"meta":196,"style":196},"from qgis.core import QgsGraduatedSymbolRenderer, QgsClassificationQuantile\n\nrenderer = QgsGraduatedSymbolRenderer(\"population\", [])\nrenderer.setSourceSymbol(symbol.clone())\nrenderer.setGraduatedMethod(QgsGraduatedSymbolRenderer.GraduatedSize)\nrenderer.setSymbolSizes(2.0, 12.0)\nrenderer.setClassificationMethod(QgsClassificationQuantile())\nrenderer.updateClasses(layer, 5)\nrenderer.setSymbolSizes(2.0, 12.0)\n\nlayer.setRenderer(renderer)\nlayer.triggerRepaint()\n",[198,775,776,787,791,805,810,815,830,835,845,857,861,866],{"__ignoreMap":196},[201,777,778,780,782,784],{"class":203,"line":204},[201,779,328],{"class":207},[201,781,331],{"class":211},[201,783,208],{"class":207},[201,785,786],{"class":211}," QgsGraduatedSymbolRenderer, QgsClassificationQuantile\n",[201,788,789],{"class":203,"line":215},[201,790,219],{"emptyLinePlaceholder":218},[201,792,793,795,797,800,802],{"class":203,"line":222},[201,794,614],{"class":211},[201,796,235],{"class":207},[201,798,799],{"class":211}," QgsGraduatedSymbolRenderer(",[201,801,391],{"class":368},[201,803,804],{"class":211},", [])\n",[201,806,807],{"class":203,"line":245},[201,808,809],{"class":211},"renderer.setSourceSymbol(symbol.clone())\n",[201,811,812],{"class":203,"line":274},[201,813,814],{"class":211},"renderer.setGraduatedMethod(QgsGraduatedSymbolRenderer.GraduatedSize)\n",[201,816,817,820,823,825,828],{"class":203,"line":283},[201,818,819],{"class":211},"renderer.setSymbolSizes(",[201,821,822],{"class":238},"2.0",[201,824,639],{"class":211},[201,826,827],{"class":238},"12.0",[201,829,351],{"class":211},[201,831,832],{"class":203,"line":380},[201,833,834],{"class":211},"renderer.setClassificationMethod(QgsClassificationQuantile())\n",[201,836,837,840,843],{"class":203,"line":397},[201,838,839],{"class":211},"renderer.updateClasses(layer, ",[201,841,842],{"class":238},"5",[201,844,351],{"class":211},[201,846,847,849,851,853,855],{"class":203,"line":402},[201,848,819],{"class":211},[201,850,822],{"class":238},[201,852,639],{"class":211},[201,854,827],{"class":238},[201,856,351],{"class":211},[201,858,859],{"class":203,"line":413},[201,860,219],{"emptyLinePlaceholder":218},[201,862,863],{"class":203,"line":428},[201,864,865],{"class":211},"layer.setRenderer(renderer)\n",[201,867,868],{"class":203,"line":441},[201,869,568],{"class":211},[14,871,872,573,874,877,878,881,882,573,886,889],{},[163,873,306],{},[198,875,876],{},"setGraduatedMethod(GraduatedSize)"," is what switches the renderer from varying colour to varying size — without it you get a five-colour choropleth of circles. ",[198,879,880],{},"setSymbolSizes()"," must be called ",[883,884,885],"em",{},"after",[198,887,888],{},"updateClasses()"," as well as before, because recomputing classes rebuilds the symbols and discards the sizes; calling it twice is ugly and reliable. Quantile classification puts an equal count in each class, which suits skewed count data far better than equal intervals, where four classes end up empty and everything lands in the first.",[14,891,892],{},"Each class now appears in the legend with its own circle and range label, which is the whole point. The cost is that two towns with meaningfully different populations can share a symbol.",[152,894,896],{"id":895},"labelling-the-symbols-without-wrecking-them","Labelling the symbols without wrecking them",[14,898,899],{},"A proportional symbol map usually needs a few values written on it, and the labelling has to respect the fact that the symbols vary in size.",[191,901,903],{"className":193,"code":902,"language":195,"meta":196,"style":196},"from qgis.core import (\n    QgsPalLayerSettings, QgsVectorLayerSimpleLabeling, QgsProperty,\n)\n\nsettings = QgsPalLayerSettings()\nsettings.fieldName = \"format_number(\\\"population\\\", 0)\"\nsettings.isExpression = True\nsettings.placement = QgsPalLayerSettings.OverPoint\nsettings.quadOffset = QgsPalLayerSettings.QuadrantOver\n\nsettings.dataDefinedProperties().setProperty(\n    QgsPalLayerSettings.Show,\n    QgsProperty.fromExpression(f'\"population\" > {maximum * 0.25}'),\n)\n\nlayer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\nlayer.setLabelsEnabled(True)\nlayer.triggerRepaint()\n",[198,904,905,915,920,924,928,938,959,969,979,989,993,998,1003,1029,1033,1037,1042,1051],{"__ignoreMap":196},[201,906,907,909,911,913],{"class":203,"line":204},[201,908,328],{"class":207},[201,910,331],{"class":211},[201,912,208],{"class":207},[201,914,336],{"class":211},[201,916,917],{"class":203,"line":215},[201,918,919],{"class":211},"    QgsPalLayerSettings, QgsVectorLayerSimpleLabeling, QgsProperty,\n",[201,921,922],{"class":203,"line":222},[201,923,351],{"class":211},[201,925,926],{"class":203,"line":245},[201,927,219],{"emptyLinePlaceholder":218},[201,929,930,933,935],{"class":203,"line":274},[201,931,932],{"class":211},"settings ",[201,934,235],{"class":207},[201,936,937],{"class":211}," QgsPalLayerSettings()\n",[201,939,940,943,945,948,951,954,956],{"class":203,"line":283},[201,941,942],{"class":211},"settings.fieldName ",[201,944,235],{"class":207},[201,946,947],{"class":368}," \"format_number(",[201,949,950],{"class":238},"\\\"",[201,952,953],{"class":368},"population",[201,955,950],{"class":238},[201,957,958],{"class":368},", 0)\"\n",[201,960,961,964,966],{"class":203,"line":380},[201,962,963],{"class":211},"settings.isExpression ",[201,965,235],{"class":207},[201,967,968],{"class":238}," True\n",[201,970,971,974,976],{"class":203,"line":397},[201,972,973],{"class":211},"settings.placement ",[201,975,235],{"class":207},[201,977,978],{"class":211}," QgsPalLayerSettings.OverPoint\n",[201,980,981,984,986],{"class":203,"line":402},[201,982,983],{"class":211},"settings.quadOffset ",[201,985,235],{"class":207},[201,987,988],{"class":211}," QgsPalLayerSettings.QuadrantOver\n",[201,990,991],{"class":203,"line":413},[201,992,219],{"emptyLinePlaceholder":218},[201,994,995],{"class":203,"line":428},[201,996,997],{"class":211},"settings.dataDefinedProperties().setProperty(\n",[201,999,1000],{"class":203,"line":441},[201,1001,1002],{"class":211},"    QgsPalLayerSettings.Show,\n",[201,1004,1005,1008,1011,1014,1016,1018,1020,1023,1026],{"class":203,"line":454},[201,1006,1007],{"class":211},"    QgsProperty.fromExpression(",[201,1009,1010],{"class":207},"f",[201,1012,1013],{"class":368},"'\"population\" > ",[201,1015,531],{"class":238},[201,1017,383],{"class":211},[201,1019,292],{"class":207},[201,1021,1022],{"class":238}," 0.25}",[201,1024,1025],{"class":368},"'",[201,1027,1028],{"class":211},"),\n",[201,1030,1031],{"class":203,"line":467},[201,1032,351],{"class":211},[201,1034,1035],{"class":203,"line":473},[201,1036,219],{"emptyLinePlaceholder":218},[201,1038,1039],{"class":203,"line":484},[201,1040,1041],{"class":211},"layer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\n",[201,1043,1044,1047,1049],{"class":203,"line":489},[201,1045,1046],{"class":211},"layer.setLabelsEnabled(",[201,1048,667],{"class":238},[201,1050,351],{"class":211},[201,1052,1053],{"class":203,"line":504},[201,1054,568],{"class":211},[14,1056,1057,573,1059,1062,1063,1066,1067,1070],{},[163,1058,306],{},[198,1060,1061],{},"QuadrantOver"," centres the label on the point, which only works where the symbol is big enough to hold it — hence the ",[198,1064,1065],{},"Show"," expression restricting labels to the larger quarter of the range. Labelling every symbol on a proportional map is almost always wrong: the small ones cannot contain their number and the label ends up bigger than the mark it describes. ",[198,1068,1069],{},"format_number()"," inserts thousands separators, which matters more here than in most places because these numbers are the point of the map.",[14,1072,1073,1074,1077,1078,1082],{},"For the smaller symbols that still need identifying, a second labelling pass with ",[198,1075,1076],{},"placement = OverPoint"," and a positive offset puts the name beside the circle rather than inside it. QGIS supports only one labelling configuration per layer, so the usual arrangement is a rule-based labelling object with two rules — one for the large symbols labelled inside, one for the rest labelled alongside — which the ",[21,1079,1081],{"href":1080},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcontrol-label-placement-and-collisions-pyqgis\u002F","label placement guide"," covers in more depth.",[152,1084,1086],{"id":1085},"keep-the-map-readable","Keep the map readable",[14,1088,1089],{},"Three adjustments do most of the work when the first attempt looks like a plate of bubbles.",[14,1091,1092],{},"Cap the maximum size against the smallest gap between features rather than against the page — if two towns are 6 mm apart at print scale, an 8 mm radius guarantees overlap. Consider a square root of a square root for extremely skewed data, accepting that the encoding becomes ordinal rather than proportional, and say so in the caption. And where the largest few values dwarf everything, split them out: draw the top three as labelled symbols and the rest proportionally, which is a common newspaper technique and more honest than a scale nobody can read.",[152,1094,1096],{"id":1095},"qgis-version-compatibility","QGIS version compatibility",[14,1098,1099,1100,1102],{},"The examples target ",[163,1101,165],{}," (Python 3.12).",[1104,1105,1106,1122],"table",{},[1107,1108,1109],"thead",{},[1110,1111,1112,1116,1119],"tr",{},[1113,1114,1115],"th",{},"QGIS version",[1113,1117,1118],{},"Python",[1113,1120,1121],{},"Notes",[1123,1124,1125,1140,1154,1167,1178],"tbody",{},[1110,1126,1127,1131,1134],{},[1128,1129,1130],"td",{},"3.16 LTR",[1128,1132,1133],{},"3.7",[1128,1135,1136,1139],{},[198,1137,1138],{},"GraduatedSize"," method and data-defined size both present.",[1110,1141,1142,1145,1148],{},[1128,1143,1144],{},"3.22 LTR",[1128,1146,1147],{},"3.9",[1128,1149,1150,1153],{},[198,1151,1152],{},"QgsClassificationMethod"," subclasses replace the older integer mode constants.",[1110,1155,1156,1159,1161],{},[1128,1157,1158],{},"3.28 LTR",[1128,1160,1147],{},[1128,1162,1163,1166],{},[198,1164,1165],{},"setOrderBy"," on renderers stable for feature draw order.",[1110,1168,1169,1172,1175],{},[1128,1170,1171],{},"3.34 LTR",[1128,1173,1174],{},"3.12",[1128,1176,1177],{},"Baseline for this page.",[1110,1179,1180,1183,1185],{},[1128,1181,1182],{},"3.40+",[1128,1184,1174],{},[1128,1186,1187],{},"Legend patch shapes let a proportional legend be drawn as nested circles.",[152,1189,1191],{"id":1190},"troubleshooting","Troubleshooting",[157,1193,1194,1204,1210,1218,1224,1232],{},[160,1195,1196,1199,1200,1203],{},[163,1197,1198],{},"Large values dominate absurdly."," The radius follows the value rather than its square root. Add ",[198,1201,1202],{},"sqrt()",".",[160,1205,1206,1209],{},[163,1207,1208],{},"Symbols rescale when you pan."," The maximum is being computed from the visible extent. Pin it with a literal in the expression.",[160,1211,1212,1215,1216,1203],{},[163,1213,1214],{},"Some symbols are a fixed medium size."," Null values are falling back to the static size. Wrap in ",[198,1217,585],{},[160,1219,1220,1223],{},[163,1221,1222],{},"Small towns are invisible."," Big symbols are painted last. Set an order-by clause descending and enable it.",[160,1225,1226,573,1229,1231],{},[163,1227,1228],{},"The graduated renderer changed colours instead of sizes.",[198,1230,876],{}," was not called.",[160,1233,1234,573,1237,1239,1240,1242],{},[163,1235,1236],{},"Sizes reverted after classification.",[198,1238,888],{}," rebuilt the symbols. Call ",[198,1241,880],{}," again afterwards.",[152,1244,1246],{"id":1245},"conclusion","Conclusion",[14,1248,1249],{},"Take the square root, pin the maximum, protect against nulls, and sort the draw order descending. Choose a continuous size when accuracy matters most and graduated classes when the legend does, and cap the largest symbol against the spacing of the data rather than the size of the page.",[152,1251,1253],{"id":1252},"frequently-asked-questions","Frequently Asked Questions",[14,1255,1256,1259,1260,1263,1264,1267],{},[163,1257,1258],{},"Can I do this with polygons?","\nYes — add a ",[21,1261,1262],{"href":172},"geometry generator"," symbol layer with ",[198,1265,1266],{},"point_on_surface($geometry)"," and a marker sub-symbol carrying the size expression. The polygon keeps its own fill underneath.",[14,1269,1270,1273,1274,1278],{},[163,1271,1272],{},"How do I build a legend showing nested circles?","\nQGIS 3.40 and newer support custom legend patch shapes; before that, the usual approach is to draw the nested-circle key as a ",[21,1275,1277],{"href":1276},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-text-annotation-to-map-pyqgis\u002F","layout picture or annotation"," rather than as a legend item.",[14,1280,1281,1284],{},[163,1282,1283],{},"Should the symbol be a circle?","\nCircles are the easiest to compare and the standard choice. Squares are marginally easier to judge accurately but read as less map-like; anything more elaborate makes area judgement harder, not easier.",[14,1286,1287,1290,1291,1294],{},[163,1288,1289],{},"What if some values are negative?","\nSize cannot encode sign. Map the absolute value to size and the sign to colour — a two-colour scheme with the same size expression wrapped in ",[198,1292,1293],{},"abs()"," is the usual solution.",[152,1296,1298],{"id":1297},"related","Related",[157,1300,1301,1306,1311,1317,1323],{},[160,1302,1303,1305],{},[21,1304,24],{"href":23}," — the guide this recipe belongs to",[160,1307,1308],{},[21,1309,1310],{"href":180},"Create a Choropleth Map in PyQGIS",[160,1312,1313],{},[21,1314,1316],{"href":1315},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis\u002F","Data-Defined Symbol Size with Expressions in PyQGIS",[160,1318,1319],{},[21,1320,1322],{"href":1321},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fstack-symbol-layers-pyqgis\u002F","Stack Symbol Layers in PyQGIS",[160,1324,1325],{},[21,1326,1328],{"href":1327},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002F","Classify a Layer with Natural Breaks in PyQGIS",[1330,1331,1332],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":196,"searchDepth":215,"depth":215,"links":1334},[1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346],{"id":154,"depth":215,"text":155},{"id":185,"depth":215,"text":186},{"id":314,"depth":215,"text":315},{"id":600,"depth":215,"text":601},{"id":766,"depth":215,"text":767},{"id":895,"depth":215,"text":896},{"id":1085,"depth":215,"text":1086},{"id":1095,"depth":215,"text":1096},{"id":1190,"depth":215,"text":1191},{"id":1245,"depth":215,"text":1246},{"id":1252,"depth":215,"text":1253},{"id":1297,"depth":215,"text":1298},"Size point symbols by a value in PyQGIS — square-root scaling so area encodes the number, QgsProperty overrides, graduated size classes, and a legend that reads correctly.","md",{"slug":1350,"type":1351,"breadcrumb":1352,"datePublished":1353,"dateModified":1353},"create-proportional-symbol-map-pyqgis","article","Proportional Symbols","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-proportional-symbol-map-pyqgis",{"title":5,"description":1347},"pyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-proportional-symbol-map-pyqgis\u002Findex","DUOuI6bohNKZVqVfcIsxi9p17CZog3gMawZOSrfa2Mo",1787823360557]