[{"data":1,"prerenderedAt":1253},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fgeometry-generator-symbol-layer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1242,"extension":1243,"meta":1244,"navigation":282,"path":1249,"seo":1250,"stem":1251,"__hash__":1252},"docs\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fgeometry-generator-symbol-layer-pyqgis\u002Findex.md","Draw with a Geometry Generator in PyQGIS",{"type":7,"value":8,"toc":1228},"minimark",[9,13,17,26,172,177,208,212,219,405,435,438,442,449,540,564,575,667,671,677,702,719,731,735,738,897,917,923,927,935,946,952,956,959,973,977,983,1064,1068,1137,1141,1147,1151,1168,1174,1180,1190,1194,1224],[10,11,5],"h1",{"id":12},"draw-with-a-geometry-generator-in-pyqgis",[14,15,16],"p",{},"A geometry generator is a symbol layer that throws away the feature's geometry and draws the result of an expression instead. The polygon in your data is still a polygon; the symbol layer draws its centroid, or a buffer around it, or a line from it to a related point, or a slightly shifted copy that reads as a shadow. Nothing is written to disk and no second layer appears in the tree.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002F","Symbol Layers & Advanced Symbology in PyQGIS",". It covers building the symbol layer from Python, the expression forms that come up most often, mixing generated geometry with the real geometry in one stack, and the point at which a generator should become a real derived layer instead.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,96,101,108,115,119,124,127,131,135,139,145,149,154,158,164,168],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 300","img","A geometry generator symbol layer replacing the feature geometry with the result of an expression during rendering, while the stored data is unchanged","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What a geometry generator substitutes, and when",[40,41,42],"desc",{},"The stored feature is a polygon. During rendering, the ordinary symbol layer draws that polygon. The geometry generator symbol layer evaluates its expression against the same feature and draws the resulting geometry instead — here a centroid marker and a buffered halo. Nothing about the stored data changes.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","300","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"ggArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The data keeps its geometry; only the drawing changes",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"24","56","200","196","10","#fffdf7","#59645f","stroke-width:2",[68,87,91],{"x":88,"y":89,"style":90,"fill":73,"textAnchor":74},"124","82","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","stored feature",[63,93],{"d":94,"fill":95,"stroke":73,"style":85},"M56 190 L74 122 L166 110 L192 176 L120 206 Z","#e7e2d4",[68,97,100],{"x":88,"y":98,"style":99,"fill":66,"textAnchor":74},"234","text-anchor:middle;font-size:11px;font-family:sans-serif","one polygon on disk",[102,103],"line",{"x1":104,"y1":105,"x2":106,"y2":105,"stroke":66,"style":107},"228","150","262","stroke-width:2;marker-end:url(#ggArrow)",[44,109],{"x":110,"y":79,"width":111,"height":81,"rx":82,"fill":112,"stroke":113,"style":114},"270","216","#eef7f4","#0f766e","stroke-width:2.5",[68,116,118],{"x":117,"y":89,"style":90,"fill":113,"textAnchor":74},"378","expression evaluated",[68,120,123],{"x":117,"y":121,"style":122,"fill":66,"textAnchor":74},"118","text-anchor:middle;font-size:11px;font-family:monospace","centroid($geometry)",[68,125,126],{"x":117,"y":105,"style":99,"fill":84,"textAnchor":74},"and, in a second layer",[68,128,130],{"x":117,"y":129,"style":122,"fill":66,"textAnchor":74},"182","buffer($geometry, 40)",[68,132,134],{"x":117,"y":133,"style":99,"fill":66,"textAnchor":74},"222","once per feature, per repaint",[102,136],{"x1":137,"y1":105,"x2":138,"y2":105,"stroke":66,"style":107},"490","524",[44,140],{"x":141,"y":79,"width":142,"height":81,"rx":82,"fill":143,"stroke":144,"style":114},"532","204","#edf8e9","#15803d",[68,146,148],{"x":147,"y":89,"style":90,"fill":144,"textAnchor":74},"634","what is painted",[63,150],{"d":151,"fill":152,"stroke":144,"style":153},"M556 194 L578 116 L692 102 L720 178 L636 214 Z","none","stroke-width:2;stroke-dasharray:5 4",[63,155],{"d":156,"fill":157,"stroke":73,"style":85},"M566 188 L584 122 L682 110 L708 174 L638 202 Z","#e8efe6",[159,160],"circle",{"cx":147,"cy":161,"r":162,"fill":163},"158","6","#b45309",[68,165,167],{"x":147,"y":166,"style":99,"fill":66,"textAnchor":74},"240","halo + polygon + centroid",[68,169,171],{"x":70,"y":170,"style":99,"fill":84,"textAnchor":74},"288","no derived layer, no file, nothing to keep in sync",[173,174,176],"h2",{"id":175},"prerequisites","Prerequisites",[178,179,180,188,200],"ul",{},[181,182,183,187],"li",{},[184,185,186],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer; geometry generators exist from 3.0 but gained per-part evaluation later.",[181,189,190,191,195,196,199],{},"A layer with a renderer you can reach. The examples use a polygon layer called ",[192,193,194],"code",{},"parcels"," and a point layer called ",[192,197,198],{},"stops",".",[181,201,202,203,207],{},"Familiarity with ",[21,204,206],{"href":205},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","QGIS expressions",", because the generator is an expression with a symbol attached.",[173,209,211],{"id":210},"build-a-generator-symbol-layer","Build a generator symbol layer",[14,213,214,215,218],{},"The class is ",[192,216,217],{},"QgsGeometryGeneratorSymbolLayer",", created from an expression string, and it must be told which geometry type its expression will produce.",[220,221,226],"pre",{"className":222,"code":223,"language":224,"meta":225,"style":225},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsProject, QgsFillSymbol, QgsGeometryGeneratorSymbolLayer,\n    QgsMarkerSymbol, QgsSymbol,\n)\nfrom qgis.PyQt.QtGui import QColor\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nsymbol = layer.renderer().symbol()\n\ngenerator = QgsGeometryGeneratorSymbolLayer.create(\n    {\"geometryModifier\": \"centroid($geometry)\"}\n)\ngenerator.setSymbolType(QgsSymbol.Marker)\ngenerator.subSymbol().setColor(QColor(\"#b45309\"))\ngenerator.subSymbol().setSize(2.6)\n\nsymbol.appendSymbolLayer(generator)\nlayer.triggerRepaint()\n","python","",[192,227,228,246,252,258,264,277,284,309,320,325,336,354,359,365,377,388,393,399],{"__ignoreMap":225},[229,230,232,236,240,243],"span",{"class":102,"line":231},1,[229,233,235],{"class":234},"snl16","from",[229,237,239],{"class":238},"s95oV"," qgis.core ",[229,241,242],{"class":234},"import",[229,244,245],{"class":238}," (\n",[229,247,249],{"class":102,"line":248},2,[229,250,251],{"class":238},"    QgsProject, QgsFillSymbol, QgsGeometryGeneratorSymbolLayer,\n",[229,253,255],{"class":102,"line":254},3,[229,256,257],{"class":238},"    QgsMarkerSymbol, QgsSymbol,\n",[229,259,261],{"class":102,"line":260},4,[229,262,263],{"class":238},")\n",[229,265,267,269,272,274],{"class":102,"line":266},5,[229,268,235],{"class":234},[229,270,271],{"class":238}," qgis.PyQt.QtGui ",[229,273,242],{"class":234},[229,275,276],{"class":238}," QColor\n",[229,278,280],{"class":102,"line":279},6,[229,281,283],{"emptyLinePlaceholder":282},true,"\n",[229,285,287,290,293,296,300,303,306],{"class":102,"line":286},7,[229,288,289],{"class":238},"layer ",[229,291,292],{"class":234},"=",[229,294,295],{"class":238}," QgsProject.instance().mapLayersByName(",[229,297,299],{"class":298},"sU2Wk","\"parcels\"",[229,301,302],{"class":238},")[",[229,304,46],{"class":305},"sDLfK",[229,307,308],{"class":238},"]\n",[229,310,312,315,317],{"class":102,"line":311},8,[229,313,314],{"class":238},"symbol ",[229,316,292],{"class":234},[229,318,319],{"class":238}," layer.renderer().symbol()\n",[229,321,323],{"class":102,"line":322},9,[229,324,283],{"emptyLinePlaceholder":282},[229,326,328,331,333],{"class":102,"line":327},10,[229,329,330],{"class":238},"generator ",[229,332,292],{"class":234},[229,334,335],{"class":238}," QgsGeometryGeneratorSymbolLayer.create(\n",[229,337,339,342,345,348,351],{"class":102,"line":338},11,[229,340,341],{"class":238},"    {",[229,343,344],{"class":298},"\"geometryModifier\"",[229,346,347],{"class":238},": ",[229,349,350],{"class":298},"\"centroid($geometry)\"",[229,352,353],{"class":238},"}\n",[229,355,357],{"class":102,"line":356},12,[229,358,263],{"class":238},[229,360,362],{"class":102,"line":361},13,[229,363,364],{"class":238},"generator.setSymbolType(QgsSymbol.Marker)\n",[229,366,368,371,374],{"class":102,"line":367},14,[229,369,370],{"class":238},"generator.subSymbol().setColor(QColor(",[229,372,373],{"class":298},"\"#b45309\"",[229,375,376],{"class":238},"))\n",[229,378,380,383,386],{"class":102,"line":379},15,[229,381,382],{"class":238},"generator.subSymbol().setSize(",[229,384,385],{"class":305},"2.6",[229,387,263],{"class":238},[229,389,391],{"class":102,"line":390},16,[229,392,283],{"emptyLinePlaceholder":282},[229,394,396],{"class":102,"line":395},17,[229,397,398],{"class":238},"symbol.appendSymbolLayer(generator)\n",[229,400,402],{"class":102,"line":401},18,[229,403,404],{"class":238},"layer.triggerRepaint()\n",[14,406,407,410,411,414,415,418,419,422,423,426,427,430,431,434],{},[184,408,409],{},"Breakdown:"," ",[192,412,413],{},"create()"," takes a property dictionary and the key is ",[192,416,417],{},"geometryModifier"," — not ",[192,420,421],{},"expression",", which is the name people reach for first and which is silently ignored. ",[192,424,425],{},"setSymbolType()"," must match what the expression returns; asking for ",[192,428,429],{},"QgsSymbol.Marker"," and returning a polygon draws nothing at all, with no error. Once the type is set, ",[192,432,433],{},"subSymbol()"," returns a real symbol of that type, so it can be stacked, given data-defined overrides, and treated exactly like any other symbol.",[14,436,437],{},"The generator is appended to the existing symbol, so the parcels still draw normally and the centroid marker appears on top. That composability is the point: one symbol can draw the feature, a halo around it and a marker at its middle, in a defined order.",[173,439,441],{"id":440},"the-expressions-that-come-up","The expressions that come up",[14,443,444,445,448],{},"Four patterns cover most real uses, and all of them take ",[192,446,447],{},"$geometry"," as their input.",[220,450,452],{"className":222,"code":451,"language":224,"meta":225,"style":225},"recipes = {\n    \"centroid marker\":  \"centroid($geometry)\",\n    \"inner halo\":       \"buffer($geometry, -8)\",\n    \"shadow copy\":      \"translate($geometry, 12, -12)\",\n    \"leader line\":      \"make_line($geometry, make_point(\\\"anchor_x\\\", \\\"anchor_y\\\"))\",\n}\n",[192,453,454,464,477,490,503,536],{"__ignoreMap":225},[229,455,456,459,461],{"class":102,"line":231},[229,457,458],{"class":238},"recipes ",[229,460,292],{"class":234},[229,462,463],{"class":238}," {\n",[229,465,466,469,472,474],{"class":102,"line":248},[229,467,468],{"class":298},"    \"centroid marker\"",[229,470,471],{"class":238},":  ",[229,473,350],{"class":298},[229,475,476],{"class":238},",\n",[229,478,479,482,485,488],{"class":102,"line":254},[229,480,481],{"class":298},"    \"inner halo\"",[229,483,484],{"class":238},":       ",[229,486,487],{"class":298},"\"buffer($geometry, -8)\"",[229,489,476],{"class":238},[229,491,492,495,498,501],{"class":102,"line":260},[229,493,494],{"class":298},"    \"shadow copy\"",[229,496,497],{"class":238},":      ",[229,499,500],{"class":298},"\"translate($geometry, 12, -12)\"",[229,502,476],{"class":238},[229,504,505,508,510,513,516,519,521,524,526,529,531,534],{"class":102,"line":266},[229,506,507],{"class":298},"    \"leader line\"",[229,509,497],{"class":238},[229,511,512],{"class":298},"\"make_line($geometry, make_point(",[229,514,515],{"class":305},"\\\"",[229,517,518],{"class":298},"anchor_x",[229,520,515],{"class":305},[229,522,523],{"class":298},", ",[229,525,515],{"class":305},[229,527,528],{"class":298},"anchor_y",[229,530,515],{"class":305},[229,532,533],{"class":298},"))\"",[229,535,476],{"class":238},[229,537,538],{"class":102,"line":279},[229,539,353],{"class":238},[14,541,542,410,544,547,548,551,552,555,556,559,560,563],{},[184,543,409],{},[192,545,546],{},"centroid()"," can fall outside a concave polygon; ",[192,549,550],{},"point_on_surface()"," is the version that guarantees a point inside, and it is nearly always the better default for label anchors. A negative ",[192,553,554],{},"buffer()"," distance shrinks the polygon, giving the inner-line effect that cartographers use for administrative boundaries. ",[192,557,558],{},"translate()"," works in layer units, so the twelve here means twelve metres in a projected CRS and twelve degrees in EPSG:4326 — a mistake with a very memorable result. ",[192,561,562],{},"make_line()"," between the feature and a coordinate pair held in attributes is how leader lines and origin-destination flows are drawn without a line layer existing anywhere.",[14,565,566,567,570,571,574],{},"Two more are worth knowing because they solve problems that look much harder: ",[192,568,569],{},"oriented_bbox($geometry)"," draws the minimum rotated rectangle around a feature, which makes a clean highlight box for irregular shapes, and ",[192,572,573],{},"segments_to_lines($geometry)"," explodes a polygon boundary so a marker line can put a tick on every edge.",[14,576,577],{},[29,578,581,584,587,590,594,599,604,608,611,615,618,621,624,627,630,633,636,640,643,646,650,653,656,659,662,664],{"viewBox":579,"role":32,"ariaLabel":580,"xmlns":34},"0 0 760 296","Four geometry generator expressions and the shapes they draw: a centroid marker, a negative buffer producing an inner line, a translated shadow copy, and a leader line to an attribute-held anchor",[36,582,583],{},"Four generator expressions and their results",[40,585,586],{},"Each panel shows the original polygon in outline and the geometry the expression produces. Centroid yields a point. A negative buffer yields a smaller polygon inside the original. Translate yields an offset copy that reads as a shadow. Make line yields a straight line from the feature to a coordinate stored in its attributes.",[44,588],{"x":46,"y":46,"width":47,"height":589,"fill":49},"296",[68,591,593],{"x":70,"y":592,"style":72,"fill":73,"textAnchor":74},"26","One feature, four different things drawn from it",[44,595],{"x":596,"y":597,"width":598,"height":111,"rx":82,"fill":83,"stroke":163,"style":85},"18","46","172",[68,600,546],{"x":601,"y":602,"style":603,"fill":163,"textAnchor":74},"104","70","text-anchor:middle;font-size:11px;font-weight:bold;font-family:monospace",[63,605],{"d":606,"fill":152,"stroke":84,"style":607},"M44 190 L58 116 L150 108 L168 176 L98 208 Z","stroke-width:1.6;stroke-dasharray:4 3",[159,609],{"cx":601,"cy":610,"r":60,"fill":163},"156",[68,612,614],{"x":601,"y":166,"style":613,"fill":66,"textAnchor":74},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","a point symbol",[44,616],{"x":617,"y":597,"width":598,"height":111,"rx":82,"fill":83,"stroke":113,"style":85},"202",[68,619,620],{"x":170,"y":602,"style":603,"fill":113,"textAnchor":74},"buffer(g, -8)",[63,622],{"d":623,"fill":152,"stroke":84,"style":607},"M228 190 L242 116 L334 108 L352 176 L282 208 Z",[63,625],{"d":626,"fill":112,"stroke":113,"style":85},"M240 184 L252 124 L328 118 L342 172 L284 196 Z",[68,628,629],{"x":170,"y":166,"style":613,"fill":66,"textAnchor":74},"an inner boundary",[44,631],{"x":632,"y":597,"width":598,"height":111,"rx":82,"fill":83,"stroke":84,"style":85},"386",[68,634,558],{"x":635,"y":602,"style":603,"fill":73,"textAnchor":74},"472",[63,637],{"d":638,"fill":639},"M424 196 L438 122 L530 114 L548 182 L478 214 Z","#c9c2ac",[63,641],{"d":642,"fill":83,"stroke":73,"style":85},"M412 188 L426 114 L518 106 L536 174 L466 206 Z",[68,644,645],{"x":635,"y":166,"style":613,"fill":66,"textAnchor":74},"an offset shadow",[44,647],{"x":648,"y":597,"width":598,"height":111,"rx":82,"fill":83,"stroke":649,"style":85},"570","#2563eb",[68,651,562],{"x":652,"y":602,"style":603,"fill":649,"textAnchor":74},"656",[63,654],{"d":655,"fill":152,"stroke":84,"style":607},"M590 196 L602 134 L668 128 L682 186 L630 210 Z",[159,657],{"cx":147,"cy":658,"r":59,"fill":649},"168",[159,660],{"cx":661,"cy":601,"r":59,"fill":649},"716",[102,663],{"x1":147,"y1":658,"x2":661,"y2":601,"stroke":649,"style":85},[68,665,666],{"x":652,"y":166,"style":613,"fill":66,"textAnchor":74},"a leader to an anchor",[173,668,670],{"id":669},"units-and-the-mistake-everybody-makes-once","Units, and the mistake everybody makes once",[14,672,673,676],{},[192,674,675],{},"setUnits()"," on the generator controls how the expression's numeric arguments are interpreted.",[220,678,680],{"className":222,"code":679,"language":224,"meta":225,"style":225},"from qgis.core import QgsUnitTypes\n\ngenerator.setUnits(QgsUnitTypes.RenderMapUnits)\n",[192,681,682,693,697],{"__ignoreMap":225},[229,683,684,686,688,690],{"class":102,"line":231},[229,685,235],{"class":234},[229,687,239],{"class":238},[229,689,242],{"class":234},[229,691,692],{"class":238}," QgsUnitTypes\n",[229,694,695],{"class":102,"line":248},[229,696,283],{"emptyLinePlaceholder":282},[229,698,699],{"class":102,"line":254},[229,700,701],{"class":238},"generator.setUnits(QgsUnitTypes.RenderMapUnits)\n",[14,703,704,706,707,710,711,714,715,718],{},[184,705,409],{}," With ",[192,708,709],{},"RenderMapUnits"," a ",[192,712,713],{},"buffer($geometry, 25)"," is twenty-five map units on the ground, so the halo grows and shrinks with the zoom exactly as a real buffer would. With ",[192,716,717],{},"RenderMillimeters"," the same expression produces a halo that is always 25 mm on paper regardless of scale, which is what a highlight effect wants. Neither is wrong; picking the one that does not match the intent produces either an invisible halo when zoomed out or one that swallows the map when zoomed in.",[14,720,721,722,726,727,730],{},"If the layer is in a geographic CRS, ground units are degrees and a buffer distance of 25 is meaningless. Either work in a projected CRS — see ",[21,723,725],{"href":724},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","handling CRS in PyQGIS"," — or wrap the expression in ",[192,728,729],{},"transform()"," so the buffering happens in metres.",[173,732,734],{"id":733},"a-worked-example-a-scale-aware-highlight","A worked example: a scale-aware highlight",[14,736,737],{},"Generators become genuinely useful when the expression reads the render context rather than only the feature. This one draws a halo whose width is constant on paper, so the highlight stays the same visual weight at every zoom level.",[220,739,741],{"className":222,"code":740,"language":224,"meta":225,"style":225},"from qgis.core import (\n    QgsGeometryGeneratorSymbolLayer, QgsSymbol, QgsUnitTypes,\n    QgsSimpleFillSymbolLayer, QgsFillSymbol,\n)\nfrom qgis.PyQt.QtGui import QColor\nfrom qgis.PyQt.QtCore import Qt\n\nhalo = QgsGeometryGeneratorSymbolLayer.create({\n    \"geometryModifier\": \"buffer($geometry, @map_scale \u002F 25000 * 30)\"\n})\nhalo.setSymbolType(QgsSymbol.Fill)\nhalo.setUnits(QgsUnitTypes.RenderMapUnits)\n\nring = QgsSimpleFillSymbolLayer(QColor(\"#0f766e\"))\nring.setBrushStyle(Qt.NoBrush)\nring.setStrokeColor(QColor(\"#0f766e\"))\nring.setStrokeWidth(0.8)\nhalo.setSubSymbol(QgsFillSymbol([ring]))\n\nsymbol.insertSymbolLayer(0, halo)\nlayer.triggerRepaint()\n",[192,742,743,753,758,763,767,777,789,793,803,813,818,823,828,832,847,852,861,871,876,881,892],{"__ignoreMap":225},[229,744,745,747,749,751],{"class":102,"line":231},[229,746,235],{"class":234},[229,748,239],{"class":238},[229,750,242],{"class":234},[229,752,245],{"class":238},[229,754,755],{"class":102,"line":248},[229,756,757],{"class":238},"    QgsGeometryGeneratorSymbolLayer, QgsSymbol, QgsUnitTypes,\n",[229,759,760],{"class":102,"line":254},[229,761,762],{"class":238},"    QgsSimpleFillSymbolLayer, QgsFillSymbol,\n",[229,764,765],{"class":102,"line":260},[229,766,263],{"class":238},[229,768,769,771,773,775],{"class":102,"line":266},[229,770,235],{"class":234},[229,772,271],{"class":238},[229,774,242],{"class":234},[229,776,276],{"class":238},[229,778,779,781,784,786],{"class":102,"line":279},[229,780,235],{"class":234},[229,782,783],{"class":238}," qgis.PyQt.QtCore ",[229,785,242],{"class":234},[229,787,788],{"class":238}," Qt\n",[229,790,791],{"class":102,"line":286},[229,792,283],{"emptyLinePlaceholder":282},[229,794,795,798,800],{"class":102,"line":311},[229,796,797],{"class":238},"halo ",[229,799,292],{"class":234},[229,801,802],{"class":238}," QgsGeometryGeneratorSymbolLayer.create({\n",[229,804,805,808,810],{"class":102,"line":322},[229,806,807],{"class":298},"    \"geometryModifier\"",[229,809,347],{"class":238},[229,811,812],{"class":298},"\"buffer($geometry, @map_scale \u002F 25000 * 30)\"\n",[229,814,815],{"class":102,"line":327},[229,816,817],{"class":238},"})\n",[229,819,820],{"class":102,"line":338},[229,821,822],{"class":238},"halo.setSymbolType(QgsSymbol.Fill)\n",[229,824,825],{"class":102,"line":356},[229,826,827],{"class":238},"halo.setUnits(QgsUnitTypes.RenderMapUnits)\n",[229,829,830],{"class":102,"line":361},[229,831,283],{"emptyLinePlaceholder":282},[229,833,834,837,839,842,845],{"class":102,"line":367},[229,835,836],{"class":238},"ring ",[229,838,292],{"class":234},[229,840,841],{"class":238}," QgsSimpleFillSymbolLayer(QColor(",[229,843,844],{"class":298},"\"#0f766e\"",[229,846,376],{"class":238},[229,848,849],{"class":102,"line":379},[229,850,851],{"class":238},"ring.setBrushStyle(Qt.NoBrush)\n",[229,853,854,857,859],{"class":102,"line":390},[229,855,856],{"class":238},"ring.setStrokeColor(QColor(",[229,858,844],{"class":298},[229,860,376],{"class":238},[229,862,863,866,869],{"class":102,"line":395},[229,864,865],{"class":238},"ring.setStrokeWidth(",[229,867,868],{"class":305},"0.8",[229,870,263],{"class":238},[229,872,873],{"class":102,"line":401},[229,874,875],{"class":238},"halo.setSubSymbol(QgsFillSymbol([ring]))\n",[229,877,879],{"class":102,"line":878},19,[229,880,283],{"emptyLinePlaceholder":282},[229,882,884,887,889],{"class":102,"line":883},20,[229,885,886],{"class":238},"symbol.insertSymbolLayer(",[229,888,46],{"class":305},[229,890,891],{"class":238},", halo)\n",[229,893,895],{"class":102,"line":894},21,[229,896,404],{"class":238},[14,898,899,410,901,904,905,908,909,912,913,916],{},[184,900,409],{},[192,902,903],{},"@map_scale"," is the denominator of the current scale, so dividing by a reference scale and multiplying by a ground distance produces a buffer that always covers the same number of millimetres on the output. Inserting at index 0 puts the halo underneath the parcel itself, which is what makes it read as a glow rather than a second outline drawn over the top. ",[192,906,907],{},"setSubSymbol()"," takes a whole ",[192,910,911],{},"QgsFillSymbol",", and giving that symbol a ",[192,914,915],{},"Qt.NoBrush"," layer is what turns a filled buffer into a ring.",[14,918,919,920,922],{},"The same idea drives a lot of finished cartography: a generator that widens a river casing as the map zooms out, a generator that draws a bounding box only below a certain scale, a generator that fades a boundary by feeding ",[192,921,903],{}," into a data-defined opacity on the sub-symbol.",[173,924,926],{"id":925},"mixing-generated-and-real-geometry-in-one-stack","Mixing generated and real geometry in one stack",[14,928,929,930,934],{},"Because a generator is a symbol layer, ordering rules from ",[21,931,933],{"href":932},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fstack-symbol-layers-pyqgis\u002F","stacking symbol layers"," apply unchanged, and the useful arrangements are predictable.",[14,936,937,938,941,942,945],{},"Put a generator ",[184,939,940],{},"below"," the real geometry when it is a background effect — a shadow, a glow, an extent box. Put it ",[184,943,944],{},"above"," when it is an annotation — a centroid marker, a tick on each edge, a leader line to a label anchor. Put two generators around the real geometry when you want both, which is the standard treatment for \"selected parcel\": a soft outer halo underneath, the parcel drawn normally, and a centroid dot on top.",[14,947,948,949,951],{},"One practical warning about debugging these stacks: because a mismatched symbol type draws nothing silently, add layers one at a time and repaint between each. When a stack of four suddenly shows three, the newest layer's ",[192,950,425],{}," is almost always the culprit rather than the expression.",[173,953,955],{"id":954},"when-to-stop-and-make-a-real-layer","When to stop and make a real layer",[14,957,958],{},"A generator runs its expression once per feature on every repaint, every pan and every zoom. For a few thousand features doing centroid arithmetic, that is free. For a hundred thousand features doing buffers, it is not — buffering is genuine geometry work and it is not cached between frames.",[14,960,961,962,965,966,969,970,972],{},"The signal to switch is straightforward: if the generated geometry does not depend on the map scale, and the layer is large, compute it once with ",[192,963,964],{},"native:centroids"," or ",[192,967,968],{},"native:buffer"," and style the result as an ordinary layer. Generators earn their keep when the geometry must stay in step with edits to the source, when it depends on ",[192,971,903],{},", or when creating and maintaining a derived layer would cost more than it saves.",[173,974,976],{"id":975},"qgis-version-compatibility","QGIS version compatibility",[14,978,979,980,982],{},"The examples target ",[184,981,186],{}," (Python 3.12).",[984,985,986,1002],"table",{},[987,988,989],"thead",{},[990,991,992,996,999],"tr",{},[993,994,995],"th",{},"QGIS version",[993,997,998],{},"Python",[993,1000,1001],{},"Notes",[1003,1004,1005,1022,1033,1043,1054],"tbody",{},[990,1006,1007,1011,1014],{},[1008,1009,1010],"td",{},"3.16 LTR",[1008,1012,1013],{},"3.7",[1008,1015,1016,1018,1019,1021],{},[192,1017,217],{}," available; ",[192,1020,417],{}," property key already in use.",[990,1023,1024,1027,1030],{},[1008,1025,1026],{},"3.22 LTR",[1008,1028,1029],{},"3.9",[1008,1031,1032],{},"Generators usable inside marker-line and pattern-fill sub-symbols.",[990,1034,1035,1038,1040],{},[1008,1036,1037],{},"3.28 LTR",[1008,1039,1029],{},[1008,1041,1042],{},"Per-part evaluation improves multipart handling.",[990,1044,1045,1048,1051],{},[1008,1046,1047],{},"3.34 LTR",[1008,1049,1050],{},"3.12",[1008,1052,1053],{},"Baseline for this page.",[990,1055,1056,1059,1061],{},[1008,1057,1058],{},"3.40+",[1008,1060,1050],{},[1008,1062,1063],{},"Generators supported in labelling callouts and legend patch shapes.",[173,1065,1067],{"id":1066},"troubleshooting","Troubleshooting",[178,1069,1070,1088,1103,1112,1122,1128],{},[181,1071,1072,1075,1076,1078,1079,1081,1082,1084,1085,199],{},[184,1073,1074],{},"Nothing is drawn."," The symbol type does not match what the expression returns. A ",[192,1077,546],{}," expression needs ",[192,1080,429],{},", a ",[192,1083,554],{}," needs ",[192,1086,1087],{},"QgsSymbol.Fill",[181,1089,1090,1093,1094,1096,1097,1099,1100,1102],{},[184,1091,1092],{},"The property is ignored."," The dictionary key is ",[192,1095,417],{},". ",[192,1098,421],{}," is accepted by ",[192,1101,413],{}," without complaint and does nothing.",[181,1104,1105,1108,1109,1111],{},[184,1106,1107],{},"The halo is enormous or invisible."," Units mismatch. Check ",[192,1110,675],{}," and confirm the layer is not in a geographic CRS if the distance is meant to be metres.",[181,1113,1114,1117,1118,1121],{},[184,1115,1116],{},"The centroid sits outside the polygon."," That is correct behaviour for a concave shape. Use ",[192,1119,1120],{},"point_on_surface($geometry)"," instead.",[181,1123,1124,1127],{},[184,1125,1126],{},"Panning became slow."," The expression is doing geometry work per feature per frame. Precompute it into a real layer or restrict the layer with a scale range.",[181,1129,1130,1133,1134,199],{},[184,1131,1132],{},"A multipart feature draws only one shape."," The expression collapses parts. Wrap it so it operates on each part, or explode the layer with ",[192,1135,1136],{},"native:multiparttosingleparts",[173,1138,1140],{"id":1139},"conclusion","Conclusion",[14,1142,1143,1144,1146],{},"A geometry generator is one property — ",[192,1145,417],{}," — plus a symbol type that matches the expression's output. Set the units to match what the distance is supposed to mean, compose the generator with the ordinary symbol layers rather than replacing them, and move the work into a real derived layer once the feature count makes per-frame evaluation expensive.",[173,1148,1150],{"id":1149},"frequently-asked-questions","Frequently Asked Questions",[14,1152,1153,1156,1157,523,1160,1163,1164,1167],{},[184,1154,1155],{},"Can a geometry generator reference another layer?","\nYes, through expression functions such as ",[192,1158,1159],{},"overlay_intersects()",[192,1161,1162],{},"aggregate()"," and ",[192,1165,1166],{},"get_feature()",". It is powerful and slow: each call is a lookup against another layer per feature per repaint, so build a spatial index on the target layer and expect to precompute if the layer is large.",[14,1169,1170,1173],{},[184,1171,1172],{},"Can I put a generator inside a marker line?","\nYes. A marker line's sub-symbol is an ordinary marker symbol, and a marker symbol may contain a generator layer. This nesting is how per-vertex derived shapes are drawn.",[14,1175,1176,1179],{},[184,1177,1178],{},"Does a generator affect labelling or selection?","\nNo. Labels are placed from the real geometry, and clicking selects on the real geometry. A generator only changes what is painted, which occasionally surprises users who click a halo and select nothing.",[14,1181,1182,1185,1186,199],{},[184,1183,1184],{},"Will a generator survive export to PDF?","\nYes — the generated geometry is drawn as vector output like anything else. What does not survive is a generator combined with a blend mode that forces rasterisation; see ",[21,1187,1189],{"href":1188},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fset-layer-opacity-and-blend-mode-pyqgis\u002F","opacity and blend modes",[173,1191,1193],{"id":1192},"related","Related",[178,1195,1196,1201,1206,1212,1218],{},[181,1197,1198,1200],{},[21,1199,24],{"href":23}," — the guide this recipe belongs to",[181,1202,1203],{},[21,1204,1205],{"href":932},"Stack Symbol Layers in PyQGIS",[181,1207,1208],{},[21,1209,1211],{"href":1210},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","Evaluate a QGIS Expression in PyQGIS",[181,1213,1214],{},[21,1215,1217],{"href":1216},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuffer-geometry-pyqgis\u002F","Buffer a Geometry in PyQGIS",[181,1219,1220],{},[21,1221,1223],{"href":1222},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcontrol-label-placement-and-collisions-pyqgis\u002F","Create Callouts and Leader Lines for Labels in PyQGIS",[1225,1226,1227],"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":225,"searchDepth":248,"depth":248,"links":1229},[1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241],{"id":175,"depth":248,"text":176},{"id":210,"depth":248,"text":211},{"id":440,"depth":248,"text":441},{"id":669,"depth":248,"text":670},{"id":733,"depth":248,"text":734},{"id":925,"depth":248,"text":926},{"id":954,"depth":248,"text":955},{"id":975,"depth":248,"text":976},{"id":1066,"depth":248,"text":1067},{"id":1139,"depth":248,"text":1140},{"id":1149,"depth":248,"text":1150},{"id":1192,"depth":248,"text":1193},"Use QgsGeometryGeneratorSymbolLayer to draw centroids, buffers, halos, leader lines and arrowheads from an expression, without creating a derived layer — and know when it costs too much.","md",{"slug":1245,"type":1246,"breadcrumb":1247,"datePublished":1248,"dateModified":1248},"geometry-generator-symbol-layer-pyqgis","article","Geometry Generator","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fgeometry-generator-symbol-layer-pyqgis",{"title":5,"description":1242},"pyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fgeometry-generator-symbol-layer-pyqgis\u002Findex","5sixorsDgyzevDQwsltvaqwgJFHotUHV80JocBtXonM",1787823360561]