[{"data":1,"prerenderedAt":1431},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fpoint-cluster-and-displacement-renderer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1420,"extension":1421,"meta":1422,"navigation":207,"path":1427,"seo":1428,"stem":1429,"__hash__":1430},"docs\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fpoint-cluster-and-displacement-renderer-pyqgis\u002Findex.md","Use Point Cluster and Displacement Renderers in PyQGIS",{"type":7,"value":8,"toc":1405},"minimark",[9,13,17,26,132,137,147,151,154,431,438,442,449,516,685,690,694,697,850,859,863,866,1018,1023,1027,1030,1154,1168,1172,1180,1235,1243,1247,1260,1264,1288,1292,1320,1324,1330,1334,1340,1346,1352,1358,1368,1372,1401],[10,11,5],"h1",{"id":12},"use-point-cluster-and-displacement-renderers-in-pyqgis",[14,15,16],"p",{},"Point layers pile up. Thousands of incidents in a city centre become a solid blob at small scales; several addresses geocoded to the same building centroid sit exactly on top of each other, so a reader sees one symbol where there are five. Two QGIS renderers deal with this at display time without changing the data. The point cluster renderer merges nearby points into a single symbol showing how many it represents. The point displacement renderer spreads exactly coincident points around their shared location so each can be seen.",[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",". It sets up a cluster renderer with count labels and size by count, wraps an existing categorized style, configures displacement for coincident points, chooses tolerances that work across scales, and explains when to aggregate the data instead.",[14,27,28],{},[29,30,35,39,43,50,59,69,78,83,87,92,96,102,105,109,113,118,123,126,129],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","A cluster renderer merging nearby points into one counted symbol, and a displacement renderer spreading coincident points around a ring","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Cluster or displace",[40,41,42],"desc",{},"Left: many points close together at a small scale. The cluster renderer replaces groups within a distance tolerance with one symbol labelled with the count, so 27 nearby incidents show as one circle with 27. Right: five points at exactly the same location. The displacement renderer arranges them on a ring around the shared position with a centre marker, so each symbol, with its own category colour, is visible.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","280","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Too many to see, or exactly on top of each other?",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","52","340","200","8","#fffdf7","#59645f","stroke-width:1.5",[70,71],"circle",{"cx":72,"cy":73,"r":74,"fill":75,"stroke":76,"style":77},"120","140","26","#fdf2e2","#b45309","stroke-width:2.5",[51,79,82],{"x":72,"y":80,"style":81,"fill":56,"textAnchor":57},"145","text-anchor:middle;font-size:13.0px;font-family:sans-serif;font-weight:bold","27",[70,84],{"cx":85,"cy":72,"r":86,"fill":75,"stroke":76,"style":77},"250","16",[51,88,91],{"x":85,"y":89,"style":90,"fill":56,"textAnchor":57},"125","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","6",[70,93],{"cx":94,"cy":64,"r":91,"fill":76,"stroke":56,"style":95},"270","stroke-width:1",[51,97,101],{"x":98,"y":99,"style":100,"fill":76,"textAnchor":57},"194","238","text-anchor:middle;font-size:10.5px;font-family:sans-serif","cluster: nearby → one counted symbol",[44,103],{"x":104,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"396",[70,106],{"cx":107,"cy":80,"r":108,"fill":56,"stroke":56,"style":95},"566","4",[70,110],{"cx":107,"cy":111,"r":65,"fill":112,"stroke":56,"style":95},"105","#2563eb",[70,114],{"cx":115,"cy":116,"r":65,"fill":117,"stroke":56,"style":95},"604","133","#15803d",[70,119],{"cx":120,"cy":121,"r":65,"fill":122,"stroke":56,"style":95},"590","178","#b91c1c",[70,124],{"cx":125,"cy":121,"r":65,"fill":112,"stroke":56,"style":95},"542",[70,127],{"cx":128,"cy":116,"r":65,"fill":76,"stroke":56,"style":95},"528",[51,130,131],{"x":107,"y":99,"style":100,"fill":112,"textAnchor":57},"displacement: coincident → ring",[133,134,136],"h2",{"id":135},"prerequisites","Prerequisites",[138,139,140,144],"ul",{},[141,142,143],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[141,145,146],{},"A point layer with overlapping points — incidents, sightings, addresses — and ideally an existing style you want to keep for single points.",[133,148,150],{"id":149},"cluster-nearby-points","Cluster nearby points",[14,152,153],{},"The cluster renderer wraps another renderer: single points draw with the embedded renderer's symbols, and groups of points within a tolerance draw as one cluster symbol.",[155,156,161],"pre",{"className":157,"code":158,"language":159,"meta":160,"style":160},"language-python shiki shiki-themes github-dark","from qgis.core import (QgsProject, QgsPointClusterRenderer, QgsSingleSymbolRenderer,\n                       QgsMarkerSymbol, QgsUnitTypes, QgsFontMarkerSymbolLayer)\nfrom qgis.PyQt.QtGui import QColor\n\nincidents = QgsProject.instance().mapLayersByName(\"incidents\")[0]\nsingle = QgsSingleSymbolRenderer(QgsMarkerSymbol.createSimple(\n    {\"name\": \"circle\", \"color\": \"#b45309\", \"size\": \"2.2\", \"outline_color\": \"#17211d\", \"outline_width\": \"0.2\"}))\n\ncluster = QgsPointClusterRenderer()\ncluster.setEmbeddedRenderer(single)\ncluster.setTolerance(6)\ncluster.setToleranceUnit(QgsUnitTypes.RenderMillimeters)\n\ncluster_symbol = QgsMarkerSymbol.createSimple(\n    {\"name\": \"circle\", \"color\": \"#b45309\", \"size\": \"5\", \"outline_color\": \"#fffdf7\", \"outline_width\": \"0.5\"})\ncluster.setClusterSymbol(cluster_symbol)\n\nincidents.setRenderer(cluster)\nincidents.triggerRepaint()\n","python","",[162,163,164,183,189,202,209,234,245,304,309,320,326,337,343,348,359,408,414,419,425],"code",{"__ignoreMap":160},[165,166,169,173,177,180],"span",{"class":167,"line":168},"line",1,[165,170,172],{"class":171},"snl16","from",[165,174,176],{"class":175},"s95oV"," qgis.core ",[165,178,179],{"class":171},"import",[165,181,182],{"class":175}," (QgsProject, QgsPointClusterRenderer, QgsSingleSymbolRenderer,\n",[165,184,186],{"class":167,"line":185},2,[165,187,188],{"class":175},"                       QgsMarkerSymbol, QgsUnitTypes, QgsFontMarkerSymbolLayer)\n",[165,190,192,194,197,199],{"class":167,"line":191},3,[165,193,172],{"class":171},[165,195,196],{"class":175}," qgis.PyQt.QtGui ",[165,198,179],{"class":171},[165,200,201],{"class":175}," QColor\n",[165,203,205],{"class":167,"line":204},4,[165,206,208],{"emptyLinePlaceholder":207},true,"\n",[165,210,212,215,218,221,225,228,231],{"class":167,"line":211},5,[165,213,214],{"class":175},"incidents ",[165,216,217],{"class":171},"=",[165,219,220],{"class":175}," QgsProject.instance().mapLayersByName(",[165,222,224],{"class":223},"sU2Wk","\"incidents\"",[165,226,227],{"class":175},")[",[165,229,46],{"class":230},"sDLfK",[165,232,233],{"class":175},"]\n",[165,235,237,240,242],{"class":167,"line":236},6,[165,238,239],{"class":175},"single ",[165,241,217],{"class":171},[165,243,244],{"class":175}," QgsSingleSymbolRenderer(QgsMarkerSymbol.createSimple(\n",[165,246,248,251,254,257,260,263,266,268,271,273,276,278,281,283,286,288,291,293,296,298,301],{"class":167,"line":247},7,[165,249,250],{"class":175},"    {",[165,252,253],{"class":223},"\"name\"",[165,255,256],{"class":175},": ",[165,258,259],{"class":223},"\"circle\"",[165,261,262],{"class":175},", ",[165,264,265],{"class":223},"\"color\"",[165,267,256],{"class":175},[165,269,270],{"class":223},"\"#b45309\"",[165,272,262],{"class":175},[165,274,275],{"class":223},"\"size\"",[165,277,256],{"class":175},[165,279,280],{"class":223},"\"2.2\"",[165,282,262],{"class":175},[165,284,285],{"class":223},"\"outline_color\"",[165,287,256],{"class":175},[165,289,290],{"class":223},"\"#17211d\"",[165,292,262],{"class":175},[165,294,295],{"class":223},"\"outline_width\"",[165,297,256],{"class":175},[165,299,300],{"class":223},"\"0.2\"",[165,302,303],{"class":175},"}))\n",[165,305,307],{"class":167,"line":306},8,[165,308,208],{"emptyLinePlaceholder":207},[165,310,312,315,317],{"class":167,"line":311},9,[165,313,314],{"class":175},"cluster ",[165,316,217],{"class":171},[165,318,319],{"class":175}," QgsPointClusterRenderer()\n",[165,321,323],{"class":167,"line":322},10,[165,324,325],{"class":175},"cluster.setEmbeddedRenderer(single)\n",[165,327,329,332,334],{"class":167,"line":328},11,[165,330,331],{"class":175},"cluster.setTolerance(",[165,333,91],{"class":230},[165,335,336],{"class":175},")\n",[165,338,340],{"class":167,"line":339},12,[165,341,342],{"class":175},"cluster.setToleranceUnit(QgsUnitTypes.RenderMillimeters)\n",[165,344,346],{"class":167,"line":345},13,[165,347,208],{"emptyLinePlaceholder":207},[165,349,351,354,356],{"class":167,"line":350},14,[165,352,353],{"class":175},"cluster_symbol ",[165,355,217],{"class":171},[165,357,358],{"class":175}," QgsMarkerSymbol.createSimple(\n",[165,360,362,364,366,368,370,372,374,376,378,380,382,384,387,389,391,393,396,398,400,402,405],{"class":167,"line":361},15,[165,363,250],{"class":175},[165,365,253],{"class":223},[165,367,256],{"class":175},[165,369,259],{"class":223},[165,371,262],{"class":175},[165,373,265],{"class":223},[165,375,256],{"class":175},[165,377,270],{"class":223},[165,379,262],{"class":175},[165,381,275],{"class":223},[165,383,256],{"class":175},[165,385,386],{"class":223},"\"5\"",[165,388,262],{"class":175},[165,390,285],{"class":223},[165,392,256],{"class":175},[165,394,395],{"class":223},"\"#fffdf7\"",[165,397,262],{"class":175},[165,399,295],{"class":223},[165,401,256],{"class":175},[165,403,404],{"class":223},"\"0.5\"",[165,406,407],{"class":175},"})\n",[165,409,411],{"class":167,"line":410},16,[165,412,413],{"class":175},"cluster.setClusterSymbol(cluster_symbol)\n",[165,415,417],{"class":167,"line":416},17,[165,418,208],{"emptyLinePlaceholder":207},[165,420,422],{"class":167,"line":421},18,[165,423,424],{"class":175},"incidents.setRenderer(cluster)\n",[165,426,428],{"class":167,"line":427},19,[165,429,430],{"class":175},"incidents.triggerRepaint()\n",[14,432,433,437],{},[434,435,436],"strong",{},"Breakdown:"," The embedded renderer draws points that are not clustered, so the cluster renderer can sit on top of any existing style. The tolerance is the distance within which points join a cluster, in millimetres on the map, so clustering adapts to scale automatically: zoom in and clusters break apart. The cluster symbol is an ordinary marker symbol, drawn once per cluster; the next section adds the count to it and sizes it by the number of points it represents.",[133,439,441],{"id":440},"show-and-scale-by-the-count","Show and scale by the count",[14,443,444,445,448],{},"A cluster symbol that shows its count, and grows with it, tells readers how much each cluster represents. The ",[162,446,447],{},"@cluster_size"," variable is available inside cluster symbols for both.",[14,450,451],{},[29,452,455,458,461,464,467,472,475,479,483,487,491,497,502,508,512],{"viewBox":453,"role":32,"ariaLabel":454,"xmlns":34},"0 0 760 240","A cluster symbol printing @cluster_size as text and growing with the square root of the count",[36,456,457],{},"Count label and size by count",[40,459,460],{},"Inside the cluster symbol the variable @cluster_size holds the number of points. A font marker layer uses it as its character, so the count is printed on the symbol. The circle's size is data-defined by an expression such as 4 plus square root of cluster size, so a cluster of 100 is visibly larger than one of 4, without huge symbols for big clusters.",[44,462],{"x":46,"y":46,"width":47,"height":463,"fill":49},"240",[51,465,466],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"@cluster_size drives text and size",[70,468],{"cx":469,"cy":470,"r":471,"fill":75,"stroke":76,"style":77},"160","130","12",[51,473,108],{"x":469,"y":474,"style":90,"fill":56,"textAnchor":57},"135",[70,476],{"cx":477,"cy":470,"r":478,"fill":75,"stroke":76,"style":77},"300","22",[51,480,482],{"x":477,"y":474,"style":481,"fill":56,"textAnchor":57},"text-anchor:middle;font-size:12.0px;font-family:sans-serif;font-weight:bold","25",[70,484],{"cx":485,"cy":470,"r":486,"fill":75,"stroke":76,"style":77},"470","36",[51,488,490],{"x":485,"y":489,"style":81,"fill":56,"textAnchor":57},"136","100",[44,492],{"x":493,"y":494,"width":495,"height":490,"rx":65,"fill":66,"stroke":67,"style":496},"560","80","176","stroke-width:2",[51,498,501],{"x":499,"y":500,"style":90,"fill":56,"textAnchor":57},"648","109.6","size =",[51,503,507],{"x":499,"y":504,"style":505,"fill":506,"textAnchor":57},"133.6","text-anchor:middle;font-size:9.5px;font-family:monospace","#2f3b35","4 + sqrt(",[51,509,511],{"x":499,"y":510,"style":505,"fill":506,"textAnchor":57},"157.6","cluster_size)",[51,513,515],{"x":477,"y":514,"style":100,"fill":67,"textAnchor":57},"210","area grows with the count, not linearly with it",[155,517,519],{"className":157,"code":518,"language":159,"meta":160,"style":160},"from qgis.core import QgsProperty, QgsSymbolLayer\n\ncluster_symbol = QgsMarkerSymbol.createSimple(\n    {\"name\": \"circle\", \"color\": \"#b45309\", \"size\": \"5\", \"outline_color\": \"#fffdf7\", \"outline_width\": \"0.5\"})\ncluster_symbol.symbolLayer(0).setDataDefinedProperty(\n    QgsSymbolLayer.PropertySize, QgsProperty.fromExpression(\"4 + sqrt(@cluster_size)\"))\n\ntext_layer = QgsFontMarkerSymbolLayer(\"Noto Sans\", \"0\", 2.6, QColor(\"#17211d\"))\ntext_layer.setDataDefinedProperty(QgsSymbolLayer.PropertyCharacter,\n                                  QgsProperty.fromExpression(\"@cluster_size\"))\ntext_layer.setDataDefinedProperty(QgsSymbolLayer.PropertySize,\n                                  QgsProperty.fromExpression(\"2 + sqrt(@cluster_size) \u002F 3\"))\ncluster_symbol.appendSymbolLayer(text_layer)\ncluster.setClusterSymbol(cluster_symbol)\nincidents.triggerRepaint()\n",[162,520,521,532,536,544,588,598,609,613,643,648,658,663,672,677,681],{"__ignoreMap":160},[165,522,523,525,527,529],{"class":167,"line":168},[165,524,172],{"class":171},[165,526,176],{"class":175},[165,528,179],{"class":171},[165,530,531],{"class":175}," QgsProperty, QgsSymbolLayer\n",[165,533,534],{"class":167,"line":185},[165,535,208],{"emptyLinePlaceholder":207},[165,537,538,540,542],{"class":167,"line":191},[165,539,353],{"class":175},[165,541,217],{"class":171},[165,543,358],{"class":175},[165,545,546,548,550,552,554,556,558,560,562,564,566,568,570,572,574,576,578,580,582,584,586],{"class":167,"line":204},[165,547,250],{"class":175},[165,549,253],{"class":223},[165,551,256],{"class":175},[165,553,259],{"class":223},[165,555,262],{"class":175},[165,557,265],{"class":223},[165,559,256],{"class":175},[165,561,270],{"class":223},[165,563,262],{"class":175},[165,565,275],{"class":223},[165,567,256],{"class":175},[165,569,386],{"class":223},[165,571,262],{"class":175},[165,573,285],{"class":223},[165,575,256],{"class":175},[165,577,395],{"class":223},[165,579,262],{"class":175},[165,581,295],{"class":223},[165,583,256],{"class":175},[165,585,404],{"class":223},[165,587,407],{"class":175},[165,589,590,593,595],{"class":167,"line":211},[165,591,592],{"class":175},"cluster_symbol.symbolLayer(",[165,594,46],{"class":230},[165,596,597],{"class":175},").setDataDefinedProperty(\n",[165,599,600,603,606],{"class":167,"line":236},[165,601,602],{"class":175},"    QgsSymbolLayer.PropertySize, QgsProperty.fromExpression(",[165,604,605],{"class":223},"\"4 + sqrt(@cluster_size)\"",[165,607,608],{"class":175},"))\n",[165,610,611],{"class":167,"line":247},[165,612,208],{"emptyLinePlaceholder":207},[165,614,615,618,620,623,626,628,631,633,636,639,641],{"class":167,"line":306},[165,616,617],{"class":175},"text_layer ",[165,619,217],{"class":171},[165,621,622],{"class":175}," QgsFontMarkerSymbolLayer(",[165,624,625],{"class":223},"\"Noto Sans\"",[165,627,262],{"class":175},[165,629,630],{"class":223},"\"0\"",[165,632,262],{"class":175},[165,634,635],{"class":230},"2.6",[165,637,638],{"class":175},", QColor(",[165,640,290],{"class":223},[165,642,608],{"class":175},[165,644,645],{"class":167,"line":311},[165,646,647],{"class":175},"text_layer.setDataDefinedProperty(QgsSymbolLayer.PropertyCharacter,\n",[165,649,650,653,656],{"class":167,"line":322},[165,651,652],{"class":175},"                                  QgsProperty.fromExpression(",[165,654,655],{"class":223},"\"@cluster_size\"",[165,657,608],{"class":175},[165,659,660],{"class":167,"line":328},[165,661,662],{"class":175},"text_layer.setDataDefinedProperty(QgsSymbolLayer.PropertySize,\n",[165,664,665,667,670],{"class":167,"line":339},[165,666,652],{"class":175},[165,668,669],{"class":223},"\"2 + sqrt(@cluster_size) \u002F 3\"",[165,671,608],{"class":175},[165,673,674],{"class":167,"line":345},[165,675,676],{"class":175},"cluster_symbol.appendSymbolLayer(text_layer)\n",[165,678,679],{"class":167,"line":350},[165,680,413],{"class":175},[165,682,683],{"class":167,"line":361},[165,684,430],{"class":175},[14,686,687,689],{},[434,688,436],{}," Sizing by the square root of the count keeps symbol area roughly proportional to the number of points, so a cluster of 100 is not drawn 25 times wider than a cluster of 4. The font marker's character is data-defined to the count, so the number is drawn on the symbol; its size grows a little with the count to stay legible inside larger circles. Contrast matters: dark text on a mid-tone fill, or white text with a thin outline on a dark one, keeps counts readable on any basemap.",[133,691,693],{"id":692},"keep-categories-visible","Keep categories visible",[14,695,696],{},"Clustering hides categories inside clusters — a red burglary and a blue traffic incident merge into one orange circle. Wrapping a categorized renderer keeps categories for single points, and an expression can colour clusters by their dominant category.",[155,698,700],{"className":157,"code":699,"language":159,"meta":160,"style":160},"from qgis.core import QgsCategorizedSymbolRenderer, QgsRendererCategory\n\ncats = [QgsRendererCategory(v, QgsMarkerSymbol.createSimple(\n            {\"name\": \"circle\", \"color\": c, \"size\": \"2.2\", \"outline_color\": \"#17211d\"}), v)\n        for v, c in ((\"burglary\", \"#b91c1c\"), (\"traffic\", \"#2563eb\"), (\"vandalism\", \"#15803d\"))]\ncluster.setEmbeddedRenderer(QgsCategorizedSymbolRenderer(\"type\", cats))\n\ncluster_symbol.symbolLayer(0).setDataDefinedProperty(\n    QgsSymbolLayer.PropertyFillColor,\n    QgsProperty.fromExpression(\"CASE WHEN @cluster_color IS NOT NULL THEN @cluster_color ELSE '#59645f' END\"))\nincidents.triggerRepaint()\n",[162,701,702,713,717,727,762,808,819,823,831,836,846],{"__ignoreMap":160},[165,703,704,706,708,710],{"class":167,"line":168},[165,705,172],{"class":171},[165,707,176],{"class":175},[165,709,179],{"class":171},[165,711,712],{"class":175}," QgsCategorizedSymbolRenderer, QgsRendererCategory\n",[165,714,715],{"class":167,"line":185},[165,716,208],{"emptyLinePlaceholder":207},[165,718,719,722,724],{"class":167,"line":191},[165,720,721],{"class":175},"cats ",[165,723,217],{"class":171},[165,725,726],{"class":175}," [QgsRendererCategory(v, QgsMarkerSymbol.createSimple(\n",[165,728,729,732,734,736,738,740,742,745,747,749,751,753,755,757,759],{"class":167,"line":204},[165,730,731],{"class":175},"            {",[165,733,253],{"class":223},[165,735,256],{"class":175},[165,737,259],{"class":223},[165,739,262],{"class":175},[165,741,265],{"class":223},[165,743,744],{"class":175},": c, ",[165,746,275],{"class":223},[165,748,256],{"class":175},[165,750,280],{"class":223},[165,752,262],{"class":175},[165,754,285],{"class":223},[165,756,256],{"class":175},[165,758,290],{"class":223},[165,760,761],{"class":175},"}), v)\n",[165,763,764,767,770,773,776,779,781,784,787,790,792,795,797,800,802,805],{"class":167,"line":211},[165,765,766],{"class":171},"        for",[165,768,769],{"class":175}," v, c ",[165,771,772],{"class":171},"in",[165,774,775],{"class":175}," ((",[165,777,778],{"class":223},"\"burglary\"",[165,780,262],{"class":175},[165,782,783],{"class":223},"\"#b91c1c\"",[165,785,786],{"class":175},"), (",[165,788,789],{"class":223},"\"traffic\"",[165,791,262],{"class":175},[165,793,794],{"class":223},"\"#2563eb\"",[165,796,786],{"class":175},[165,798,799],{"class":223},"\"vandalism\"",[165,801,262],{"class":175},[165,803,804],{"class":223},"\"#15803d\"",[165,806,807],{"class":175},"))]\n",[165,809,810,813,816],{"class":167,"line":236},[165,811,812],{"class":175},"cluster.setEmbeddedRenderer(QgsCategorizedSymbolRenderer(",[165,814,815],{"class":223},"\"type\"",[165,817,818],{"class":175},", cats))\n",[165,820,821],{"class":167,"line":247},[165,822,208],{"emptyLinePlaceholder":207},[165,824,825,827,829],{"class":167,"line":306},[165,826,592],{"class":175},[165,828,46],{"class":230},[165,830,597],{"class":175},[165,832,833],{"class":167,"line":311},[165,834,835],{"class":175},"    QgsSymbolLayer.PropertyFillColor,\n",[165,837,838,841,844],{"class":167,"line":322},[165,839,840],{"class":175},"    QgsProperty.fromExpression(",[165,842,843],{"class":223},"\"CASE WHEN @cluster_color IS NOT NULL THEN @cluster_color ELSE '#59645f' END\"",[165,845,608],{"class":175},[165,847,848],{"class":167,"line":328},[165,849,430],{"class":175},[14,851,852,854,855,858],{},[434,853,436],{}," Single points keep their category colours from the embedded categorized renderer. ",[162,856,857],{},"@cluster_color"," is set when all points in a cluster share the same colour, so a cluster of three burglaries stays red while a mixed cluster turns neutral grey — honest about mixing without inventing a dominant category. For per-category counts inside clusters, separate layers per category, each with its own cluster renderer, are clearer than one mixed layer.",[133,860,862],{"id":861},"displace-coincident-points","Displace coincident points",[14,864,865],{},"When points share the exact same location, clustering would just count them. The displacement renderer instead arranges them around their common position so each symbol — and each category — is visible.",[155,867,869],{"className":157,"code":868,"language":159,"meta":160,"style":160},"from qgis.core import QgsPointDisplacementRenderer\n\naddresses = QgsProject.instance().mapLayersByName(\"geocoded_addresses\")[0]\ndisplace = QgsPointDisplacementRenderer()\ndisplace.setEmbeddedRenderer(addresses.renderer().clone())\ndisplace.setTolerance(0.5)\ndisplace.setToleranceUnit(QgsUnitTypes.RenderMillimeters)\ndisplace.setPlacement(QgsPointDisplacementRenderer.ConcentricRings)\ndisplace.setCircleRadiusAddition(0)\ndisplace.setCircleColor(QColor(\"#59645f\"))\ndisplace.setCircleWidth(0.2)\ndisplace.setCenterSymbol(QgsMarkerSymbol.createSimple({\"name\": \"circle\", \"color\": \"#17211d\", \"size\": \"1\"}))\ndisplace.setLabelAttributeName(\"house_no\")\naddresses.setRenderer(displace)\naddresses.triggerRepaint()\n",[162,870,871,882,886,904,914,919,929,934,939,948,958,968,998,1008,1013],{"__ignoreMap":160},[165,872,873,875,877,879],{"class":167,"line":168},[165,874,172],{"class":171},[165,876,176],{"class":175},[165,878,179],{"class":171},[165,880,881],{"class":175}," QgsPointDisplacementRenderer\n",[165,883,884],{"class":167,"line":185},[165,885,208],{"emptyLinePlaceholder":207},[165,887,888,891,893,895,898,900,902],{"class":167,"line":191},[165,889,890],{"class":175},"addresses ",[165,892,217],{"class":171},[165,894,220],{"class":175},[165,896,897],{"class":223},"\"geocoded_addresses\"",[165,899,227],{"class":175},[165,901,46],{"class":230},[165,903,233],{"class":175},[165,905,906,909,911],{"class":167,"line":204},[165,907,908],{"class":175},"displace ",[165,910,217],{"class":171},[165,912,913],{"class":175}," QgsPointDisplacementRenderer()\n",[165,915,916],{"class":167,"line":211},[165,917,918],{"class":175},"displace.setEmbeddedRenderer(addresses.renderer().clone())\n",[165,920,921,924,927],{"class":167,"line":236},[165,922,923],{"class":175},"displace.setTolerance(",[165,925,926],{"class":230},"0.5",[165,928,336],{"class":175},[165,930,931],{"class":167,"line":247},[165,932,933],{"class":175},"displace.setToleranceUnit(QgsUnitTypes.RenderMillimeters)\n",[165,935,936],{"class":167,"line":306},[165,937,938],{"class":175},"displace.setPlacement(QgsPointDisplacementRenderer.ConcentricRings)\n",[165,940,941,944,946],{"class":167,"line":311},[165,942,943],{"class":175},"displace.setCircleRadiusAddition(",[165,945,46],{"class":230},[165,947,336],{"class":175},[165,949,950,953,956],{"class":167,"line":322},[165,951,952],{"class":175},"displace.setCircleColor(QColor(",[165,954,955],{"class":223},"\"#59645f\"",[165,957,608],{"class":175},[165,959,960,963,966],{"class":167,"line":328},[165,961,962],{"class":175},"displace.setCircleWidth(",[165,964,965],{"class":230},"0.2",[165,967,336],{"class":175},[165,969,970,973,975,977,979,981,983,985,987,989,991,993,996],{"class":167,"line":339},[165,971,972],{"class":175},"displace.setCenterSymbol(QgsMarkerSymbol.createSimple({",[165,974,253],{"class":223},[165,976,256],{"class":175},[165,978,259],{"class":223},[165,980,262],{"class":175},[165,982,265],{"class":223},[165,984,256],{"class":175},[165,986,290],{"class":223},[165,988,262],{"class":175},[165,990,275],{"class":223},[165,992,256],{"class":175},[165,994,995],{"class":223},"\"1\"",[165,997,303],{"class":175},[165,999,1000,1003,1006],{"class":167,"line":345},[165,1001,1002],{"class":175},"displace.setLabelAttributeName(",[165,1004,1005],{"class":223},"\"house_no\"",[165,1007,336],{"class":175},[165,1009,1010],{"class":167,"line":350},[165,1011,1012],{"class":175},"addresses.setRenderer(displace)\n",[165,1014,1015],{"class":167,"line":361},[165,1016,1017],{"class":175},"addresses.triggerRepaint()\n",[14,1019,1020,1022],{},[434,1021,436],{}," A very small tolerance means only points that practically coincide are displaced, which is the right use: geocoding to building centroids, multiple records at one site. Concentric rings scale better than a single ring when many points share a location; a grid placement is another option. The centre symbol marks the true location, and the ring shows that the surrounding symbols are offsets, not real positions. A label attribute can label each displaced symbol — here the house number — which turns an unreadable pile into a readable list.",[133,1024,1026],{"id":1025},"save-and-reuse-the-clustered-style","Save and reuse the clustered style",[14,1028,1029],{},"Cluster and displacement renderers, with their data-defined cluster symbols, take some effort to tune. Saving the result as a QML style makes it reusable on every similar point layer and keeps maps consistent.",[155,1031,1033],{"className":157,"code":1032,"language":159,"meta":160,"style":160},"from qgis.core import QgsMapLayer\n\nok, msg = incidents.saveNamedStyle(\"\u002Fdata\u002Fstyles\u002Fincidents_clustered.qml\",\n                                   categories=QgsMapLayer.Symbology)\nprint(\"saved:\", ok, msg)\n\nfor name in (\"incidents_2025\", \"incidents_2026\"):\n    lyr = QgsProject.instance().mapLayersByName(name)[0]\n    lyr.loadNamedStyle(\"\u002Fdata\u002Fstyles\u002Fincidents_clustered.qml\", categories=QgsMapLayer.Symbology)\n    lyr.triggerRepaint()\n",[162,1034,1035,1046,1050,1066,1077,1091,1095,1119,1133,1149],{"__ignoreMap":160},[165,1036,1037,1039,1041,1043],{"class":167,"line":168},[165,1038,172],{"class":171},[165,1040,176],{"class":175},[165,1042,179],{"class":171},[165,1044,1045],{"class":175}," QgsMapLayer\n",[165,1047,1048],{"class":167,"line":185},[165,1049,208],{"emptyLinePlaceholder":207},[165,1051,1052,1055,1057,1060,1063],{"class":167,"line":191},[165,1053,1054],{"class":175},"ok, msg ",[165,1056,217],{"class":171},[165,1058,1059],{"class":175}," incidents.saveNamedStyle(",[165,1061,1062],{"class":223},"\"\u002Fdata\u002Fstyles\u002Fincidents_clustered.qml\"",[165,1064,1065],{"class":175},",\n",[165,1067,1068,1072,1074],{"class":167,"line":204},[165,1069,1071],{"class":1070},"s9osk","                                   categories",[165,1073,217],{"class":171},[165,1075,1076],{"class":175},"QgsMapLayer.Symbology)\n",[165,1078,1079,1082,1085,1088],{"class":167,"line":211},[165,1080,1081],{"class":230},"print",[165,1083,1084],{"class":175},"(",[165,1086,1087],{"class":223},"\"saved:\"",[165,1089,1090],{"class":175},", ok, msg)\n",[165,1092,1093],{"class":167,"line":236},[165,1094,208],{"emptyLinePlaceholder":207},[165,1096,1097,1100,1103,1105,1108,1111,1113,1116],{"class":167,"line":247},[165,1098,1099],{"class":171},"for",[165,1101,1102],{"class":175}," name ",[165,1104,772],{"class":171},[165,1106,1107],{"class":175}," (",[165,1109,1110],{"class":223},"\"incidents_2025\"",[165,1112,262],{"class":175},[165,1114,1115],{"class":223},"\"incidents_2026\"",[165,1117,1118],{"class":175},"):\n",[165,1120,1121,1124,1126,1129,1131],{"class":167,"line":306},[165,1122,1123],{"class":175},"    lyr ",[165,1125,217],{"class":171},[165,1127,1128],{"class":175}," QgsProject.instance().mapLayersByName(name)[",[165,1130,46],{"class":230},[165,1132,233],{"class":175},[165,1134,1135,1138,1140,1142,1145,1147],{"class":167,"line":311},[165,1136,1137],{"class":175},"    lyr.loadNamedStyle(",[165,1139,1062],{"class":223},[165,1141,262],{"class":175},[165,1143,1144],{"class":1070},"categories",[165,1146,217],{"class":171},[165,1148,1076],{"class":175},[165,1150,1151],{"class":167,"line":322},[165,1152,1153],{"class":175},"    lyr.triggerRepaint()\n",[14,1155,1156,1158,1159,1162,1163,1167],{},[434,1157,436],{}," Restricting the saved categories to symbology stores the renderer — including the embedded categorized renderer and the data-defined cluster symbol — without labels, forms or other settings, so it applies cleanly to any layer with the same ",[162,1160,1161],{},"type"," field. Loading it on yearly layers gives every year the same look, which matters when the maps are compared side by side. Styles saved this way can also live in a GeoPackage or database next to the data, as described in ",[21,1164,1166],{"href":1165},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fsave-styles-to-database-pyqgis\u002F","saving styles to a database",".",[133,1169,1171],{"id":1170},"choose-tolerances-that-work-across-scales","Choose tolerances that work across scales",[14,1173,1174,1175,1179],{},"Tolerances in map units cluster the same real distance at every scale; tolerances in millimetres cluster what ",[1176,1177,1178],"em",{},"looks"," close at the current scale. For display, millimetres are almost always right.",[14,1181,1182],{},[29,1183,1186,1189,1192,1195,1198,1203,1209,1213,1217,1221,1223,1226,1229,1232],{"viewBox":1184,"role":32,"ariaLabel":1185,"xmlns":34},"0 0 760 230","A millimetre tolerance clustering what overlaps on screen at every scale, compared with a fixed map-unit tolerance",[36,1187,1188],{},"Tolerance in screen or map units",[40,1190,1191],{},"A tolerance of 6 millimetres clusters points that would overlap visually, so clusters break apart as you zoom in and merge as you zoom out, matching what the eye sees. A tolerance of 500 map metres always merges points within 500 metres, which over-clusters at large scales and under-clusters at small ones.",[44,1193],{"x":46,"y":46,"width":47,"height":1194,"fill":49},"230",[51,1196,1197],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Cluster what overlaps on screen",[44,1199],{"x":61,"y":1200,"width":63,"height":1201,"rx":65,"fill":1202,"stroke":117,"style":496},"56","150","#e8efe6",[51,1204,1208],{"x":98,"y":1205,"style":1206,"fill":1207,"textAnchor":57},"92.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","#166534","millimetres",[51,1210,1212],{"x":98,"y":1211,"style":100,"fill":506,"textAnchor":57},"120.78","visual overlap",[51,1214,1216],{"x":98,"y":1215,"style":100,"fill":506,"textAnchor":57},"148.78","adapts to zoom",[51,1218,1220],{"x":98,"y":1219,"style":100,"fill":67,"textAnchor":57},"176.78","usually right",[44,1222],{"x":104,"y":1200,"width":63,"height":1201,"rx":65,"fill":75,"stroke":76,"style":496},[51,1224,1225],{"x":107,"y":1205,"style":1206,"fill":76,"textAnchor":57},"map units",[51,1227,1228],{"x":107,"y":1211,"style":100,"fill":506,"textAnchor":57},"fixed real distance",[51,1230,1231],{"x":107,"y":1215,"style":100,"fill":506,"textAnchor":57},"same at every scale",[51,1233,1234],{"x":107,"y":1219,"style":100,"fill":67,"textAnchor":57},"analysis, not display",[14,1236,1237,1238,1242],{},"A millimetre tolerance about twice the symbol size clusters exactly the points that would otherwise overlap. If clustering has an analytical meaning — \"incidents within 100 m count as one hotspot\" — that is an analysis, not a display choice, and belongs in a ",[21,1239,1241],{"href":1240},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcluster-points-with-dbscan-and-kmeans-pyqgis\u002F","DBSCAN clustering"," or aggregation step whose result is stored and styled. Renderers change only the picture; the attribute table and every analysis still see all the points.",[133,1244,1246],{"id":1245},"when-to-aggregate-instead","When to aggregate instead",[14,1248,1249,1250,1254,1255,1259],{},"Cluster renderers suit interactive maps where users zoom. For printed maps at a single scale, and for very large point sets, aggregating into a hexagon grid or administrative units and styling the counts is usually clearer: every area shows its count, nothing is hidden behind symbols, and the counts can be normalised by population or area. ",[21,1251,1253],{"href":1252},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fcreate-hexagon-grid-and-count-points-pyqgis\u002F","Creating a hexagon grid and counting points"," shows the aggregation route, and ",[21,1256,1258],{"href":1257},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fkernel-density-heatmap-raster-pyqgis\u002F","kernel density"," a continuous one.",[133,1261,1263],{"id":1262},"qgis-version-compatibility","QGIS version compatibility",[14,1265,1266,1269,1270,1273,1274,1269,1276,1278,1279,262,1282,262,1285,1167],{},[162,1267,1268],{},"QgsPointClusterRenderer"," and ",[162,1271,1272],{},"QgsPointDisplacementRenderer"," work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. The ",[162,1275,447],{},[162,1277,857],{}," variables are available inside cluster symbols on all these releases. On QGIS 4, enums are scoped: ",[162,1280,1281],{},"QgsPointDisplacementRenderer.Placement.ConcentricRings",[162,1283,1284],{},"QgsSymbolLayer.Property.Size",[162,1286,1287],{},"Qgis.RenderUnit.Millimeters",[133,1289,1291],{"id":1290},"troubleshooting","Troubleshooting",[138,1293,1294,1300,1308,1314],{},[141,1295,1296,1299],{},[434,1297,1298],{},"Everything becomes one cluster."," The tolerance is in map units or far too large; use a few millimetres.",[141,1301,1302,1305,1306,1167],{},[434,1303,1304],{},"Counts do not appear."," The font marker's character is not data-defined to ",[162,1307,447],{},[141,1309,1310,1313],{},[434,1311,1312],{},"Categories disappear."," The embedded renderer is single-symbol; embed the categorized renderer.",[141,1315,1316,1319],{},[434,1317,1318],{},"Displacement has no effect."," Points are close but not coincident within the tolerance; displacement is for near-identical positions.",[133,1321,1323],{"id":1322},"conclusion","Conclusion",[14,1325,1326,1327,1329],{},"Wrap the existing renderer in a point cluster renderer with a millimetre tolerance, print ",[162,1328,447],{}," on the cluster symbol and size it by the square root of the count, keep categories for single points and colour clusters only when they are uniform, use the displacement renderer for truly coincident points with a centre marker and labels, and aggregate into areas for print maps and analysis.",[133,1331,1333],{"id":1332},"frequently-asked-questions","Frequently Asked Questions",[14,1335,1336,1339],{},[434,1337,1338],{},"Do cluster renderers change the data?","\nNo. They change only how points are drawn; queries and exports see every point.",[14,1341,1342,1345],{},[434,1343,1344],{},"Can I click a cluster to see its points?","\nThe identify tool returns all points in a cluster; plugins can zoom to their extent.",[14,1347,1348,1351],{},[434,1349,1350],{},"Does clustering work in print layouts?","\nYes, at the layout map's scale; check the result at the print scale.",[14,1353,1354,1357],{},[434,1355,1356],{},"Can I cluster only at small scales?","\nYes — use rule-based scale ranges, or two copies of the layer with scale-dependent visibility: clustered when zoomed out, plain when zoomed in past a threshold.",[14,1359,1360,1363,1364,1167],{},[434,1361,1362],{},"Is the heatmap renderer an alternative?","\nFor density, yes — see ",[21,1365,1367],{"href":1366},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fcreate-heatmap-renderer-pyqgis\u002F","creating a heatmap renderer",[133,1369,1371],{"id":1370},"related","Related",[138,1373,1374,1379,1385,1391,1396],{},[141,1375,1376,1378],{},[21,1377,24],{"href":23}," — the guide this recipe belongs to",[141,1380,1381],{},[21,1382,1384],{"href":1383},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-categorized-renderer-pyqgis\u002F","Create a Categorized Renderer in PyQGIS",[141,1386,1387],{},[21,1388,1390],{"href":1389},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-proportional-symbol-map-pyqgis\u002F","Create a Proportional Symbol Map in PyQGIS",[141,1392,1393],{},[21,1394,1395],{"href":1240},"Cluster Points with DBSCAN and K-Means in PyQGIS",[141,1397,1398],{},[21,1399,1400],{"href":1366},"Create a Heatmap Renderer in PyQGIS",[1402,1403,1404],"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);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":160,"searchDepth":185,"depth":185,"links":1406},[1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419],{"id":135,"depth":185,"text":136},{"id":149,"depth":185,"text":150},{"id":440,"depth":185,"text":441},{"id":692,"depth":185,"text":693},{"id":861,"depth":185,"text":862},{"id":1025,"depth":185,"text":1026},{"id":1170,"depth":185,"text":1171},{"id":1245,"depth":185,"text":1246},{"id":1262,"depth":185,"text":1263},{"id":1290,"depth":185,"text":1291},{"id":1322,"depth":185,"text":1323},{"id":1332,"depth":185,"text":1333},{"id":1370,"depth":185,"text":1371},"Handle overlapping points on the map with QgsPointClusterRenderer and QgsPointDisplacementRenderer — cluster tolerance and units, count labels and sized cluster symbols, displacing coincident points into rings, keeping the underlying categorized style, and when to aggregate instead.","md",{"slug":1423,"type":1424,"breadcrumb":1425,"datePublished":1426,"dateModified":1426},"point-cluster-and-displacement-renderer-pyqgis","article","Point Cluster and Displacement Renderers","2026-10-02","\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fpoint-cluster-and-displacement-renderer-pyqgis",{"title":5,"description":1420},"pyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fpoint-cluster-and-displacement-renderer-pyqgis\u002Findex","XGj30a5E95l5RNHGcn9nJgDO5MY3vIchyk1BipbIvHg",1790966252902]