[{"data":1,"prerenderedAt":1697},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Ffilter-and-sort-atlas-features-pyqgis":3},{"id":4,"title":5,"body":6,"description":1686,"extension":1687,"meta":1688,"navigation":235,"path":1693,"seo":1694,"stem":1695,"__hash__":1696},"docs\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Ffilter-and-sort-atlas-features-pyqgis\u002Findex.md","Filter and Sort Atlas Features in PyQGIS",{"type":7,"value":8,"toc":1670},"minimark",[9,13,26,35,175,180,195,199,202,404,435,439,442,539,551,608,612,615,661,671,675,678,746,759,763,766,876,889,893,896,1046,1054,1058,1061,1114,1131,1135,1138,1226,1507,1512,1516,1533,1537,1570,1574,1577,1581,1595,1604,1614,1628,1634,1638,1666],[10,11,5],"h1",{"id":12},"filter-and-sort-atlas-features-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"An atlas makes one page per feature of its coverage layer — every district, every inspection area, every sheet of a map grid. In practice you rarely want ",[18,19,20],"em",{},"every"," feature in ",[18,23,24],{},"whatever order"," the data source returns: the planning team wants only districts with open applications, sorted by priority; the field crew wants their own area's sheets in route order; the annual report wants districts alphabetically. The atlas has settings for exactly this — a filter expression, a sort expression and a page-name expression — and from Python they can be changed between runs to produce several tailored series from one layout.",[14,27,28,29,34],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002F","Automating Atlas Map Series",". It filters coverage features with an expression, sorts pages by fields and computed values, names pages for output files, hides the coverage layer, and runs the same atlas several times with different filters.",[14,36,37],{},[38,39,44,48,52,59,76,85,94,100,105,109,115,120,126,131,136,140,145,149,152,155,159,165,171],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 226","img","A coverage layer filtered by an expression, sorted by another, and named by a third to produce the atlas page sequence","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"From coverage layer to page sequence",[49,50,51],"desc",{},"The coverage layer holds all features. The filter expression keeps only those that should become pages, for example districts with open applications. The sort expression orders the remaining features, for example by priority descending then name. The page-name expression gives each page a name used for file names and the page number list. The result is the page sequence the atlas renders.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","226","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"atFilterArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Filter, sort, name",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":67,"fill":91,"stroke":92,"style":93},"24","70","160","140","#fffdf7","#59645f","stroke-width:2",[77,95,99],{"x":96,"y":97,"style":98,"fill":82,"textAnchor":83},"104","117.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","coverage",[77,101,104],{"x":96,"y":102,"style":103,"fill":92,"textAnchor":83},"143.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","all 48",[77,106,108],{"x":96,"y":107,"style":103,"fill":92,"textAnchor":83},"169.78","districts",[110,111],"line",{"x1":112,"y1":90,"x2":113,"y2":90,"stroke":75,"style":114},"184","224","stroke-width:1.8;marker-end:url(#atFilterArrow)",[53,116],{"x":117,"y":88,"width":89,"height":90,"rx":67,"fill":118,"stroke":119,"style":93},"228","#fdf2e2","#b45309",[77,121,125],{"x":122,"y":123,"style":124,"fill":119,"textAnchor":83},"308","117.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","filter",[77,127,130],{"x":122,"y":128,"style":129,"fill":75,"textAnchor":83},"143.6","text-anchor:middle;font-size:9.5px;font-family:monospace","\"open_apps\" > 0",[77,132,135],{"x":122,"y":133,"style":134,"fill":92,"textAnchor":83},"169.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","31 left",[110,137],{"x1":138,"y1":90,"x2":139,"y2":90,"stroke":75,"style":114},"388","428",[53,141],{"x":142,"y":88,"width":89,"height":90,"rx":67,"fill":143,"stroke":144,"style":93},"432","#eff3ff","#2563eb",[77,146,148],{"x":147,"y":123,"style":124,"fill":144,"textAnchor":83},"512","sort",[77,150,151],{"x":147,"y":128,"style":129,"fill":75,"textAnchor":83},"\"priority\" desc",[77,153,154],{"x":147,"y":133,"style":134,"fill":92,"textAnchor":83},"then name",[110,156],{"x1":157,"y1":90,"x2":158,"y2":90,"stroke":75,"style":114},"592","632",[53,160],{"x":161,"y":88,"width":162,"height":90,"rx":67,"fill":163,"stroke":164,"style":93},"636","100","#e8efe6","#15803d",[77,166,170],{"x":167,"y":168,"style":98,"fill":169,"textAnchor":83},"686","130.78","#166534","pages",[77,172,174],{"x":167,"y":173,"style":103,"fill":92,"textAnchor":83},"156.78","31, named",[176,177,179],"h2",{"id":178},"prerequisites","Prerequisites",[181,182,183,187],"ul",{},[184,185,186],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[184,188,189,190,194],{},"A layout with an atlas enabled and a coverage layer set, created in the designer or in code as in ",[30,191,193],{"href":192},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fconfigure-atlas-coverage-layer-pyqgis\u002F","configuring an atlas coverage layer",".",[176,196,198],{"id":197},"filter-the-coverage-features","Filter the coverage features",[14,200,201],{},"The filter expression is evaluated for each coverage feature; only those returning true become pages.",[203,204,209],"pre",{"className":205,"code":206,"language":207,"meta":208,"style":208},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nlayout = QgsProject.instance().layoutManager().layoutByName(\"District atlas\")\natlas = layout.atlas()\ncoverage = atlas.coverageLayer()\nprint(\"coverage:\", coverage.name(), coverage.featureCount(), \"features\")\n\natlas.setFilterFeatures(True)\nok, error = atlas.setFilterExpression('\"open_apps\" > 0 AND \"status\" \u003C> \\'archived\\'')\nif not ok:\n    raise ValueError(f\"bad filter: {error}\")\natlas.updateFeatures()\nprint(atlas.count(), \"pages after filtering\")\n","python","",[210,211,212,230,237,256,267,278,299,304,315,342,354,385,391],"code",{"__ignoreMap":208},[213,214,216,220,224,227],"span",{"class":110,"line":215},1,[213,217,219],{"class":218},"snl16","from",[213,221,223],{"class":222},"s95oV"," qgis.core ",[213,225,226],{"class":218},"import",[213,228,229],{"class":222}," QgsProject\n",[213,231,233],{"class":110,"line":232},2,[213,234,236],{"emptyLinePlaceholder":235},true,"\n",[213,238,240,243,246,249,253],{"class":110,"line":239},3,[213,241,242],{"class":222},"layout ",[213,244,245],{"class":218},"=",[213,247,248],{"class":222}," QgsProject.instance().layoutManager().layoutByName(",[213,250,252],{"class":251},"sU2Wk","\"District atlas\"",[213,254,255],{"class":222},")\n",[213,257,259,262,264],{"class":110,"line":258},4,[213,260,261],{"class":222},"atlas ",[213,263,245],{"class":218},[213,265,266],{"class":222}," layout.atlas()\n",[213,268,270,273,275],{"class":110,"line":269},5,[213,271,272],{"class":222},"coverage ",[213,274,245],{"class":218},[213,276,277],{"class":222}," atlas.coverageLayer()\n",[213,279,281,285,288,291,294,297],{"class":110,"line":280},6,[213,282,284],{"class":283},"sDLfK","print",[213,286,287],{"class":222},"(",[213,289,290],{"class":251},"\"coverage:\"",[213,292,293],{"class":222},", coverage.name(), coverage.featureCount(), ",[213,295,296],{"class":251},"\"features\"",[213,298,255],{"class":222},[213,300,302],{"class":110,"line":301},7,[213,303,236],{"emptyLinePlaceholder":235},[213,305,307,310,313],{"class":110,"line":306},8,[213,308,309],{"class":222},"atlas.setFilterFeatures(",[213,311,312],{"class":283},"True",[213,314,255],{"class":222},[213,316,318,321,323,326,329,332,335,337,340],{"class":110,"line":317},9,[213,319,320],{"class":222},"ok, error ",[213,322,245],{"class":218},[213,324,325],{"class":222}," atlas.setFilterExpression(",[213,327,328],{"class":251},"'\"open_apps\" > 0 AND \"status\" \u003C> ",[213,330,331],{"class":283},"\\'",[213,333,334],{"class":251},"archived",[213,336,331],{"class":283},[213,338,339],{"class":251},"'",[213,341,255],{"class":222},[213,343,345,348,351],{"class":110,"line":344},10,[213,346,347],{"class":218},"if",[213,349,350],{"class":218}," not",[213,352,353],{"class":222}," ok:\n",[213,355,357,360,363,365,368,371,374,377,380,383],{"class":110,"line":356},11,[213,358,359],{"class":218},"    raise",[213,361,362],{"class":283}," ValueError",[213,364,287],{"class":222},[213,366,367],{"class":218},"f",[213,369,370],{"class":251},"\"bad filter: ",[213,372,373],{"class":283},"{",[213,375,376],{"class":222},"error",[213,378,379],{"class":283},"}",[213,381,382],{"class":251},"\"",[213,384,255],{"class":222},[213,386,388],{"class":110,"line":387},12,[213,389,390],{"class":222},"atlas.updateFeatures()\n",[213,392,394,396,399,402],{"class":110,"line":393},13,[213,395,284],{"class":283},[213,397,398],{"class":222},"(atlas.count(), ",[213,400,401],{"class":251},"\"pages after filtering\"",[213,403,255],{"class":222},[14,405,406,410,411,414,415,418,419,422,423,426,427,430,431,434],{},[407,408,409],"strong",{},"Breakdown:"," ",[210,412,413],{},"setFilterFeatures(True)"," switches filtering on; the expression alone does nothing without it. ",[210,416,417],{},"setFilterExpression"," validates the expression and returns an error message for syntax mistakes, which is worth checking because a broken filter otherwise yields an atlas of zero pages. ",[210,420,421],{},"updateFeatures()"," re-evaluates the filter and sorting immediately; ",[210,424,425],{},"count()"," then reports the page count, a good sanity check before a long export. NULL values follow expression rules — a NULL ",[210,428,429],{},"open_apps"," is not greater than zero, so those districts are excluded; use ",[210,432,433],{},"coalesce"," if they should count.",[176,436,438],{"id":437},"sort-pages","Sort pages",[14,440,441],{},"Without sorting, page order is the provider's feature order. A sort expression makes it deliberate.",[203,443,445],{"className":205,"code":444,"language":207,"meta":208,"style":208},"atlas.setSortFeatures(True)\natlas.setSortExpression('\"priority\"')\natlas.setSortAscending(False)\natlas.updateFeatures()\n\nnames = []\nfor i in range(atlas.count()):\n    atlas.seekTo(i)\n    names.append(layout.reportContext().feature()[\"name\"])\nprint(names[:10])\n",[210,446,447,456,466,476,480,484,494,511,516,527],{"__ignoreMap":208},[213,448,449,452,454],{"class":110,"line":215},[213,450,451],{"class":222},"atlas.setSortFeatures(",[213,453,312],{"class":283},[213,455,255],{"class":222},[213,457,458,461,464],{"class":110,"line":232},[213,459,460],{"class":222},"atlas.setSortExpression(",[213,462,463],{"class":251},"'\"priority\"'",[213,465,255],{"class":222},[213,467,468,471,474],{"class":110,"line":239},[213,469,470],{"class":222},"atlas.setSortAscending(",[213,472,473],{"class":283},"False",[213,475,255],{"class":222},[213,477,478],{"class":110,"line":258},[213,479,390],{"class":222},[213,481,482],{"class":110,"line":269},[213,483,236],{"emptyLinePlaceholder":235},[213,485,486,489,491],{"class":110,"line":280},[213,487,488],{"class":222},"names ",[213,490,245],{"class":218},[213,492,493],{"class":222}," []\n",[213,495,496,499,502,505,508],{"class":110,"line":301},[213,497,498],{"class":218},"for",[213,500,501],{"class":222}," i ",[213,503,504],{"class":218},"in",[213,506,507],{"class":283}," range",[213,509,510],{"class":222},"(atlas.count()):\n",[213,512,513],{"class":110,"line":306},[213,514,515],{"class":222},"    atlas.seekTo(i)\n",[213,517,518,521,524],{"class":110,"line":317},[213,519,520],{"class":222},"    names.append(layout.reportContext().feature()[",[213,522,523],{"class":251},"\"name\"",[213,525,526],{"class":222},"])\n",[213,528,529,531,534,537],{"class":110,"line":344},[213,530,284],{"class":283},[213,532,533],{"class":222},"(names[:",[213,535,536],{"class":283},"10",[213,538,526],{"class":222},[14,540,541,543,544,547,548,194],{},[407,542,409],{}," Sorting by ",[210,545,546],{},"\"priority\""," descending puts urgent districts first. The expression can be any expression, so composite orders are possible, as the next section shows. Seeking through the atlas and reading each current feature confirms the order before exporting. The report context holds the current atlas feature after ",[210,549,550],{},"seekTo",[14,552,553],{},[38,554,557,560,563,566,569,574,579,583,587,591,596,600,603,606],{"viewBox":555,"role":41,"ariaLabel":556,"xmlns":43},"0 0 760 222","Composite sort expressions combining priority and name, or a route sequence, into one sortable key",[45,558,559],{},"Composite sort keys",[49,561,562],{},"A single sort expression can encode several keys by building a sortable string or number: priority as a zero-padded number followed by the name, so pages are ordered by priority and alphabetically within each priority. For route order, a sort by a precomputed sequence field gives the order a field crew will travel.",[53,564],{"x":55,"y":55,"width":56,"height":565,"fill":58},"222",[77,567,568],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"One expression, several keys",[53,570],{"x":87,"y":571,"width":572,"height":573,"rx":67,"fill":143,"stroke":144,"style":93},"56","340","150",[77,575,578],{"x":576,"y":577,"style":124,"fill":144,"textAnchor":83},"194","92.6","priority, then name",[77,580,582],{"x":576,"y":581,"style":129,"fill":75,"textAnchor":83},"120.6","lpad(10 - \"priority\", 2, '0')",[77,584,586],{"x":576,"y":585,"style":129,"fill":75,"textAnchor":83},"148.6","|| \"name\"",[77,588,590],{"x":576,"y":589,"style":134,"fill":92,"textAnchor":83},"176.6","ascending",[53,592],{"x":593,"y":571,"width":572,"height":573,"rx":67,"fill":594,"stroke":595,"style":93},"396","#eef7f4","#0f766e",[77,597,599],{"x":598,"y":577,"style":124,"fill":595,"textAnchor":83},"566","route order",[77,601,602],{"x":598,"y":581,"style":129,"fill":75,"textAnchor":83},"\"route_seq\"",[77,604,605],{"x":598,"y":585,"style":134,"fill":92,"textAnchor":83},"from a routing step",[77,607,590],{"x":598,"y":589,"style":134,"fill":92,"textAnchor":83},[176,609,611],{"id":610},"sort-by-more-than-one-key","Sort by more than one key",[14,613,614],{},"The atlas takes one sort expression, but one expression can combine keys by building a string that sorts correctly.",[203,616,618],{"className":205,"code":617,"language":207,"meta":208,"style":208},"atlas.setSortExpression(\"lpad(to_string(10 - \\\"priority\\\"), 2, '0') || ' ' || \\\"name\\\"\")\natlas.setSortAscending(True)\natlas.updateFeatures()\n",[210,619,620,649,657],{"__ignoreMap":208},[213,621,622,624,627,630,633,635,638,640,643,645,647],{"class":110,"line":215},[213,623,460],{"class":222},[213,625,626],{"class":251},"\"lpad(to_string(10 - ",[213,628,629],{"class":283},"\\\"",[213,631,632],{"class":251},"priority",[213,634,629],{"class":283},[213,636,637],{"class":251},"), 2, '0') || ' ' || ",[213,639,629],{"class":283},[213,641,642],{"class":251},"name",[213,644,629],{"class":283},[213,646,382],{"class":251},[213,648,255],{"class":222},[213,650,651,653,655],{"class":110,"line":232},[213,652,470],{"class":222},[213,654,312],{"class":283},[213,656,255],{"class":222},[213,658,659],{"class":110,"line":239},[213,660,390],{"class":222},[14,662,663,665,666,670],{},[407,664,409],{}," Turning priority 9 into \"01\" and priority 1 into \"09\" makes higher priorities sort first in ascending order; appending the name sorts alphabetically within each priority. Zero-padding is essential, because strings compare character by character — \"10\" sorts before \"9\" otherwise. For a field route, compute a sequence number with the ",[30,667,669],{"href":668},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fshortest-path-between-points-pyqgis\u002F","nearest facility or shortest path tools"," and sort by it, so pages appear in the order the crew drives.",[176,672,674],{"id":673},"name-pages-for-output-files","Name pages for output files",[14,676,677],{},"The page-name expression gives each page a name. It appears in the atlas toolbar's page list and, more usefully, can drive file names when exporting one file per page.",[203,679,681],{"className":205,"code":680,"language":207,"meta":208,"style":208},"atlas.setPageNameExpression(\"\\\"district_code\\\" || '_' || replace(\\\"name\\\", ' ', '_')\")\natlas.setFilenameExpression(\"'district_' || @atlas_pagename\")\natlas.updateFeatures()\n\natlas.first()\nprint(atlas.currentFilename(), atlas.nameForPage(0))\n",[210,682,683,711,721,725,729,734],{"__ignoreMap":208},[213,684,685,688,690,692,695,697,700,702,704,706,709],{"class":110,"line":215},[213,686,687],{"class":222},"atlas.setPageNameExpression(",[213,689,382],{"class":251},[213,691,629],{"class":283},[213,693,694],{"class":251},"district_code",[213,696,629],{"class":283},[213,698,699],{"class":251}," || '_' || replace(",[213,701,629],{"class":283},[213,703,642],{"class":251},[213,705,629],{"class":283},[213,707,708],{"class":251},", ' ', '_')\"",[213,710,255],{"class":222},[213,712,713,716,719],{"class":110,"line":232},[213,714,715],{"class":222},"atlas.setFilenameExpression(",[213,717,718],{"class":251},"\"'district_' || @atlas_pagename\"",[213,720,255],{"class":222},[213,722,723],{"class":110,"line":239},[213,724,390],{"class":222},[213,726,727],{"class":110,"line":258},[213,728,236],{"emptyLinePlaceholder":235},[213,730,731],{"class":110,"line":269},[213,732,733],{"class":222},"atlas.first()\n",[213,735,736,738,741,743],{"class":110,"line":280},[213,737,284],{"class":283},[213,739,740],{"class":222},"(atlas.currentFilename(), atlas.nameForPage(",[213,742,55],{"class":283},[213,744,745],{"class":222},"))\n",[14,747,748,750,751,754,755,194],{},[407,749,409],{}," A page name built from a stable code plus the name sorts sensibly in a file browser and survives renamed districts better than the name alone. The filename expression references ",[210,752,753],{},"@atlas_pagename",", so both stay in step; replacing spaces avoids awkward file names. Exporting with these names is covered in ",[30,756,758],{"href":757},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fexport-atlas-pages-to-individual-pdfs-pyqgis\u002F","exporting atlas pages to individual PDFs",[176,760,762],{"id":761},"build-the-filter-from-a-selection","Build the filter from a selection",[14,764,765],{},"Interactive tools often let a user select features on the map and then print \"just these\". The atlas filter can be generated from the coverage layer's current selection.",[203,767,769],{"className":205,"code":768,"language":207,"meta":208,"style":208},"selected = coverage.selectedFeatureIds()\nif not selected:\n    raise ValueError(\"select at least one feature to print\")\nid_list = \",\".join(str(i) for i in selected)\natlas.setFilterFeatures(True)\natlas.setFilterExpression(f\"$id IN ({id_list})\")\natlas.updateFeatures()\nprint(atlas.count(), \"pages from the selection\")\n",[210,770,771,781,790,803,831,839,861,865],{"__ignoreMap":208},[213,772,773,776,778],{"class":110,"line":215},[213,774,775],{"class":222},"selected ",[213,777,245],{"class":218},[213,779,780],{"class":222}," coverage.selectedFeatureIds()\n",[213,782,783,785,787],{"class":110,"line":232},[213,784,347],{"class":218},[213,786,350],{"class":218},[213,788,789],{"class":222}," selected:\n",[213,791,792,794,796,798,801],{"class":110,"line":239},[213,793,359],{"class":218},[213,795,362],{"class":283},[213,797,287],{"class":222},[213,799,800],{"class":251},"\"select at least one feature to print\"",[213,802,255],{"class":222},[213,804,805,808,810,813,816,819,822,824,826,828],{"class":110,"line":258},[213,806,807],{"class":222},"id_list ",[213,809,245],{"class":218},[213,811,812],{"class":251}," \",\"",[213,814,815],{"class":222},".join(",[213,817,818],{"class":283},"str",[213,820,821],{"class":222},"(i) ",[213,823,498],{"class":218},[213,825,501],{"class":222},[213,827,504],{"class":218},[213,829,830],{"class":222}," selected)\n",[213,832,833,835,837],{"class":110,"line":269},[213,834,309],{"class":222},[213,836,312],{"class":283},[213,838,255],{"class":222},[213,840,841,844,846,849,851,854,856,859],{"class":110,"line":280},[213,842,843],{"class":222},"atlas.setFilterExpression(",[213,845,367],{"class":218},[213,847,848],{"class":251},"\"$id IN (",[213,850,373],{"class":283},[213,852,853],{"class":222},"id_list",[213,855,379],{"class":283},[213,857,858],{"class":251},")\"",[213,860,255],{"class":222},[213,862,863],{"class":110,"line":301},[213,864,390],{"class":222},[213,866,867,869,871,874],{"class":110,"line":306},[213,868,284],{"class":283},[213,870,398],{"class":222},[213,872,873],{"class":251},"\"pages from the selection\"",[213,875,255],{"class":222},[14,877,878,880,881,884,885,194],{},[407,879,409],{}," Feature ids of the selection go straight into an ",[210,882,883],{},"IN"," list, which the expression engine evaluates quickly even for hundreds of ids. Refusing an empty selection prevents an accidental export of zero pages — or, if the filter were skipped, of every page. Within a plugin, wire this to a button so users can select, click and get their maps, as described in ",[30,886,888],{"href":887},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-toolbar-button-to-qgis-plugin\u002F","adding a toolbar button",[176,890,892],{"id":891},"preview-the-page-list-before-exporting","Preview the page list before exporting",[14,894,895],{},"A long atlas export can take an hour. Listing the pages it will produce — names, order and count — takes a second and catches filter and sort mistakes before the time is spent.",[203,897,899],{"className":205,"code":898,"language":207,"meta":208,"style":208},"def preview_pages(atlas, limit=20):\n    atlas.updateFeatures()\n    rows = []\n    for i in range(min(atlas.count(), limit)):\n        rows.append((i + 1, atlas.nameForPage(i)))\n    return atlas.count(), rows\n\ntotal, rows = preview_pages(atlas)\nprint(total, \"pages\")\nfor n, name in rows:\n    print(f\"{n:>3}  {name}\")\n",[210,900,901,921,926,935,954,968,976,980,990,1002,1014],{"__ignoreMap":208},[213,902,903,906,910,913,915,918],{"class":110,"line":215},[213,904,905],{"class":218},"def",[213,907,909],{"class":908},"svObZ"," preview_pages",[213,911,912],{"class":222},"(atlas, limit",[213,914,245],{"class":218},[213,916,917],{"class":283},"20",[213,919,920],{"class":222},"):\n",[213,922,923],{"class":110,"line":232},[213,924,925],{"class":222},"    atlas.updateFeatures()\n",[213,927,928,931,933],{"class":110,"line":239},[213,929,930],{"class":222},"    rows ",[213,932,245],{"class":218},[213,934,493],{"class":222},[213,936,937,940,942,944,946,948,951],{"class":110,"line":258},[213,938,939],{"class":218},"    for",[213,941,501],{"class":222},[213,943,504],{"class":218},[213,945,507],{"class":283},[213,947,287],{"class":222},[213,949,950],{"class":283},"min",[213,952,953],{"class":222},"(atlas.count(), limit)):\n",[213,955,956,959,962,965],{"class":110,"line":269},[213,957,958],{"class":222},"        rows.append((i ",[213,960,961],{"class":218},"+",[213,963,964],{"class":283}," 1",[213,966,967],{"class":222},", atlas.nameForPage(i)))\n",[213,969,970,973],{"class":110,"line":280},[213,971,972],{"class":218},"    return",[213,974,975],{"class":222}," atlas.count(), rows\n",[213,977,978],{"class":110,"line":301},[213,979,236],{"emptyLinePlaceholder":235},[213,981,982,985,987],{"class":110,"line":306},[213,983,984],{"class":222},"total, rows ",[213,986,245],{"class":218},[213,988,989],{"class":222}," preview_pages(atlas)\n",[213,991,992,994,997,1000],{"class":110,"line":317},[213,993,284],{"class":283},[213,995,996],{"class":222},"(total, ",[213,998,999],{"class":251},"\"pages\"",[213,1001,255],{"class":222},[213,1003,1004,1006,1009,1011],{"class":110,"line":344},[213,1005,498],{"class":218},[213,1007,1008],{"class":222}," n, name ",[213,1010,504],{"class":218},[213,1012,1013],{"class":222}," rows:\n",[213,1015,1016,1019,1021,1023,1025,1027,1030,1033,1035,1038,1040,1042,1044],{"class":110,"line":356},[213,1017,1018],{"class":283},"    print",[213,1020,287],{"class":222},[213,1022,367],{"class":218},[213,1024,382],{"class":251},[213,1026,373],{"class":283},[213,1028,1029],{"class":222},"n",[213,1031,1032],{"class":218},":>3",[213,1034,379],{"class":283},[213,1036,1037],{"class":283},"  {",[213,1039,642],{"class":222},[213,1041,379],{"class":283},[213,1043,382],{"class":251},[213,1045,255],{"class":222},[14,1047,1048,410,1050,1053],{},[407,1049,409],{},[210,1051,1052],{},"nameForPage"," returns each page's evaluated name without rendering anything, so the preview is instant. Printing the first twenty in order is usually enough to see whether filtering and sorting did what was intended; the total confirms the size of the run. Put this check at the top of export scripts and stop if the count is zero or wildly different from expectations.",[176,1055,1057],{"id":1056},"hide-the-coverage-layer","Hide the coverage layer",[14,1059,1060],{},"The coverage layer is often a helper — a grid of map sheets, the district boundaries used only to drive pages. The atlas can hide it from every map item without changing the project's visibility.",[203,1062,1064],{"className":205,"code":1063,"language":207,"meta":208,"style":208},"atlas.setHideCoverage(True)\nmap_item = layout.itemById(\"main_map\")\nmap_item.setAtlasDriven(True)\nmap_item.setAtlasScalingMode(map_item.Auto)\nmap_item.setAtlasMargin(0.1)\n",[210,1065,1066,1075,1090,1099,1104],{"__ignoreMap":208},[213,1067,1068,1071,1073],{"class":110,"line":215},[213,1069,1070],{"class":222},"atlas.setHideCoverage(",[213,1072,312],{"class":283},[213,1074,255],{"class":222},[213,1076,1077,1080,1082,1085,1088],{"class":110,"line":232},[213,1078,1079],{"class":222},"map_item ",[213,1081,245],{"class":218},[213,1083,1084],{"class":222}," layout.itemById(",[213,1086,1087],{"class":251},"\"main_map\"",[213,1089,255],{"class":222},[213,1091,1092,1095,1097],{"class":110,"line":239},[213,1093,1094],{"class":222},"map_item.setAtlasDriven(",[213,1096,312],{"class":283},[213,1098,255],{"class":222},[213,1100,1101],{"class":110,"line":258},[213,1102,1103],{"class":222},"map_item.setAtlasScalingMode(map_item.Auto)\n",[213,1105,1106,1109,1112],{"class":110,"line":269},[213,1107,1108],{"class":222},"map_item.setAtlasMargin(",[213,1110,1111],{"class":283},"0.1",[213,1113,255],{"class":222},[14,1115,1116,1118,1119,1122,1123,1126,1127,194],{},[407,1117,409],{}," With ",[210,1120,1121],{},"setHideCoverage",", the coverage layer is not drawn on any atlas page, whatever its visibility in the Layers panel. The map item must be atlas-driven to follow the current feature; ",[210,1124,1125],{},"Auto"," scaling chooses fixed or fitted scale depending on the feature's size, and a 10 % margin keeps the feature off the frame edge. To show the current feature differently — highlighted while others are dimmed — see ",[30,1128,1130],{"href":1129},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fhighlight-atlas-feature-in-overview-map-pyqgis\u002F","highlighting the atlas feature",[176,1132,1134],{"id":1133},"run-one-atlas-several-ways","Run one atlas several ways",[14,1136,1137],{},"Different audiences need different subsets of the same series. Changing the filter between exports produces each subset from one layout, without copies to maintain.",[14,1139,1140],{},[38,1141,1144,1147,1150,1153,1160,1163,1168,1173,1177,1182,1184,1187,1193,1197,1199,1203,1206,1210,1214,1218,1222],{"viewBox":1142,"role":41,"ariaLabel":1143,"xmlns":43},"0 0 760 230","One atlas layout exported several times with different filters and output folders for different audiences",[45,1145,1146],{},"Several exports from one layout",[49,1148,1149],{},"One layout with an atlas is exported three times. Each run sets a different filter and output folder: the north team's districts, the south team's districts, and all districts with open applications. The layout itself is never duplicated, so design changes apply to every run.",[53,1151],{"x":55,"y":55,"width":56,"height":1152,"fill":58},"230",[60,1154,1155],{},[63,1156,1158],{"id":1157,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"atRunsArrow",[72,1159],{"d":74,"fill":75},[77,1161,1162],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Same design, different subsets",[53,1164],{"x":87,"y":1165,"width":1166,"height":1167,"rx":67,"fill":91,"stroke":92,"style":93},"60","200","120",[77,1169,1172],{"x":1170,"y":1171,"style":98,"fill":82,"textAnchor":83},"124","110.78","one layout",[77,1174,1176],{"x":1170,"y":1175,"style":103,"fill":92,"textAnchor":83},"136.78","design once",[110,1178],{"x1":113,"y1":1179,"x2":1180,"y2":88,"stroke":75,"style":1181},"90","266","stroke-width:1.8;marker-end:url(#atRunsArrow)",[110,1183],{"x1":113,"y1":1167,"x2":1180,"y2":1167,"stroke":75,"style":1181},[110,1185],{"x1":113,"y1":573,"x2":1180,"y2":1186,"stroke":75,"style":1181},"170",[53,1188],{"x":1189,"y":1190,"width":1191,"height":1192,"rx":67,"fill":594,"stroke":595,"style":93},"270","44","220","52",[77,1194,1196],{"x":79,"y":1195,"style":124,"fill":595,"textAnchor":83},"73.6","team north",[53,1198],{"x":1189,"y":96,"width":1191,"height":1192,"rx":67,"fill":143,"stroke":144,"style":93},[77,1200,1202],{"x":79,"y":1201,"style":124,"fill":144,"textAnchor":83},"133.6","team south",[53,1204],{"x":1189,"y":1205,"width":1191,"height":1192,"rx":67,"fill":118,"stroke":119,"style":93},"164",[77,1207,1209],{"x":79,"y":1208,"style":124,"fill":119,"textAnchor":83},"193.6","open applications",[110,1211],{"x1":1212,"y1":1167,"x2":1213,"y2":1167,"stroke":75,"style":1181},"490","532",[53,1215],{"x":1216,"y":1217,"width":1166,"height":1217,"rx":67,"fill":163,"stroke":164,"style":93},"536","80",[77,1219,1221],{"x":161,"y":1220,"style":98,"fill":169,"textAnchor":83},"112.78","folder per run",[77,1223,1225],{"x":161,"y":1224,"style":103,"fill":92,"textAnchor":83},"134.78","same file names",[203,1227,1229],{"className":205,"code":1228,"language":207,"meta":208,"style":208},"from qgis.core import QgsLayoutExporter\nfrom pathlib import Path\n\nruns = {\n    \"team_north\": \"\\\"team\\\" = 'north'\",\n    \"team_south\": \"\\\"team\\\" = 'south'\",\n    \"open_applications\": \"\\\"open_apps\\\" > 0\",\n}\nfor run, expr in runs.items():\n    atlas.setFilterFeatures(True)\n    atlas.setFilterExpression(expr)\n    atlas.updateFeatures()\n    out_dir = Path(\"\u002Fdata\u002Fmaps\") \u002F run\n    out_dir.mkdir(parents=True, exist_ok=True)\n    result, error = QgsLayoutExporter.exportToPdfs(atlas, str(out_dir \u002F \"page.pdf\"),\n                                                   QgsLayoutExporter.PdfExportSettings())\n    print(f\"{run:\u003C20} {atlas.count():>3} pages  {'ok' if result == QgsLayoutExporter.Success else error}\")\n",[210,1230,1231,1242,1254,1258,1268,1291,1311,1331,1336,1348,1357,1362,1366,1388,1414,1438,1444],{"__ignoreMap":208},[213,1232,1233,1235,1237,1239],{"class":110,"line":215},[213,1234,219],{"class":218},[213,1236,223],{"class":222},[213,1238,226],{"class":218},[213,1240,1241],{"class":222}," QgsLayoutExporter\n",[213,1243,1244,1246,1249,1251],{"class":110,"line":232},[213,1245,219],{"class":218},[213,1247,1248],{"class":222}," pathlib ",[213,1250,226],{"class":218},[213,1252,1253],{"class":222}," Path\n",[213,1255,1256],{"class":110,"line":239},[213,1257,236],{"emptyLinePlaceholder":235},[213,1259,1260,1263,1265],{"class":110,"line":258},[213,1261,1262],{"class":222},"runs ",[213,1264,245],{"class":218},[213,1266,1267],{"class":222}," {\n",[213,1269,1270,1273,1276,1278,1280,1283,1285,1288],{"class":110,"line":269},[213,1271,1272],{"class":251},"    \"team_north\"",[213,1274,1275],{"class":222},": ",[213,1277,382],{"class":251},[213,1279,629],{"class":283},[213,1281,1282],{"class":251},"team",[213,1284,629],{"class":283},[213,1286,1287],{"class":251}," = 'north'\"",[213,1289,1290],{"class":222},",\n",[213,1292,1293,1296,1298,1300,1302,1304,1306,1309],{"class":110,"line":280},[213,1294,1295],{"class":251},"    \"team_south\"",[213,1297,1275],{"class":222},[213,1299,382],{"class":251},[213,1301,629],{"class":283},[213,1303,1282],{"class":251},[213,1305,629],{"class":283},[213,1307,1308],{"class":251}," = 'south'\"",[213,1310,1290],{"class":222},[213,1312,1313,1316,1318,1320,1322,1324,1326,1329],{"class":110,"line":301},[213,1314,1315],{"class":251},"    \"open_applications\"",[213,1317,1275],{"class":222},[213,1319,382],{"class":251},[213,1321,629],{"class":283},[213,1323,429],{"class":251},[213,1325,629],{"class":283},[213,1327,1328],{"class":251}," > 0\"",[213,1330,1290],{"class":222},[213,1332,1333],{"class":110,"line":306},[213,1334,1335],{"class":222},"}\n",[213,1337,1338,1340,1343,1345],{"class":110,"line":317},[213,1339,498],{"class":218},[213,1341,1342],{"class":222}," run, expr ",[213,1344,504],{"class":218},[213,1346,1347],{"class":222}," runs.items():\n",[213,1349,1350,1353,1355],{"class":110,"line":344},[213,1351,1352],{"class":222},"    atlas.setFilterFeatures(",[213,1354,312],{"class":283},[213,1356,255],{"class":222},[213,1358,1359],{"class":110,"line":356},[213,1360,1361],{"class":222},"    atlas.setFilterExpression(expr)\n",[213,1363,1364],{"class":110,"line":387},[213,1365,925],{"class":222},[213,1367,1368,1371,1373,1376,1379,1382,1385],{"class":110,"line":393},[213,1369,1370],{"class":222},"    out_dir ",[213,1372,245],{"class":218},[213,1374,1375],{"class":222}," Path(",[213,1377,1378],{"class":251},"\"\u002Fdata\u002Fmaps\"",[213,1380,1381],{"class":222},") ",[213,1383,1384],{"class":218},"\u002F",[213,1386,1387],{"class":222}," run\n",[213,1389,1391,1394,1398,1400,1402,1405,1408,1410,1412],{"class":110,"line":1390},14,[213,1392,1393],{"class":222},"    out_dir.mkdir(",[213,1395,1397],{"class":1396},"s9osk","parents",[213,1399,245],{"class":218},[213,1401,312],{"class":283},[213,1403,1404],{"class":222},", ",[213,1406,1407],{"class":1396},"exist_ok",[213,1409,245],{"class":218},[213,1411,312],{"class":283},[213,1413,255],{"class":222},[213,1415,1417,1420,1422,1425,1427,1430,1432,1435],{"class":110,"line":1416},15,[213,1418,1419],{"class":222},"    result, error ",[213,1421,245],{"class":218},[213,1423,1424],{"class":222}," QgsLayoutExporter.exportToPdfs(atlas, ",[213,1426,818],{"class":283},[213,1428,1429],{"class":222},"(out_dir ",[213,1431,1384],{"class":218},[213,1433,1434],{"class":251}," \"page.pdf\"",[213,1436,1437],{"class":222},"),\n",[213,1439,1441],{"class":110,"line":1440},16,[213,1442,1443],{"class":222},"                                                   QgsLayoutExporter.PdfExportSettings())\n",[213,1445,1447,1449,1451,1453,1455,1457,1460,1463,1465,1468,1471,1473,1475,1478,1480,1483,1486,1489,1492,1495,1498,1501,1503,1505],{"class":110,"line":1446},17,[213,1448,1018],{"class":283},[213,1450,287],{"class":222},[213,1452,367],{"class":218},[213,1454,382],{"class":251},[213,1456,373],{"class":283},[213,1458,1459],{"class":222},"run",[213,1461,1462],{"class":218},":\u003C20",[213,1464,379],{"class":283},[213,1466,1467],{"class":283}," {",[213,1469,1470],{"class":222},"atlas.count()",[213,1472,1032],{"class":218},[213,1474,379],{"class":283},[213,1476,1477],{"class":251}," pages  ",[213,1479,373],{"class":283},[213,1481,1482],{"class":251},"'ok'",[213,1484,1485],{"class":218}," if",[213,1487,1488],{"class":222}," result ",[213,1490,1491],{"class":218},"==",[213,1493,1494],{"class":222}," QgsLayoutExporter.Success ",[213,1496,1497],{"class":218},"else",[213,1499,1500],{"class":222}," error",[213,1502,379],{"class":283},[213,1504,382],{"class":251},[213,1506,255],{"class":222},[14,1508,1509,1511],{},[407,1510,409],{}," Each run sets its filter, refreshes the feature list and exports into its own folder, using the filename expression for individual files. The layout is never duplicated, so a design change — a new logo, a corrected legend — applies to every audience's maps at once. Restore the original filter afterwards if the project will be saved, or the next person to open the atlas sees only the last subset.",[176,1513,1515],{"id":1514},"qgis-version-compatibility","QGIS version compatibility",[14,1517,1518,1521,1522,1525,1526,1529,1530,194],{},[210,1519,1520],{},"QgsLayoutAtlas"," filter, sort, page name and filename settings work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. ",[210,1523,1524],{},"QgsLayoutExporter.exportToPdfs"," for atlases has existed throughout QGIS 3. On QGIS 4, ",[210,1527,1528],{},"QgsLayoutItemMap.Auto"," is ",[210,1531,1532],{},"QgsLayoutItemMap.AtlasScalingMode.Auto",[176,1534,1536],{"id":1535},"troubleshooting","Troubleshooting",[181,1538,1539,1550,1556,1564],{},[184,1540,1541,1544,1545,1547,1548,194],{},[407,1542,1543],{},"The atlas has zero pages."," The filter expression is invalid or excludes everything; check the return value of ",[210,1546,417],{}," and ",[210,1549,425],{},[184,1551,1552,1555],{},[407,1553,1554],{},"Order looks random."," Sorting was not enabled, or numbers are compared as text without padding.",[184,1557,1558,410,1561,1563],{},[407,1559,1560],{},"The coverage boundaries still show.",[210,1562,1121],{}," is off, or the same data is loaded as a second layer.",[184,1565,1566,1569],{},[407,1567,1568],{},"The next user sees a subset."," A filter from the last run was saved in the project; reset it.",[176,1571,1573],{"id":1572},"conclusion","Conclusion",[14,1575,1576],{},"Filter atlas pages with a validated expression and enable filtering explicitly, sort with an expression that encodes all keys with padding, name pages from stable codes for file names, hide helper coverage layers, and run one layout with different filters for different audiences instead of copying it.",[176,1578,1580],{"id":1579},"frequently-asked-questions","Frequently Asked Questions",[14,1582,1583,1586,1587,1590,1591,1594],{},[407,1584,1585],{},"Can I filter on attributes of another layer?","\nYes, with ",[210,1588,1589],{},"aggregate()"," or ",[210,1592,1593],{},"get_feature()"," in the filter expression, at some performance cost.",[14,1596,1597,1600,1601,194],{},[407,1598,1599],{},"Can the atlas skip features without geometry?","\nFeatures without geometry cannot be framed; filter them out with ",[210,1602,1603],{},"$geometry IS NOT NULL",[14,1605,1606,1609,1610,1613],{},[407,1607,1608],{},"Can pages be ordered by area?","\nSort by ",[210,1611,1612],{},"$area"," descending to put the largest features first.",[14,1615,1616,1619,1620,1623,1624,1627],{},[407,1617,1618],{},"How do I print only pages changed since the last run?","\nKeep an ",[210,1621,1622],{},"edited_on"," field on the coverage layer and filter with ",[210,1625,1626],{},"\"edited_on\" >= to_date('2026-09-01')",", using the date of the previous export.",[14,1629,1630,1633],{},[407,1631,1632],{},"Does filtering change the coverage layer?","\nNo. The filter applies only to the atlas.",[176,1635,1637],{"id":1636},"related","Related",[181,1639,1640,1645,1650,1655,1661],{},[184,1641,1642,1644],{},[30,1643,33],{"href":32}," — the guide this recipe belongs to",[184,1646,1647],{},[30,1648,1649],{"href":192},"Configure an Atlas Coverage Layer in PyQGIS",[184,1651,1652],{},[30,1653,1654],{"href":757},"Export Atlas Pages to Individual PDFs in PyQGIS",[184,1656,1657],{},[30,1658,1660],{"href":1659},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fatlas-expressions-and-dynamic-text-pyqgis\u002F","Atlas Expressions and Dynamic Text in PyQGIS",[184,1662,1663],{},[30,1664,1665],{"href":1129},"Highlight the Atlas Feature in an Overview Map in PyQGIS",[1667,1668,1669],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":208,"searchDepth":232,"depth":232,"links":1671},[1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685],{"id":178,"depth":232,"text":179},{"id":197,"depth":232,"text":198},{"id":437,"depth":232,"text":438},{"id":610,"depth":232,"text":611},{"id":673,"depth":232,"text":674},{"id":761,"depth":232,"text":762},{"id":891,"depth":232,"text":892},{"id":1056,"depth":232,"text":1057},{"id":1133,"depth":232,"text":1134},{"id":1514,"depth":232,"text":1515},{"id":1535,"depth":232,"text":1536},{"id":1572,"depth":232,"text":1573},{"id":1579,"depth":232,"text":1580},{"id":1636,"depth":232,"text":1637},"Control which features become atlas pages and in what order — filter expressions, sorting by a field or expression, hiding the coverage layer, page names for file naming, and splitting one atlas into several runs for different audiences.","md",{"slug":1689,"type":1690,"breadcrumb":1691,"datePublished":1692,"dateModified":1692},"filter-and-sort-atlas-features-pyqgis","article","Filter and Sort Atlas Features","2026-10-02","\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Ffilter-and-sort-atlas-features-pyqgis",{"title":5,"description":1686},"spatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Ffilter-and-sort-atlas-features-pyqgis\u002Findex","KO11tOFhuENhTDQUZHjFHR9kIXad2t5UhwPt_0uj4cc",1790966260627]