[{"data":1,"prerenderedAt":1422},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-legend-to-layout-pyqgis":3},{"id":4,"title":5,"body":6,"description":1411,"extension":1412,"meta":1413,"navigation":256,"path":1418,"seo":1419,"stem":1420,"__hash__":1421},"docs\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-legend-to-layout-pyqgis\u002Findex.md","Add a Legend to a Print Layout in PyQGIS",{"type":7,"value":8,"toc":1397},"minimark",[9,13,17,26,189,194,219,223,392,418,422,460,476,480,626,651,742,746,877,890,894,897,933,949,953,956,1065,1077,1085,1169,1173,1179,1251,1255,1304,1308,1311,1315,1325,1335,1345,1358,1364,1368,1393],[10,11,5],"h1",{"id":12},"add-a-legend-to-a-print-layout-in-pyqgis",[14,15,16],"p",{},"A legend is the part of an automated map that most often looks automated. Left to its defaults it lists every layer in the project, including the basemap and the three scratch layers nobody meant to publish, in the order the layer tree happens to hold them, with the file names as titles. Ten lines of PyQGIS turn that into a curated legend that says exactly what the map shows.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","Automated Map Layout Generation with PyQGIS",". It covers adding the legend item, linking it to a map, filtering it to what is actually drawn, renaming and removing entries, and keeping it consistent while an atlas iterates.",[14,27,28],{},[29,30,35,39,43,50,67,76,84,90,99,105,111,117,124,130,134,138,142,146,153,160,165,170,174,178,182,185],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 276","img","A layout page containing a map item and a legend item, with the legend reading its content from the map's visible layers rather than from the project","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What the legend is reading from",[40,41,42],"desc",{},"A layout page holds a map item and a legend item. The legend is linked to the map, so filtering by map content restricts it to layers visible in that map's extent and scale. Without the link the legend lists every layer in the project layer tree, including layers the map does not show.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","276","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"legArrow","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","Link the legend to the map, or it lists the whole project",[44,77],{"x":78,"y":79,"width":70,"height":80,"rx":58,"fill":81,"stroke":82,"style":83},"20","52","200","#fffdf7","#59645f","stroke-width:2",[68,85,89],{"x":86,"y":87,"style":88,"fill":73,"textAnchor":74},"210","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","the layout page",[44,91],{"x":92,"y":93,"width":94,"height":95,"rx":96,"fill":97,"stroke":98,"style":83},"40","90","212","144","6","#eef7f4","#0f766e",[68,100,104],{"x":101,"y":102,"style":103,"fill":98,"textAnchor":74},"146","166","text-anchor:middle;font-size:12px;font-family:sans-serif","map item",[44,106],{"x":107,"y":93,"width":108,"height":95,"rx":96,"fill":109,"stroke":110,"style":83},"264","120","#eff3ff","#2563eb",[68,112,116],{"x":113,"y":114,"style":115,"fill":110,"textAnchor":74},"324","116","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","legend",[44,118],{"x":48,"y":119,"width":120,"height":121,"fill":110,"fillOpacity":122,"stroke":110,"style":123},"128","14","10",0.5,"stroke-width:1",[68,125,129],{"x":126,"y":127,"style":128,"fill":66},"300","137","font-size:10px;font-family:sans-serif","Wards",[44,131],{"x":48,"y":132,"width":120,"height":121,"fill":133,"fillOpacity":122,"stroke":133,"style":123},"150","#15803d",[68,135,137],{"x":126,"y":136,"style":128,"fill":66},"159","Parks",[44,139],{"x":48,"y":140,"width":120,"height":121,"fill":141,"fillOpacity":122,"stroke":141,"style":123},"172","#b45309",[68,143,145],{"x":126,"y":144,"style":128,"fill":66},"181","Roads",[147,148],"line",{"x1":149,"y1":150,"x2":151,"y2":150,"stroke":66,"style":152},"252","162","260","stroke-width:2;marker-end:url(#legArrow)",[44,154],{"x":155,"y":79,"width":156,"height":157,"rx":58,"fill":158,"stroke":133,"style":159},"432","312","92","#edf8e9","stroke-width:2.5",[68,161,164],{"x":162,"y":163,"style":88,"fill":133,"textAnchor":74},"588","78","linked + filtered by map",[68,166,169],{"x":162,"y":167,"style":168,"fill":66,"textAnchor":74},"102","text-anchor:middle;font-size:11px;font-family:sans-serif","only layers this map draws",[68,171,173],{"x":162,"y":172,"style":168,"fill":66,"textAnchor":74},"124","scale-hidden rules omitted too",[44,175],{"x":155,"y":176,"width":156,"height":157,"rx":58,"fill":177,"stroke":141,"style":83},"160","#fdf2e2",[68,179,181],{"x":162,"y":180,"style":88,"fill":141,"textAnchor":74},"186","unlinked",[68,183,184],{"x":162,"y":86,"style":168,"fill":66,"textAnchor":74},"every layer in the project",[68,186,188],{"x":162,"y":187,"style":168,"fill":66,"textAnchor":74},"232","including scratch and basemap",[190,191,193],"h2",{"id":192},"prerequisites","Prerequisites",[195,196,197,205,216],"ul",{},[198,199,200,204],"li",{},[201,202,203],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[198,206,207,208,210,211,215],{},"A layout with a map item — see ",[21,209,24],{"href":23}," for creating one, or ",[21,212,214],{"href":213},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","Exporting Multiple QGIS Layouts to PDF"," for the export side.",[198,217,218],{},"Layers already styled, because the legend renders whatever symbology they currently have.",[190,220,222],{"id":221},"add-and-place-the-legend","Add and place the legend",[224,225,230],"pre",{"className":226,"code":227,"language":228,"meta":229,"style":229},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsLayoutItemLegend, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes\n\nproject = QgsProject.instance()\nlayout = project.layoutManager().layoutByName(\"Ward map\")\nmap_item = layout.itemById(\"main_map\")\n\nlegend = QgsLayoutItemLegend(layout)\nlegend.setId(\"main_legend\")\nlegend.setTitle(\"Legend\")\nlegend.setLinkedMap(map_item)\n\nlayout.addLayoutItem(legend)\nlegend.attemptMove(QgsLayoutPoint(210, 20, QgsUnitTypes.LayoutMillimeters))\nlegend.attemptResize(QgsLayoutSize(70, 100, QgsUnitTypes.LayoutMillimeters))\n","python","",[231,232,233,251,258,270,288,304,309,320,331,342,348,353,359,376],"code",{"__ignoreMap":229},[234,235,237,241,245,248],"span",{"class":147,"line":236},1,[234,238,240],{"class":239},"snl16","from",[234,242,244],{"class":243},"s95oV"," qgis.core ",[234,246,247],{"class":239},"import",[234,249,250],{"class":243}," QgsProject, QgsLayoutItemLegend, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes\n",[234,252,254],{"class":147,"line":253},2,[234,255,257],{"emptyLinePlaceholder":256},true,"\n",[234,259,261,264,267],{"class":147,"line":260},3,[234,262,263],{"class":243},"project ",[234,265,266],{"class":239},"=",[234,268,269],{"class":243}," QgsProject.instance()\n",[234,271,273,276,278,281,285],{"class":147,"line":272},4,[234,274,275],{"class":243},"layout ",[234,277,266],{"class":239},[234,279,280],{"class":243}," project.layoutManager().layoutByName(",[234,282,284],{"class":283},"sU2Wk","\"Ward map\"",[234,286,287],{"class":243},")\n",[234,289,291,294,296,299,302],{"class":147,"line":290},5,[234,292,293],{"class":243},"map_item ",[234,295,266],{"class":239},[234,297,298],{"class":243}," layout.itemById(",[234,300,301],{"class":283},"\"main_map\"",[234,303,287],{"class":243},[234,305,307],{"class":147,"line":306},6,[234,308,257],{"emptyLinePlaceholder":256},[234,310,312,315,317],{"class":147,"line":311},7,[234,313,314],{"class":243},"legend ",[234,316,266],{"class":239},[234,318,319],{"class":243}," QgsLayoutItemLegend(layout)\n",[234,321,323,326,329],{"class":147,"line":322},8,[234,324,325],{"class":243},"legend.setId(",[234,327,328],{"class":283},"\"main_legend\"",[234,330,287],{"class":243},[234,332,334,337,340],{"class":147,"line":333},9,[234,335,336],{"class":243},"legend.setTitle(",[234,338,339],{"class":283},"\"Legend\"",[234,341,287],{"class":243},[234,343,345],{"class":147,"line":344},10,[234,346,347],{"class":243},"legend.setLinkedMap(map_item)\n",[234,349,351],{"class":147,"line":350},11,[234,352,257],{"emptyLinePlaceholder":256},[234,354,356],{"class":147,"line":355},12,[234,357,358],{"class":243},"layout.addLayoutItem(legend)\n",[234,360,362,365,368,371,373],{"class":147,"line":361},13,[234,363,364],{"class":243},"legend.attemptMove(QgsLayoutPoint(",[234,366,86],{"class":367},"sDLfK",[234,369,370],{"class":243},", ",[234,372,78],{"class":367},[234,374,375],{"class":243},", QgsUnitTypes.LayoutMillimeters))\n",[234,377,379,382,385,387,390],{"class":147,"line":378},14,[234,380,381],{"class":243},"legend.attemptResize(QgsLayoutSize(",[234,383,384],{"class":367},"70",[234,386,370],{"class":243},[234,388,389],{"class":367},"100",[234,391,375],{"class":243},[14,393,394,397,398,401,402,405,406,409,410,413,414,417],{},[201,395,396],{},"Breakdown:"," ",[231,399,400],{},"setLinkedMap()"," is the call that matters — it ties the legend to a specific map item so that filtering, scale-dependent rules and any atlas-driven layer visibility are taken from that map rather than the project. ",[231,403,404],{},"addLayoutItem()"," must come before positioning, because an item not yet in the layout has no coordinate space to move within. ",[231,407,408],{},"attemptMove()"," and ",[231,411,412],{},"attemptResize()"," are named for a reason: a locked or referenced item may end up somewhere slightly different, and the return is silent. Setting an id makes the item findable later with ",[231,415,416],{},"itemById()",", which is how a script re-runs against an existing layout without creating duplicates.",[190,419,421],{"id":420},"show-only-what-the-map-draws","Show only what the map draws",[224,423,425],{"className":226,"code":424,"language":228,"meta":229,"style":229},"legend.setLegendFilterByMapEnabled(True)\nlegend.setLegendFilterOutAtlas(True)\nlegend.setAutoUpdateModel(True)\nlegend.updateLegend()\n",[231,426,427,437,446,455],{"__ignoreMap":229},[234,428,429,432,435],{"class":147,"line":236},[234,430,431],{"class":243},"legend.setLegendFilterByMapEnabled(",[234,433,434],{"class":367},"True",[234,436,287],{"class":243},[234,438,439,442,444],{"class":147,"line":253},[234,440,441],{"class":243},"legend.setLegendFilterOutAtlas(",[234,443,434],{"class":367},[234,445,287],{"class":243},[234,447,448,451,453],{"class":147,"line":260},[234,449,450],{"class":243},"legend.setAutoUpdateModel(",[234,452,434],{"class":367},[234,454,287],{"class":243},[234,456,457],{"class":147,"line":272},[234,458,459],{"class":243},"legend.updateLegend()\n",[14,461,462,397,464,467,468,471,472,475],{},[201,463,396],{},[231,465,466],{},"setLegendFilterByMapEnabled(True)"," restricts entries to layers with features inside the linked map's extent — a ward map of an area with no parks stops advertising a Parks symbol nobody can see. ",[231,469,470],{},"setLegendFilterOutAtlas(True)"," extends that to the current atlas feature, so a per-ward map's legend reflects that ward. ",[231,473,474],{},"setAutoUpdateModel(True)"," keeps the legend synchronised with the project layer tree, which is what you want until the moment you start curating entries by hand — at which point it must be turned off, because an auto-updating model discards manual edits on the next refresh.",[190,477,479],{"id":478},"curate-the-entries","Curate the entries",[224,481,483],{"className":226,"code":482,"language":228,"meta":229,"style":229},"legend.setAutoUpdateModel(False)\n\nmodel = legend.model()\nroot = model.rootGroup()\n\nfor layer_node in list(root.findLayers()):\n    layer = layer_node.layer()\n    if layer is None or layer.name().startswith(\"scratch_\"):\n        root.removeChildNode(layer_node)\n        continue\n    if layer.name() == \"wards_2026\":\n        layer_node.setName(\"Ward boundaries\")\n\nlegend.adjustBoxSize()\nlegend.refresh()\n",[231,484,485,494,498,508,518,522,539,549,575,580,585,601,611,615,620],{"__ignoreMap":229},[234,486,487,489,492],{"class":147,"line":236},[234,488,450],{"class":243},[234,490,491],{"class":367},"False",[234,493,287],{"class":243},[234,495,496],{"class":147,"line":253},[234,497,257],{"emptyLinePlaceholder":256},[234,499,500,503,505],{"class":147,"line":260},[234,501,502],{"class":243},"model ",[234,504,266],{"class":239},[234,506,507],{"class":243}," legend.model()\n",[234,509,510,513,515],{"class":147,"line":272},[234,511,512],{"class":243},"root ",[234,514,266],{"class":239},[234,516,517],{"class":243}," model.rootGroup()\n",[234,519,520],{"class":147,"line":290},[234,521,257],{"emptyLinePlaceholder":256},[234,523,524,527,530,533,536],{"class":147,"line":306},[234,525,526],{"class":239},"for",[234,528,529],{"class":243}," layer_node ",[234,531,532],{"class":239},"in",[234,534,535],{"class":367}," list",[234,537,538],{"class":243},"(root.findLayers()):\n",[234,540,541,544,546],{"class":147,"line":311},[234,542,543],{"class":243},"    layer ",[234,545,266],{"class":239},[234,547,548],{"class":243}," layer_node.layer()\n",[234,550,551,554,557,560,563,566,569,572],{"class":147,"line":322},[234,552,553],{"class":239},"    if",[234,555,556],{"class":243}," layer ",[234,558,559],{"class":239},"is",[234,561,562],{"class":367}," None",[234,564,565],{"class":239}," or",[234,567,568],{"class":243}," layer.name().startswith(",[234,570,571],{"class":283},"\"scratch_\"",[234,573,574],{"class":243},"):\n",[234,576,577],{"class":147,"line":333},[234,578,579],{"class":243},"        root.removeChildNode(layer_node)\n",[234,581,582],{"class":147,"line":344},[234,583,584],{"class":239},"        continue\n",[234,586,587,589,592,595,598],{"class":147,"line":350},[234,588,553],{"class":239},[234,590,591],{"class":243}," layer.name() ",[234,593,594],{"class":239},"==",[234,596,597],{"class":283}," \"wards_2026\"",[234,599,600],{"class":243},":\n",[234,602,603,606,609],{"class":147,"line":355},[234,604,605],{"class":243},"        layer_node.setName(",[234,607,608],{"class":283},"\"Ward boundaries\"",[234,610,287],{"class":243},[234,612,613],{"class":147,"line":361},[234,614,257],{"emptyLinePlaceholder":256},[234,616,617],{"class":147,"line":378},[234,618,619],{"class":243},"legend.adjustBoxSize()\n",[234,621,623],{"class":147,"line":622},15,[234,624,625],{"class":243},"legend.refresh()\n",[14,627,628,630,631,634,635,638,639,642,643,646,647,650],{},[201,629,396],{}," Turning off the auto-update model is mandatory before editing, otherwise every change is reverted on the next refresh. ",[231,632,633],{},"findLayers()"," returns the layer nodes in the legend's own tree — a copy of the project's layer tree, so removing a node removes the legend entry and leaves the project untouched. Wrapping it in ",[231,636,637],{},"list()"," matters because removing nodes while iterating the live collection skips entries. ",[231,640,641],{},"setName()"," on the node renames the legend entry only, which is how a legend reads \"Ward boundaries\" while the layer stays ",[231,644,645],{},"wards_2026"," for every script that looks it up by name. ",[231,648,649],{},"adjustBoxSize()"," shrinks the frame to the content, so the box does not float over the map with empty space below the last entry.",[14,652,653],{},[29,654,657,660,663,666,669,673,678,681,685,687,691,694,698,701,705,708,712,716,719,723,726,730,732,735,737,739],{"viewBox":655,"role":32,"ariaLabel":656,"xmlns":34},"0 0 760 268","A default legend listing raw layer names including scratch layers, next to a curated legend with friendly names, scratch entries removed and the box shrunk to fit",[36,658,659],{},"Default legend against a curated one",[40,661,662],{},"The default legend lists five entries using raw layer names including two scratch layers and a basemap, inside an oversized frame. The curated legend shows three entries with readable titles, no scratch layers, and a frame shrunk to the content.",[44,664],{"x":46,"y":46,"width":47,"height":665,"fill":49},"268",[68,667,668],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"The same project, before and after ten lines of curation",[44,670],{"x":92,"y":671,"width":126,"height":672,"rx":58,"fill":177,"stroke":141,"style":159},"48","196",[68,674,677],{"x":675,"y":676,"style":88,"fill":141,"textAnchor":74},"190","74","default",[44,679],{"x":680,"y":157,"width":120,"height":121,"fill":110,"fillOpacity":122,"stroke":110,"style":123},"64",[68,682,645],{"x":93,"y":683,"style":684,"fill":66},"101","font-size:11px;font-family:sans-serif",[44,686],{"x":680,"y":114,"width":120,"height":121,"fill":133,"fillOpacity":122,"stroke":133,"style":123},[68,688,690],{"x":93,"y":689,"style":684,"fill":66},"125","parks_final_v3",[44,692],{"x":680,"y":693,"width":120,"height":121,"fill":82,"fillOpacity":122,"stroke":82,"style":123},"140",[68,695,697],{"x":93,"y":696,"style":684,"fill":82},"149","scratch_buffer",[44,699],{"x":680,"y":700,"width":120,"height":121,"fill":82,"fillOpacity":122,"stroke":82,"style":123},"164",[68,702,704],{"x":93,"y":703,"style":684,"fill":82},"173","scratch_clip2",[44,706],{"x":680,"y":707,"width":120,"height":121,"fill":141,"fillOpacity":122,"stroke":141,"style":123},"188",[68,709,711],{"x":93,"y":710,"style":684,"fill":66},"197","OSM Standard",[68,713,715],{"x":675,"y":714,"style":168,"fill":141,"textAnchor":74},"228","oversized frame, raw names",[44,717],{"x":718,"y":671,"width":126,"height":693,"rx":58,"fill":158,"stroke":133,"style":159},"420",[68,720,722],{"x":721,"y":676,"style":88,"fill":133,"textAnchor":74},"570","curated",[44,724],{"x":725,"y":157,"width":120,"height":121,"fill":110,"fillOpacity":122,"stroke":110,"style":123},"444",[68,727,729],{"x":728,"y":683,"style":684,"fill":66},"470","Ward boundaries",[44,731],{"x":725,"y":114,"width":120,"height":121,"fill":133,"fillOpacity":122,"stroke":133,"style":123},[68,733,734],{"x":728,"y":689,"style":684,"fill":66},"Public parks",[44,736],{"x":725,"y":693,"width":120,"height":121,"fill":141,"fillOpacity":122,"stroke":141,"style":123},[68,738,145],{"x":728,"y":696,"style":684,"fill":66},[68,740,741],{"x":721,"y":94,"style":168,"fill":133,"textAnchor":74},"box shrunk to content, names readable",[190,743,745],{"id":744},"control-the-typography-and-columns","Control the typography and columns",[224,747,749],{"className":226,"code":748,"language":228,"meta":229,"style":229},"from qgis.core import QgsLegendStyle\nfrom qgis.PyQt.QtGui import QFont\n\nlegend.setStyleFont(QgsLegendStyle.Title, QFont(\"Noto Sans\", 11, QFont.Bold))\nlegend.setStyleFont(QgsLegendStyle.Subgroup, QFont(\"Noto Sans\", 9, QFont.Bold))\nlegend.setStyleFont(QgsLegendStyle.SymbolLabel, QFont(\"Noto Sans\", 8))\n\nlegend.setColumnCount(2)\nlegend.setSplitLayer(True)\nlegend.setEqualColumnWidth(True)\nlegend.setSymbolWidth(6)\nlegend.setSymbolHeight(3)\nlegend.adjustBoxSize()\n",[231,750,751,762,774,778,794,808,822,826,836,845,854,863,873],{"__ignoreMap":229},[234,752,753,755,757,759],{"class":147,"line":236},[234,754,240],{"class":239},[234,756,244],{"class":243},[234,758,247],{"class":239},[234,760,761],{"class":243}," QgsLegendStyle\n",[234,763,764,766,769,771],{"class":147,"line":253},[234,765,240],{"class":239},[234,767,768],{"class":243}," qgis.PyQt.QtGui ",[234,770,247],{"class":239},[234,772,773],{"class":243}," QFont\n",[234,775,776],{"class":147,"line":260},[234,777,257],{"emptyLinePlaceholder":256},[234,779,780,783,786,788,791],{"class":147,"line":272},[234,781,782],{"class":243},"legend.setStyleFont(QgsLegendStyle.Title, QFont(",[234,784,785],{"class":283},"\"Noto Sans\"",[234,787,370],{"class":243},[234,789,790],{"class":367},"11",[234,792,793],{"class":243},", QFont.Bold))\n",[234,795,796,799,801,803,806],{"class":147,"line":290},[234,797,798],{"class":243},"legend.setStyleFont(QgsLegendStyle.Subgroup, QFont(",[234,800,785],{"class":283},[234,802,370],{"class":243},[234,804,805],{"class":367},"9",[234,807,793],{"class":243},[234,809,810,813,815,817,819],{"class":147,"line":306},[234,811,812],{"class":243},"legend.setStyleFont(QgsLegendStyle.SymbolLabel, QFont(",[234,814,785],{"class":283},[234,816,370],{"class":243},[234,818,58],{"class":367},[234,820,821],{"class":243},"))\n",[234,823,824],{"class":147,"line":311},[234,825,257],{"emptyLinePlaceholder":256},[234,827,828,831,834],{"class":147,"line":322},[234,829,830],{"class":243},"legend.setColumnCount(",[234,832,833],{"class":367},"2",[234,835,287],{"class":243},[234,837,838,841,843],{"class":147,"line":333},[234,839,840],{"class":243},"legend.setSplitLayer(",[234,842,434],{"class":367},[234,844,287],{"class":243},[234,846,847,850,852],{"class":147,"line":344},[234,848,849],{"class":243},"legend.setEqualColumnWidth(",[234,851,434],{"class":367},[234,853,287],{"class":243},[234,855,856,859,861],{"class":147,"line":350},[234,857,858],{"class":243},"legend.setSymbolWidth(",[234,860,96],{"class":367},[234,862,287],{"class":243},[234,864,865,868,871],{"class":147,"line":355},[234,866,867],{"class":243},"legend.setSymbolHeight(",[234,869,870],{"class":367},"3",[234,872,287],{"class":243},[234,874,875],{"class":147,"line":361},[234,876,619],{"class":243},[14,878,879,881,882,885,886,889],{},[201,880,396],{}," Each part of a legend has its own style slot, so a title, group headings and symbol labels can be sized independently. ",[231,883,884],{},"setColumnCount(2)"," with ",[231,887,888],{},"setSplitLayer(True)"," allows a single layer's many classes to flow across columns rather than forcing each layer into one — essential for a graduated renderer with nine classes on a portrait page. Symbol width and height are in millimetres and default to values tuned for screen; shrinking them slightly is usually what makes a dense legend fit.",[190,891,893],{"id":892},"keep-it-stable-through-an-atlas","Keep it stable through an atlas",[14,895,896],{},"An atlas redraws the legend for every feature, and a legend that resizes per page makes a document that looks unsettled.",[224,898,900],{"className":226,"code":899,"language":228,"meta":229,"style":229},"atlas = layout.atlas()\nlegend.setResizeToContents(False)\nlegend.attemptResize(QgsLayoutSize(70, 100, QgsUnitTypes.LayoutMillimeters))\n",[231,901,902,912,921],{"__ignoreMap":229},[234,903,904,907,909],{"class":147,"line":236},[234,905,906],{"class":243},"atlas ",[234,908,266],{"class":239},[234,910,911],{"class":243}," layout.atlas()\n",[234,913,914,917,919],{"class":147,"line":253},[234,915,916],{"class":243},"legend.setResizeToContents(",[234,918,491],{"class":367},[234,920,287],{"class":243},[234,922,923,925,927,929,931],{"class":147,"line":260},[234,924,381],{"class":243},[234,926,384],{"class":367},[234,928,370],{"class":243},[234,930,389],{"class":367},[234,932,375],{"class":243},[14,934,935,397,937,940,941,943,944,948],{},[201,936,396],{},[231,938,939],{},"setResizeToContents(False)"," fixes the frame, so a ward with fewer visible classes still produces a legend box of the same size in the same place. Combined with ",[231,942,466],{},", the entries still change per page while the layout does not shift — the combination that makes an atlas look designed rather than generated. Atlas iteration itself is covered in ",[21,945,947],{"href":946},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fgenerate-atlas-pdf-pyqgis\u002F","Generate an Atlas PDF in PyQGIS",".",[190,950,952],{"id":951},"place-it-against-the-map-rather-than-the-page","Place it against the map rather than the page",[14,954,955],{},"Positioning by absolute page coordinates works until the map item moves, the page size changes, or a portrait layout gains a landscape sibling. Anchoring the legend to a corner of the map keeps the relationship intact.",[224,957,959],{"className":226,"code":958,"language":228,"meta":229,"style":229},"from qgis.core import QgsLayoutItem, QgsLayoutPoint, QgsUnitTypes\n\nmap_rect = map_item.rect()\nmap_pos = map_item.pagePos()\n\nmargin = 4\nlegend.setReferencePoint(QgsLayoutItem.UpperRight)\nlegend.attemptMove(\n    QgsLayoutPoint(\n        map_pos.x() + map_rect.width() - margin,\n        map_pos.y() + margin,\n        QgsUnitTypes.LayoutMillimeters,\n    )\n)\n",[231,960,961,972,976,986,996,1000,1010,1015,1020,1025,1042,1051,1056,1061],{"__ignoreMap":229},[234,962,963,965,967,969],{"class":147,"line":236},[234,964,240],{"class":239},[234,966,244],{"class":243},[234,968,247],{"class":239},[234,970,971],{"class":243}," QgsLayoutItem, QgsLayoutPoint, QgsUnitTypes\n",[234,973,974],{"class":147,"line":253},[234,975,257],{"emptyLinePlaceholder":256},[234,977,978,981,983],{"class":147,"line":260},[234,979,980],{"class":243},"map_rect ",[234,982,266],{"class":239},[234,984,985],{"class":243}," map_item.rect()\n",[234,987,988,991,993],{"class":147,"line":272},[234,989,990],{"class":243},"map_pos ",[234,992,266],{"class":239},[234,994,995],{"class":243}," map_item.pagePos()\n",[234,997,998],{"class":147,"line":290},[234,999,257],{"emptyLinePlaceholder":256},[234,1001,1002,1005,1007],{"class":147,"line":306},[234,1003,1004],{"class":243},"margin ",[234,1006,266],{"class":239},[234,1008,1009],{"class":367}," 4\n",[234,1011,1012],{"class":147,"line":311},[234,1013,1014],{"class":243},"legend.setReferencePoint(QgsLayoutItem.UpperRight)\n",[234,1016,1017],{"class":147,"line":322},[234,1018,1019],{"class":243},"legend.attemptMove(\n",[234,1021,1022],{"class":147,"line":333},[234,1023,1024],{"class":243},"    QgsLayoutPoint(\n",[234,1026,1027,1030,1033,1036,1039],{"class":147,"line":344},[234,1028,1029],{"class":243},"        map_pos.x() ",[234,1031,1032],{"class":239},"+",[234,1034,1035],{"class":243}," map_rect.width() ",[234,1037,1038],{"class":239},"-",[234,1040,1041],{"class":243}," margin,\n",[234,1043,1044,1047,1049],{"class":147,"line":350},[234,1045,1046],{"class":243},"        map_pos.y() ",[234,1048,1032],{"class":239},[234,1050,1041],{"class":243},[234,1052,1053],{"class":147,"line":355},[234,1054,1055],{"class":243},"        QgsUnitTypes.LayoutMillimeters,\n",[234,1057,1058],{"class":147,"line":361},[234,1059,1060],{"class":243},"    )\n",[234,1062,1063],{"class":147,"line":378},[234,1064,287],{"class":243},[14,1066,1067,397,1069,1072,1073,1076],{},[201,1068,396],{},[231,1070,1071],{},"setReferencePoint()"," decides which corner of the legend the coordinate refers to. With ",[231,1074,1075],{},"UpperRight",", the position given is the legend's top-right corner, so the box grows leftward and downward as entries are added — exactly what you want for a legend tucked inside the map's top-right corner, and the opposite of the default, which would push it off the page. Deriving the coordinate from the map item's own position and size rather than from constants means a layout whose map is resized keeps its legend in the corner.",[14,1078,1079,1080,1084],{},"For a layout that will be reused at several page sizes, go one step further and compute the margin as a fraction of the map's width. The same arithmetic then places a scalebar, a north arrow and a title block — the pieces covered in ",[21,1081,1083],{"href":1082},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fadd-scalebar-and-north-arrow-pyqgis\u002F","Add a Scalebar and North Arrow in PyQGIS"," — and a single helper that anchors any item to any corner of the map removes most of the fiddly coordinate work from layout scripts.",[14,1086,1087],{},[29,1088,1091,1094,1097,1100,1103,1107,1110,1115,1117,1122,1128,1131,1136,1140,1143,1146,1149,1152,1155,1159,1162,1165],{"viewBox":1089,"role":32,"ariaLabel":1090,"xmlns":34},"0 0 760 258","A legend anchored by its upper-right reference point growing leftward and downward inside the map corner, compared with a default upper-left anchor that pushes the box off the page as entries are added",[36,1092,1093],{},"The reference point decides which way the box grows",[40,1095,1096],{},"With an upper-left reference point the legend grows to the right and off the page edge as entries are added. With an upper-right reference point the same coordinate keeps the box's top-right corner fixed in the map's corner, so it expands inward and stays on the page.",[44,1098],{"x":46,"y":46,"width":47,"height":1099,"fill":49},"258",[68,1101,1102],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Same coordinate, opposite outcome",[44,1104],{"x":78,"y":79,"width":1105,"height":1106,"rx":58,"fill":81,"stroke":82,"style":83},"340","180",[68,1108,1109],{"x":675,"y":676,"style":115,"fill":141,"textAnchor":74},"reference point: upper left",[44,1111],{"x":1112,"y":1113,"width":1114,"height":119,"rx":96,"fill":97,"stroke":98,"style":83},"44","88","292",[68,1116,104],{"x":108,"y":80,"style":168,"fill":98,"textAnchor":74},[44,1118],{"x":94,"y":389,"width":1119,"height":1120,"rx":59,"fill":177,"stroke":1121,"style":83},"118","72","#b91c1c",[68,1123,1127],{"x":1124,"y":1125,"style":1126,"fill":66,"textAnchor":74},"271","132","text-anchor:middle;font-size:10px;font-family:sans-serif","legend grows",[68,1129,1130],{"x":1124,"y":132,"style":1126,"fill":66,"textAnchor":74},"rightward",[63,1132],{"d":1133,"fill":1134,"stroke":1121,"style":1135},"M330 136 H386","none","stroke-width:2;stroke-dasharray:5 4",[1137,1138],"circle",{"cx":94,"cy":389,"r":1139,"fill":1121},"4",[44,1141],{"x":1142,"y":79,"width":1105,"height":1106,"rx":58,"fill":81,"stroke":82,"style":83},"400",[68,1144,1145],{"x":721,"y":676,"style":115,"fill":133,"textAnchor":74},"reference point: upper right",[44,1147],{"x":1148,"y":1113,"width":1114,"height":119,"rx":96,"fill":97,"stroke":98,"style":83},"424",[68,1150,104],{"x":1151,"y":80,"style":168,"fill":98,"textAnchor":74},"500",[44,1153],{"x":1154,"y":389,"width":693,"height":1120,"rx":59,"fill":158,"stroke":133,"style":83},"560",[68,1156,1158],{"x":1157,"y":119,"style":1126,"fill":66,"textAnchor":74},"630","← legend grows",[68,1160,1161],{"x":1157,"y":132,"style":1126,"fill":133,"textAnchor":74},"stays in the corner",[1137,1163],{"cx":1164,"cy":389,"r":1139,"fill":133},"700",[68,1166,1168],{"x":70,"y":1167,"style":168,"fill":82,"textAnchor":74},"250","Derive the coordinate from the map item's position so a resized map keeps its furniture",[190,1170,1172],{"id":1171},"qgis-version-compatibility","QGIS version compatibility",[14,1174,1175,1176,1178],{},"The examples target ",[201,1177,203],{}," (Python 3.12).",[1180,1181,1182,1198],"table",{},[1183,1184,1185],"thead",{},[1186,1187,1188,1192,1195],"tr",{},[1189,1190,1191],"th",{},"QGIS version",[1189,1193,1194],{},"Python",[1189,1196,1197],{},"Notes",[1199,1200,1201,1217,1227,1238],"tbody",{},[1186,1202,1203,1207,1210],{},[1204,1205,1206],"td",{},"3.22 LTR",[1204,1208,1209],{},"3.9",[1204,1211,1212,1213,1216],{},"Full legend API; ",[231,1214,1215],{},"setLegendFilterOutAtlas"," present.",[1186,1218,1219,1222,1224],{},[1204,1220,1221],{},"3.28 LTR",[1204,1223,1209],{},[1204,1225,1226],{},"Behaviour matches this page.",[1186,1228,1229,1232,1235],{},[1204,1230,1231],{},"3.34 LTR",[1204,1233,1234],{},"3.12",[1204,1236,1237],{},"Baseline for this page.",[1186,1239,1240,1243,1245],{},[1204,1241,1242],{},"3.40 \u002F 3.44",[1204,1244,1234],{},[1204,1246,1247,1250],{},[231,1248,1249],{},"QgsLegendStyle.Style"," members are scoped; adds per-item text formats alongside fonts.",[190,1252,1254],{"id":1253},"troubleshooting","Troubleshooting",[195,1256,1257,1263,1271,1277,1283,1294],{},[198,1258,1259,1262],{},[201,1260,1261],{},"The legend lists layers the map does not show."," No linked map, or filtering not enabled. Set both.",[198,1264,1265,397,1268,1270],{},[201,1266,1267],{},"Manual renames disappear.",[231,1269,474],{}," is still set; the model regenerated from the project tree.",[198,1272,1273,1276],{},[201,1274,1275],{},"The legend is empty."," Filtering by map is on and the map extent contains no features from any layer — often because the map item's extent was never set.",[198,1278,1279,1282],{},[201,1280,1281],{},"Entries appear in the wrong order."," The legend follows the project layer tree. Reorder the layers, or reorder the nodes in the legend's own model.",[198,1284,1285,397,1288,1290,1291,1293],{},[201,1286,1287],{},"The box overlaps the map.",[231,1289,649],{}," was not called after editing, or ",[231,1292,939],{}," fixed it at the wrong size.",[198,1295,1296,1299,1300,948],{},[201,1297,1298],{},"Fonts differ between screen and PDF."," The font is not installed on the exporting machine. Embed fonts on export, or use a font present in the container — see ",[21,1301,1303],{"href":1302},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Frun-pyqgis-in-docker-container\u002F","Run PyQGIS in a Docker Container",[190,1305,1307],{"id":1306},"conclusion","Conclusion",[14,1309,1310],{},"A good automated legend is four decisions: link it to the map item, filter it to what that map draws, turn off the auto-update model before curating names and removing entries, and fix the frame size when an atlas will iterate. Everything else — columns, fonts, symbol sizes — is typography you set once and reuse across every layout in the project.",[190,1312,1314],{"id":1313},"frequently-asked-questions","Frequently Asked Questions",[14,1316,1317,1320,1321,1324],{},[201,1318,1319],{},"How do I add a layer to the legend that is not in the map?","\nTurn off the auto-update model and add the node yourself with ",[231,1322,1323],{},"model().rootGroup().addLayer(layer)",". Use it sparingly: a legend entry for something the map does not draw is a promise the map breaks.",[14,1326,1327,1330,1331,1334],{},[201,1328,1329],{},"Can I show only some classes of a layer?","\nYes — find the layer node and remove individual symbol items via ",[231,1332,1333],{},"QgsMapLayerLegendUtils.setLegendNodeOrder()",", or hide them by index. It is fiddly; often the cleaner answer is a rule-based renderer whose rules are the classes you want shown.",[14,1336,1337,1340,1341,948],{},[201,1338,1339],{},"Why does my graduated legend show the raw class ranges?","\nThose are the renderer's labels. Set friendlier ones on the renderer's range objects before building the legend — see ",[21,1342,1344],{"href":1343},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS",[14,1346,1347,1350,1353,1354,1357],{},[201,1348,1349],{},"How do I add a title above the legend but below the frame?",[231,1351,1352],{},"setTitle()"," handles it, and the title's font comes from the ",[231,1355,1356],{},"QgsLegendStyle.Title"," slot. An empty string removes it entirely.",[14,1359,1360,1363],{},[201,1361,1362],{},"Does the legend update if I restyle a layer afterwards?","\nOn the next refresh, yes — the symbols are read from the layer at render time. Renamed entries survive only while the auto-update model is off.",[190,1365,1367],{"id":1366},"related","Related",[195,1369,1370,1375,1379,1383,1387],{},[198,1371,1372,1374],{},[21,1373,24],{"href":23}," — the guide this recipe belongs to",[198,1376,1377],{},[21,1378,214],{"href":213},[198,1380,1381],{},[21,1382,947],{"href":946},[198,1384,1385],{},[21,1386,1083],{"href":1082},[198,1388,1389],{},[21,1390,1392],{"href":1391},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002F","Create a Rule-Based Renderer in PyQGIS",[1394,1395,1396],"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":229,"searchDepth":253,"depth":253,"links":1398},[1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410],{"id":192,"depth":253,"text":193},{"id":221,"depth":253,"text":222},{"id":420,"depth":253,"text":421},{"id":478,"depth":253,"text":479},{"id":744,"depth":253,"text":745},{"id":892,"depth":253,"text":893},{"id":951,"depth":253,"text":952},{"id":1171,"depth":253,"text":1172},{"id":1253,"depth":253,"text":1254},{"id":1306,"depth":253,"text":1307},{"id":1313,"depth":253,"text":1314},{"id":1366,"depth":253,"text":1367},"Build a layout legend in Python — link it to a map item, filter to visible layers, curate entries with a custom legend model, control columns and fonts, and keep it stable across an atlas.","md",{"slug":1414,"type":1415,"breadcrumb":1416,"datePublished":1417,"dateModified":1417},"add-legend-to-layout-pyqgis","article","Add a Legend","2026-08-10","\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-legend-to-layout-pyqgis",{"title":5,"description":1411},"spatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-legend-to-layout-pyqgis\u002Findex","s_LI0lD1-QP9lhdflFLi7ZpEF8OBWceZ99TmrC7bD04",1786401337556]