[{"data":1,"prerenderedAt":1620},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fset-scale-based-visibility-pyqgis":3},{"id":4,"title":5,"body":6,"description":1609,"extension":1610,"meta":1611,"navigation":232,"path":1616,"seo":1617,"stem":1618,"__hash__":1619},"docs\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fset-scale-based-visibility-pyqgis\u002Findex.md","Set Scale-Based Visibility in PyQGIS",{"type":7,"value":8,"toc":1594},"minimark",[9,13,17,26,170,175,192,196,199,313,332,338,342,345,495,510,514,517,641,649,731,735,738,811,828,832,835,1140,1154,1157,1161,1169,1215,1227,1231,1234,1351,1365,1369,1375,1456,1460,1509,1513,1519,1523,1529,1540,1546,1556,1560,1590],[10,11,5],"h1",{"id":12},"set-scale-based-visibility-in-pyqgis",[14,15,16],"p",{},"A map that shows every layer at every zoom is unreadable at both ends: a wall of parcel boundaries at national scale, and a single coastline when you are looking at one street. Scale-based visibility fixes that by giving each layer a range of scales it is allowed to draw in — and because the layer is skipped entirely outside that range, it makes the map faster as well as clearer.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002F","Map Themes & Layer Visibility in PyQGIS",". It covers setting the range from Python, the minimum\u002Fmaximum naming that confuses everybody exactly once, applying ranges per rule instead of per layer, restricting labels independently of the features, and checking what will be visible before an export.",[14,27,28],{},[29,30,35,39,43,50,59,67,73,77,81,85,90,96,104,110,114,120,125,129,134,139,143,150,155,160,163,166],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 312","img","A scale axis from zoomed out to zoomed in, with bars showing the scale range each layer is allowed to draw in, so different layers appear at different zoom levels","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"A scale ladder for a multi-scale map",[40,41,42],"desc",{},"A horizontal axis runs from one to two million on the left, zoomed out, to one to one thousand on the right, zoomed in. Boundaries draw across the whole range. Roads appear from one to two hundred and fifty thousand inwards. Buildings and parcels appear only at the closest scales, so each zoom level shows an appropriate amount of detail.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","312","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Each layer earns its place at some scales and not others",[60,61],"line",{"x1":62,"y1":63,"x2":64,"y2":63,"stroke":65,"style":66},"150","264","732","#2f3b35","stroke-width:2",[51,68,72],{"x":62,"y":69,"style":70,"fill":71,"textAnchor":57},"288","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#59645f","1:2 000 000",[51,74,76],{"x":75,"y":69,"style":70,"fill":71,"textAnchor":57},"340","1:250 000",[51,78,80],{"x":79,"y":69,"style":70,"fill":71,"textAnchor":57},"530","1:25 000",[51,82,84],{"x":83,"y":69,"style":70,"fill":71,"textAnchor":57},"716","1:1 000",[51,86,89],{"x":87,"y":88,"style":70,"fill":71,"textAnchor":57},"440","308","zoomed out ← → zoomed in",[51,91,95],{"x":92,"y":93,"style":94,"fill":65},"24","76","font-size:11px;font-family:sans-serif","boundaries",[44,97],{"x":62,"y":98,"width":99,"height":92,"rx":100,"fill":101,"stroke":102,"style":103},"62","582","5","#eff3ff","#2563eb","stroke-width:1.8",[51,105,109],{"x":106,"y":107,"style":108,"fill":65,"textAnchor":57},"441","79","text-anchor:middle;font-size:10px;font-family:sans-serif","always drawn",[51,111,113],{"x":92,"y":112,"style":94,"fill":65},"120","roads",[44,115],{"x":75,"y":116,"width":117,"height":92,"rx":100,"fill":118,"stroke":119,"style":103},"106","392","#eef7f4","#0f766e",[51,121,124],{"x":122,"y":123,"style":108,"fill":65,"textAnchor":57},"536","123","minimumScale = 250 000",[51,126,128],{"x":92,"y":127,"style":94,"fill":65},"164","buildings",[44,130],{"x":79,"y":62,"width":131,"height":92,"rx":100,"fill":132,"stroke":133,"style":103},"202","#edf8e9","#15803d",[51,135,138],{"x":136,"y":137,"style":108,"fill":65,"textAnchor":57},"631","167","minimumScale = 25 000",[51,140,142],{"x":92,"y":141,"style":94,"fill":65},"208","parcel labels",[44,144],{"x":145,"y":146,"width":147,"height":92,"rx":100,"fill":148,"stroke":149,"style":103},"600","194","132","#fdf2e2","#b45309",[51,151,154],{"x":152,"y":153,"style":108,"fill":65,"textAnchor":57},"666","211","labels only",[60,156],{"x1":75,"y1":157,"x2":75,"y2":158,"stroke":71,"style":159},"94","258","stroke-width:1;stroke-dasharray:4 4",[60,161],{"x1":79,"y1":162,"x2":79,"y2":158,"stroke":71,"style":159},"138",[60,164],{"x1":145,"y1":165,"x2":145,"y2":158,"stroke":71,"style":159},"182",[51,167,169],{"x":53,"y":168,"style":70,"fill":71,"textAnchor":57},"242","a layer outside its range is skipped, not drawn and discarded",[171,172,174],"h2",{"id":173},"prerequisites","Prerequisites",[176,177,178,186,189],"ul",{},[179,180,181,185],"li",{},[182,183,184],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[179,187,188],{},"A project with layers at different levels of detail — a national boundary set and a parcel layer is the classic pairing.",[179,190,191],{},"A rough idea of the scales your map will actually be read at, since the ranges are only as good as that judgement.",[171,193,195],{"id":194},"set-a-range-on-a-layer","Set a range on a layer",[14,197,198],{},"Three calls, and only one of them is obvious.",[200,201,206],"pre",{"className":202,"code":203,"language":204,"meta":205,"style":205},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nlayer.setScaleBasedVisibility(True)\nlayer.setMinimumScale(10000)     # hidden when zoomed OUT past 1:10 000\nlayer.setMaximumScale(500)       # hidden when zoomed IN past 1:500\nlayer.triggerRepaint()\n","python","",[207,208,209,227,234,259,264,276,292,307],"code",{"__ignoreMap":205},[210,211,213,217,221,224],"span",{"class":60,"line":212},1,[210,214,216],{"class":215},"snl16","from",[210,218,220],{"class":219},"s95oV"," qgis.core ",[210,222,223],{"class":215},"import",[210,225,226],{"class":219}," QgsProject\n",[210,228,230],{"class":60,"line":229},2,[210,231,233],{"emptyLinePlaceholder":232},true,"\n",[210,235,237,240,243,246,250,253,256],{"class":60,"line":236},3,[210,238,239],{"class":219},"layer ",[210,241,242],{"class":215},"=",[210,244,245],{"class":219}," QgsProject.instance().mapLayersByName(",[210,247,249],{"class":248},"sU2Wk","\"parcels\"",[210,251,252],{"class":219},")[",[210,254,46],{"class":255},"sDLfK",[210,257,258],{"class":219},"]\n",[210,260,262],{"class":60,"line":261},4,[210,263,233],{"emptyLinePlaceholder":232},[210,265,267,270,273],{"class":60,"line":266},5,[210,268,269],{"class":219},"layer.setScaleBasedVisibility(",[210,271,272],{"class":255},"True",[210,274,275],{"class":219},")\n",[210,277,279,282,285,288],{"class":60,"line":278},6,[210,280,281],{"class":219},"layer.setMinimumScale(",[210,283,284],{"class":255},"10000",[210,286,287],{"class":219},")     ",[210,289,291],{"class":290},"sjoCn","# hidden when zoomed OUT past 1:10 000\n",[210,293,295,298,301,304],{"class":60,"line":294},7,[210,296,297],{"class":219},"layer.setMaximumScale(",[210,299,300],{"class":255},"500",[210,302,303],{"class":219},")       ",[210,305,306],{"class":290},"# hidden when zoomed IN past 1:500\n",[210,308,310],{"class":60,"line":309},8,[210,311,312],{"class":219},"layer.triggerRepaint()\n",[14,314,315,318,319,322,323,327,328,331],{},[182,316,317],{},"Breakdown:"," ",[207,320,321],{},"setScaleBasedVisibility(True)"," is the master switch; set the scales without it and nothing happens, which is the most common reason this feature \"does not work\". The naming is the real hazard. A scale of 1:10 000 is ",[324,325,326],"em",{},"numerically larger"," than 1:500 but shows ",[324,329,330],{},"less"," detail, and QGIS names the range by the denominator: minimum scale is the largest denominator, i.e. the zoomed-out limit. Read the two lines as \"not drawn beyond 1:10 000 zoomed out\" and \"not drawn beyond 1:500 zoomed in\" and the ambiguity disappears.",[14,333,334,335,337],{},"Passing ",[207,336,46],{}," to either setter removes that end of the range, so a layer can have a zoomed-out limit and no zoomed-in limit. That asymmetric form is the common one: most detail layers want to disappear when the reader zooms out and to keep drawing however far they zoom in.",[171,339,341],{"id":340},"read-the-range-back-safely","Read the range back safely",[14,343,344],{},"Reporting the current settings is where the naming bites a second time.",[200,346,348],{"className":202,"code":347,"language":204,"meta":205,"style":205},"def describe_range(layer):\n    if not layer.hasScaleBasedVisibility():\n        return \"always drawn\"\n    out = layer.minimumScale()\n    into = layer.maximumScale()\n    parts = []\n    if out:\n        parts.append(f\"hidden above 1:{out:,.0f}\")\n    if into:\n        parts.append(f\"hidden below 1:{into:,.0f}\")\n    return \"; \".join(parts) or \"range enabled but unbounded\"\n",[207,349,350,362,373,381,391,401,411,418,446,454,477],{"__ignoreMap":205},[210,351,352,355,359],{"class":60,"line":212},[210,353,354],{"class":215},"def",[210,356,358],{"class":357},"svObZ"," describe_range",[210,360,361],{"class":219},"(layer):\n",[210,363,364,367,370],{"class":60,"line":229},[210,365,366],{"class":215},"    if",[210,368,369],{"class":215}," not",[210,371,372],{"class":219}," layer.hasScaleBasedVisibility():\n",[210,374,375,378],{"class":60,"line":236},[210,376,377],{"class":215},"        return",[210,379,380],{"class":248}," \"always drawn\"\n",[210,382,383,386,388],{"class":60,"line":261},[210,384,385],{"class":219},"    out ",[210,387,242],{"class":215},[210,389,390],{"class":219}," layer.minimumScale()\n",[210,392,393,396,398],{"class":60,"line":266},[210,394,395],{"class":219},"    into ",[210,397,242],{"class":215},[210,399,400],{"class":219}," layer.maximumScale()\n",[210,402,403,406,408],{"class":60,"line":278},[210,404,405],{"class":219},"    parts ",[210,407,242],{"class":215},[210,409,410],{"class":219}," []\n",[210,412,413,415],{"class":60,"line":294},[210,414,366],{"class":215},[210,416,417],{"class":219}," out:\n",[210,419,420,423,426,429,432,435,438,441,444],{"class":60,"line":309},[210,421,422],{"class":219},"        parts.append(",[210,424,425],{"class":215},"f",[210,427,428],{"class":248},"\"hidden above 1:",[210,430,431],{"class":255},"{",[210,433,434],{"class":219},"out",[210,436,437],{"class":215},":,.0f",[210,439,440],{"class":255},"}",[210,442,443],{"class":248},"\"",[210,445,275],{"class":219},[210,447,449,451],{"class":60,"line":448},9,[210,450,366],{"class":215},[210,452,453],{"class":219}," into:\n",[210,455,457,459,461,464,466,469,471,473,475],{"class":60,"line":456},10,[210,458,422],{"class":219},[210,460,425],{"class":215},[210,462,463],{"class":248},"\"hidden below 1:",[210,465,431],{"class":255},[210,467,468],{"class":219},"into",[210,470,437],{"class":215},[210,472,440],{"class":255},[210,474,443],{"class":248},[210,476,275],{"class":219},[210,478,480,483,486,489,492],{"class":60,"line":479},11,[210,481,482],{"class":215},"    return",[210,484,485],{"class":248}," \"; \"",[210,487,488],{"class":219},".join(parts) ",[210,490,491],{"class":215},"or",[210,493,494],{"class":248}," \"range enabled but unbounded\"\n",[14,496,497,318,499,502,503,506,507,509],{},[182,498,317],{},[207,500,501],{},"hasScaleBasedVisibility()"," is the getter matching ",[207,504,505],{},"setScaleBasedVisibility()",", and checking it first avoids reporting a range that is stored but inactive. Testing each bound for truthiness handles the ",[207,508,46],{},"-means-unbounded convention without a separate sentinel check. Producing a sentence rather than two numbers is worth the extra lines, because a table of raw minimum and maximum values is misread by almost everyone reviewing a project.",[171,511,513],{"id":512},"per-rule-ranges-when-one-layer-needs-several","Per-rule ranges, when one layer needs several",[14,515,516],{},"A rule-based renderer attaches a scale range to each rule, so one layer can change how it draws rather than whether it draws.",[200,518,520],{"className":202,"code":519,"language":204,"meta":205,"style":205},"from qgis.core import QgsRuleBasedRenderer, QgsSymbol\n\nrenderer = layer.renderer()\nroot_rule = renderer.rootRule()\n\ndetailed = root_rule.children()[0]\ndetailed.setMinimumScale(25000)\ndetailed.setMaximumScale(0)\n\nsimplified = root_rule.children()[1]\nsimplified.setMinimumScale(0)\nsimplified.setMaximumScale(25000)\n\nlayer.triggerRepaint()\n",[207,521,522,533,537,547,557,561,575,585,594,598,612,621,631,636],{"__ignoreMap":205},[210,523,524,526,528,530],{"class":60,"line":212},[210,525,216],{"class":215},[210,527,220],{"class":219},[210,529,223],{"class":215},[210,531,532],{"class":219}," QgsRuleBasedRenderer, QgsSymbol\n",[210,534,535],{"class":60,"line":229},[210,536,233],{"emptyLinePlaceholder":232},[210,538,539,542,544],{"class":60,"line":236},[210,540,541],{"class":219},"renderer ",[210,543,242],{"class":215},[210,545,546],{"class":219}," layer.renderer()\n",[210,548,549,552,554],{"class":60,"line":261},[210,550,551],{"class":219},"root_rule ",[210,553,242],{"class":215},[210,555,556],{"class":219}," renderer.rootRule()\n",[210,558,559],{"class":60,"line":266},[210,560,233],{"emptyLinePlaceholder":232},[210,562,563,566,568,571,573],{"class":60,"line":278},[210,564,565],{"class":219},"detailed ",[210,567,242],{"class":215},[210,569,570],{"class":219}," root_rule.children()[",[210,572,46],{"class":255},[210,574,258],{"class":219},[210,576,577,580,583],{"class":60,"line":294},[210,578,579],{"class":219},"detailed.setMinimumScale(",[210,581,582],{"class":255},"25000",[210,584,275],{"class":219},[210,586,587,590,592],{"class":60,"line":309},[210,588,589],{"class":219},"detailed.setMaximumScale(",[210,591,46],{"class":255},[210,593,275],{"class":219},[210,595,596],{"class":60,"line":448},[210,597,233],{"emptyLinePlaceholder":232},[210,599,600,603,605,607,610],{"class":60,"line":456},[210,601,602],{"class":219},"simplified ",[210,604,242],{"class":215},[210,606,570],{"class":219},[210,608,609],{"class":255},"1",[210,611,258],{"class":219},[210,613,614,617,619],{"class":60,"line":479},[210,615,616],{"class":219},"simplified.setMinimumScale(",[210,618,46],{"class":255},[210,620,275],{"class":219},[210,622,624,627,629],{"class":60,"line":623},12,[210,625,626],{"class":219},"simplified.setMaximumScale(",[210,628,582],{"class":255},[210,630,275],{"class":219},[210,632,634],{"class":60,"line":633},13,[210,635,233],{"emptyLinePlaceholder":232},[210,637,639],{"class":60,"line":638},14,[210,640,312],{"class":219},[14,642,643,645,646,648],{},[182,644,317],{}," Rules use the same denominator convention as layers, so the two rules here hand over at 1:25 000 — the detailed symbol takes everything zoomed in from there, the simplified one everything zoomed out. Setting the far end to ",[207,647,46],{}," on each rule leaves it unbounded, giving a clean handover with no gap and no overlap. This is how a road layer draws full casings in town and plain lines at regional scale from a single layer, which keeps labels, attributes and edits in one place instead of two.",[14,650,651],{},[29,652,655,658,661,664,667,672,679,684,690,696,701,703,707,710,716,720,724,728],{"viewBox":653,"role":32,"ariaLabel":654,"xmlns":34},"0 0 760 280","Two rules of a rule-based renderer with complementary scale ranges, handing over at one to twenty five thousand so the layer changes appearance rather than disappearing",[36,656,657],{},"Two rules, one handover point",[40,659,660],{},"The simplified rule covers scales from fully zoomed out to one to twenty five thousand. The detailed rule covers one to twenty five thousand and closer. Setting the far bound of each rule to zero leaves it unbounded, so the two ranges meet exactly with no gap where nothing draws.",[44,662],{"x":46,"y":46,"width":47,"height":663,"fill":49},"280",[51,665,666],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"The layer never disappears — its symbol changes",[60,668],{"x1":669,"y1":670,"x2":671,"y2":670,"stroke":65,"style":66},"80","220","700",[60,673],{"x1":674,"y1":675,"x2":674,"y2":676,"stroke":677,"style":678},"390","72","232","#b91c1c","stroke-width:2;stroke-dasharray:5 4",[51,680,683],{"x":674,"y":681,"style":682,"fill":677,"textAnchor":57},"252","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","handover at 1:25 000",[44,685],{"x":669,"y":686,"width":687,"height":688,"rx":689,"fill":101,"stroke":102,"style":66},"88","310","46","6",[51,691,695],{"x":692,"y":693,"style":694,"fill":102,"textAnchor":57},"235","108","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","rule: simplified",[51,697,700],{"x":692,"y":698,"style":699,"fill":65,"textAnchor":57},"126","text-anchor:middle;font-size:10px;font-family:monospace","min 0 · max 25 000",[44,702],{"x":674,"y":686,"width":687,"height":688,"rx":689,"fill":132,"stroke":133,"style":66},[51,704,706],{"x":705,"y":693,"style":694,"fill":133,"textAnchor":57},"545","rule: detailed",[51,708,709],{"x":705,"y":698,"style":699,"fill":65,"textAnchor":57},"min 25 000 · max 0",[711,712],"path",{"d":713,"fill":714,"stroke":102,"style":715},"M110 176 L200 164 L300 180 L370 170","none","stroke-width:2.5",[711,717],{"d":718,"fill":714,"stroke":56,"style":719},"M410 176 L500 164 L600 180 L670 170","stroke-width:9;stroke-linecap:round",[711,721],{"d":718,"fill":714,"stroke":722,"style":723},"#f59e0b","stroke-width:5;stroke-linecap:round",[51,725,727],{"x":692,"y":726,"style":70,"fill":65,"textAnchor":57},"204","a plain line",[51,729,730],{"x":705,"y":726,"style":70,"fill":65,"textAnchor":57},"casing and fill",[171,732,734],{"id":733},"labels-can-have-their-own-limits","Labels can have their own limits",[14,736,737],{},"Features and their labels rarely want the same range. Parcels can be useful long before their reference numbers are legible.",[200,739,741],{"className":202,"code":740,"language":204,"meta":205,"style":205},"settings = layer.labeling().settings()\nsettings.scaleVisibility = True\nsettings.minimumScale = 2500\nsettings.maximumScale = 0\n\nlabeling = layer.labeling().clone()\nlabeling.setSettings(settings)\nlayer.setLabeling(labeling)\nlayer.triggerRepaint()\n",[207,742,743,753,763,773,783,787,797,802,807],{"__ignoreMap":205},[210,744,745,748,750],{"class":60,"line":212},[210,746,747],{"class":219},"settings ",[210,749,242],{"class":215},[210,751,752],{"class":219}," layer.labeling().settings()\n",[210,754,755,758,760],{"class":60,"line":229},[210,756,757],{"class":219},"settings.scaleVisibility ",[210,759,242],{"class":215},[210,761,762],{"class":255}," True\n",[210,764,765,768,770],{"class":60,"line":236},[210,766,767],{"class":219},"settings.minimumScale ",[210,769,242],{"class":215},[210,771,772],{"class":255}," 2500\n",[210,774,775,778,780],{"class":60,"line":261},[210,776,777],{"class":219},"settings.maximumScale ",[210,779,242],{"class":215},[210,781,782],{"class":255}," 0\n",[210,784,785],{"class":60,"line":266},[210,786,233],{"emptyLinePlaceholder":232},[210,788,789,792,794],{"class":60,"line":278},[210,790,791],{"class":219},"labeling ",[210,793,242],{"class":215},[210,795,796],{"class":219}," layer.labeling().clone()\n",[210,798,799],{"class":60,"line":294},[210,800,801],{"class":219},"labeling.setSettings(settings)\n",[210,803,804],{"class":60,"line":309},[210,805,806],{"class":219},"layer.setLabeling(labeling)\n",[210,808,809],{"class":60,"line":448},[210,810,312],{"class":219},[14,812,813,318,815,818,819,822,823,827],{},[182,814,317],{},[207,816,817],{},"QgsPalLayerSettings"," carries its own ",[207,820,821],{},"scaleVisibility"," flag and its own pair of bounds, entirely independent of the layer's. Cloning the labeling object before setting it back is the safe pattern: settings retrieved from a live labeling object are a copy in some versions and a reference in others, and cloning removes the doubt. The result is a layer whose polygons appear at 1:25 000 and whose labels wait until 1:2 500, which is the arrangement almost every cadastral map ends up with. More on the surrounding settings in ",[21,824,826],{"href":825},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcontrol-label-placement-and-collisions-pyqgis\u002F","controlling label placement",".",[171,829,831],{"id":830},"building-a-scale-ladder-for-a-whole-project","Building a scale ladder for a whole project",[14,833,834],{},"Applied one layer at a time, scale ranges drift out of alignment: two detail layers that ought to appear together end up with limits a factor of two apart because they were set months apart. Declaring the ladder once fixes that.",[200,836,838],{"className":202,"code":837,"language":204,"meta":205,"style":205},"LADDER = {\n    \"overview\":  (None,      None),      # always drawn\n    \"regional\":  (1_000_000, None),\n    \"local\":     (100_000,   None),\n    \"detail\":    (10_000,    None),\n    \"survey\":    (2_000,     None),\n}\n\nASSIGNMENT = {\n    \"boundaries\": \"overview\",\n    \"roads\": \"regional\",\n    \"buildings\": \"local\",\n    \"parcels\": \"detail\",\n    \"utility points\": \"survey\",\n}\n\nfor layer_name, band in ASSIGNMENT.items():\n    layer = QgsProject.instance().mapLayersByName(layer_name)[0]\n    out, into = LADDER[band]\n    layer.setScaleBasedVisibility(out is not None or into is not None)\n    layer.setMinimumScale(out or 0)\n    layer.setMaximumScale(into or 0)\n    layer.triggerRepaint()\n",[207,839,840,851,873,891,909,927,944,949,953,962,976,988,1000,1012,1024,1029,1034,1052,1067,1081,1109,1122,1134],{"__ignoreMap":205},[210,841,842,845,848],{"class":60,"line":212},[210,843,844],{"class":255},"LADDER",[210,846,847],{"class":215}," =",[210,849,850],{"class":219}," {\n",[210,852,853,856,859,862,865,867,870],{"class":60,"line":229},[210,854,855],{"class":248},"    \"overview\"",[210,857,858],{"class":219},":  (",[210,860,861],{"class":255},"None",[210,863,864],{"class":219},",      ",[210,866,861],{"class":255},[210,868,869],{"class":219},"),      ",[210,871,872],{"class":290},"# always drawn\n",[210,874,875,878,880,883,886,888],{"class":60,"line":236},[210,876,877],{"class":248},"    \"regional\"",[210,879,858],{"class":219},[210,881,882],{"class":255},"1_000_000",[210,884,885],{"class":219},", ",[210,887,861],{"class":255},[210,889,890],{"class":219},"),\n",[210,892,893,896,899,902,905,907],{"class":60,"line":261},[210,894,895],{"class":248},"    \"local\"",[210,897,898],{"class":219},":     (",[210,900,901],{"class":255},"100_000",[210,903,904],{"class":219},",   ",[210,906,861],{"class":255},[210,908,890],{"class":219},[210,910,911,914,917,920,923,925],{"class":60,"line":266},[210,912,913],{"class":248},"    \"detail\"",[210,915,916],{"class":219},":    (",[210,918,919],{"class":255},"10_000",[210,921,922],{"class":219},",    ",[210,924,861],{"class":255},[210,926,890],{"class":219},[210,928,929,932,934,937,940,942],{"class":60,"line":278},[210,930,931],{"class":248},"    \"survey\"",[210,933,916],{"class":219},[210,935,936],{"class":255},"2_000",[210,938,939],{"class":219},",     ",[210,941,861],{"class":255},[210,943,890],{"class":219},[210,945,946],{"class":60,"line":294},[210,947,948],{"class":219},"}\n",[210,950,951],{"class":60,"line":309},[210,952,233],{"emptyLinePlaceholder":232},[210,954,955,958,960],{"class":60,"line":448},[210,956,957],{"class":255},"ASSIGNMENT",[210,959,847],{"class":215},[210,961,850],{"class":219},[210,963,964,967,970,973],{"class":60,"line":456},[210,965,966],{"class":248},"    \"boundaries\"",[210,968,969],{"class":219},": ",[210,971,972],{"class":248},"\"overview\"",[210,974,975],{"class":219},",\n",[210,977,978,981,983,986],{"class":60,"line":479},[210,979,980],{"class":248},"    \"roads\"",[210,982,969],{"class":219},[210,984,985],{"class":248},"\"regional\"",[210,987,975],{"class":219},[210,989,990,993,995,998],{"class":60,"line":623},[210,991,992],{"class":248},"    \"buildings\"",[210,994,969],{"class":219},[210,996,997],{"class":248},"\"local\"",[210,999,975],{"class":219},[210,1001,1002,1005,1007,1010],{"class":60,"line":633},[210,1003,1004],{"class":248},"    \"parcels\"",[210,1006,969],{"class":219},[210,1008,1009],{"class":248},"\"detail\"",[210,1011,975],{"class":219},[210,1013,1014,1017,1019,1022],{"class":60,"line":638},[210,1015,1016],{"class":248},"    \"utility points\"",[210,1018,969],{"class":219},[210,1020,1021],{"class":248},"\"survey\"",[210,1023,975],{"class":219},[210,1025,1027],{"class":60,"line":1026},15,[210,1028,948],{"class":219},[210,1030,1032],{"class":60,"line":1031},16,[210,1033,233],{"emptyLinePlaceholder":232},[210,1035,1037,1040,1043,1046,1049],{"class":60,"line":1036},17,[210,1038,1039],{"class":215},"for",[210,1041,1042],{"class":219}," layer_name, band ",[210,1044,1045],{"class":215},"in",[210,1047,1048],{"class":255}," ASSIGNMENT",[210,1050,1051],{"class":219},".items():\n",[210,1053,1055,1058,1060,1063,1065],{"class":60,"line":1054},18,[210,1056,1057],{"class":219},"    layer ",[210,1059,242],{"class":215},[210,1061,1062],{"class":219}," QgsProject.instance().mapLayersByName(layer_name)[",[210,1064,46],{"class":255},[210,1066,258],{"class":219},[210,1068,1070,1073,1075,1078],{"class":60,"line":1069},19,[210,1071,1072],{"class":219},"    out, into ",[210,1074,242],{"class":215},[210,1076,1077],{"class":255}," LADDER",[210,1079,1080],{"class":219},"[band]\n",[210,1082,1084,1087,1090,1092,1095,1098,1101,1103,1105,1107],{"class":60,"line":1083},20,[210,1085,1086],{"class":219},"    layer.setScaleBasedVisibility(out ",[210,1088,1089],{"class":215},"is",[210,1091,369],{"class":215},[210,1093,1094],{"class":255}," None",[210,1096,1097],{"class":215}," or",[210,1099,1100],{"class":219}," into ",[210,1102,1089],{"class":215},[210,1104,369],{"class":215},[210,1106,1094],{"class":255},[210,1108,275],{"class":219},[210,1110,1112,1115,1117,1120],{"class":60,"line":1111},21,[210,1113,1114],{"class":219},"    layer.setMinimumScale(out ",[210,1116,491],{"class":215},[210,1118,1119],{"class":255}," 0",[210,1121,275],{"class":219},[210,1123,1125,1128,1130,1132],{"class":60,"line":1124},22,[210,1126,1127],{"class":219},"    layer.setMaximumScale(into ",[210,1129,491],{"class":215},[210,1131,1119],{"class":255},[210,1133,275],{"class":219},[210,1135,1137],{"class":60,"line":1136},23,[210,1138,1139],{"class":219},"    layer.triggerRepaint()\n",[14,1141,1142,1144,1145,1147,1148,1150,1151,1153],{},[182,1143,317],{}," Naming the bands rather than the numbers is what makes the arrangement reviewable — a colleague can disagree with \"parcels belong in the detail band\" far more usefully than with \"parcels have minimum scale 10 000\". Using ",[207,1146,861],{}," in the table and converting to ",[207,1149,46],{}," at the boundary keeps the QGIS convention out of the declaration, so the table reads as intent rather than as API detail. Adding a layer later is one line in ",[207,1152,957],{},", and the ladder stays consistent by construction.",[14,1155,1156],{},"A ladder also documents the map's design. Five bands roughly a factor of ten apart matches how people actually zoom, and a project whose layers all sit in two bands is usually a project where scale ranges were added reactively to fix one slow screen rather than designed.",[171,1158,1160],{"id":1159},"scale-ranges-inside-a-layout","Scale ranges inside a layout",[14,1162,1163,1164,1168],{},"A ",[21,1165,1167],{"href":1166},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-map-item-and-set-extent-pyqgis\u002F","layout map item"," computes its own scale from its extent and its printed size, and that number is often nothing like the canvas scale — an A3 map of a whole county might be 1:120 000 while you were working at 1:5 000.",[200,1170,1172],{"className":202,"code":1171,"language":204,"meta":205,"style":205},"map_item = layout.itemById(\"main map\")\nprint(f\"the layout map will render at 1:{map_item.scale():,.0f}\")\n",[207,1173,1174,1189],{"__ignoreMap":205},[210,1175,1176,1179,1181,1184,1187],{"class":60,"line":212},[210,1177,1178],{"class":219},"map_item ",[210,1180,242],{"class":215},[210,1182,1183],{"class":219}," layout.itemById(",[210,1185,1186],{"class":248},"\"main map\"",[210,1188,275],{"class":219},[210,1190,1191,1194,1197,1199,1202,1204,1207,1209,1211,1213],{"class":60,"line":229},[210,1192,1193],{"class":255},"print",[210,1195,1196],{"class":219},"(",[210,1198,425],{"class":215},[210,1200,1201],{"class":248},"\"the layout map will render at 1:",[210,1203,431],{"class":255},[210,1205,1206],{"class":219},"map_item.scale()",[210,1208,437],{"class":215},[210,1210,440],{"class":255},[210,1212,443],{"class":248},[210,1214,275],{"class":219},[14,1216,1217,318,1219,1222,1223,1226],{},[182,1218,317],{},[207,1220,1221],{},"scale()"," returns the denominator the item will actually use, so pairing it with the ",[207,1224,1225],{},"visible_at()"," helper above answers \"what will be on this page\" before anything is exported. The item can also be told to ignore layer ranges entirely, which is occasionally right for an inset map that must show detail at a scale where the layer would normally be hidden — but it is worth doing deliberately rather than discovering later that one page follows different rules from the rest.",[171,1228,1230],{"id":1229},"check-before-exporting","Check before exporting",[14,1232,1233],{},"An export at a scale you did not test is the usual way a scale range produces a blank map.",[200,1235,1237],{"className":202,"code":1236,"language":204,"meta":205,"style":205},"def visible_at(layers, scale):\n    return [\n        layer.name() for layer in layers\n        if not layer.hasScaleBasedVisibility()\n        or ((not layer.minimumScale() or scale \u003C= layer.minimumScale())\n            and (not layer.maximumScale() or scale >= layer.maximumScale()))\n    ]\n\nprint(visible_at(QgsProject.instance().mapLayers().values(), 50000))\n",[207,1238,1239,1249,1256,1271,1281,1306,1329,1334,1338],{"__ignoreMap":205},[210,1240,1241,1243,1246],{"class":60,"line":212},[210,1242,354],{"class":215},[210,1244,1245],{"class":357}," visible_at",[210,1247,1248],{"class":219},"(layers, scale):\n",[210,1250,1251,1253],{"class":60,"line":229},[210,1252,482],{"class":215},[210,1254,1255],{"class":219}," [\n",[210,1257,1258,1261,1263,1266,1268],{"class":60,"line":236},[210,1259,1260],{"class":219},"        layer.name() ",[210,1262,1039],{"class":215},[210,1264,1265],{"class":219}," layer ",[210,1267,1045],{"class":215},[210,1269,1270],{"class":219}," layers\n",[210,1272,1273,1276,1278],{"class":60,"line":261},[210,1274,1275],{"class":215},"        if",[210,1277,369],{"class":215},[210,1279,1280],{"class":219}," layer.hasScaleBasedVisibility()\n",[210,1282,1283,1286,1289,1292,1295,1297,1300,1303],{"class":60,"line":266},[210,1284,1285],{"class":215},"        or",[210,1287,1288],{"class":219}," ((",[210,1290,1291],{"class":215},"not",[210,1293,1294],{"class":219}," layer.minimumScale() ",[210,1296,491],{"class":215},[210,1298,1299],{"class":219}," scale ",[210,1301,1302],{"class":215},"\u003C=",[210,1304,1305],{"class":219}," layer.minimumScale())\n",[210,1307,1308,1311,1314,1316,1319,1321,1323,1326],{"class":60,"line":278},[210,1309,1310],{"class":215},"            and",[210,1312,1313],{"class":219}," (",[210,1315,1291],{"class":215},[210,1317,1318],{"class":219}," layer.maximumScale() ",[210,1320,491],{"class":215},[210,1322,1299],{"class":219},[210,1324,1325],{"class":215},">=",[210,1327,1328],{"class":219}," layer.maximumScale()))\n",[210,1330,1331],{"class":60,"line":294},[210,1332,1333],{"class":219},"    ]\n",[210,1335,1336],{"class":60,"line":309},[210,1337,233],{"emptyLinePlaceholder":232},[210,1339,1340,1342,1345,1348],{"class":60,"line":448},[210,1341,1193],{"class":255},[210,1343,1344],{"class":219},"(visible_at(QgsProject.instance().mapLayers().values(), ",[210,1346,1347],{"class":255},"50000",[210,1349,1350],{"class":219},"))\n",[14,1352,1353,1355,1356,1359,1360,1364],{},[182,1354,317],{}," The comparison reads oddly because of the denominator convention: a scale is inside the range when it is ",[324,1357,1358],{},"less than or equal to"," the minimum, since a smaller denominator means more zoomed in. Running this for each scale a ",[21,1361,1363],{"href":1362},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","layout"," will export at turns a class of silent blank maps into a printed list you can check in a second.",[171,1366,1368],{"id":1367},"qgis-version-compatibility","QGIS version compatibility",[14,1370,1371,1372,1374],{},"The examples target ",[182,1373,184],{}," (Python 3.12).",[1376,1377,1378,1394],"table",{},[1379,1380,1381],"thead",{},[1382,1383,1384,1388,1391],"tr",{},[1385,1386,1387],"th",{},"QGIS version",[1385,1389,1390],{},"Python",[1385,1392,1393],{},"Notes",[1395,1396,1397,1412,1425,1435,1446],"tbody",{},[1382,1398,1399,1403,1406],{},[1400,1401,1402],"td",{},"3.16 LTR",[1400,1404,1405],{},"3.7",[1400,1407,1408,1409,1411],{},"Layer and rule scale ranges present; label ",[207,1410,821],{}," available.",[1382,1413,1414,1417,1420],{},[1400,1415,1416],{},"3.22 LTR",[1400,1418,1419],{},"3.9",[1400,1421,1422,1424],{},[207,1423,501],{}," accessor stable.",[1382,1426,1427,1430,1432],{},[1400,1428,1429],{},"3.28 LTR",[1400,1431,1419],{},[1400,1433,1434],{},"Layout map items can override layer scale ranges per item.",[1382,1436,1437,1440,1443],{},[1400,1438,1439],{},"3.34 LTR",[1400,1441,1442],{},"3.12",[1400,1444,1445],{},"Baseline for this page.",[1382,1447,1448,1451,1453],{},[1400,1449,1450],{},"3.40+",[1400,1452,1442],{},[1400,1454,1455],{},"Scale-range editing exposed on symbol-level rendering passes.",[171,1457,1459],{"id":1458},"troubleshooting","Troubleshooting",[176,1461,1462,1470,1476,1485,1494,1503],{},[179,1463,1464,318,1467,1469],{},[182,1465,1466],{},"Setting the scales did nothing.",[207,1468,321],{}," was never called.",[179,1471,1472,1475],{},[182,1473,1474],{},"The layer vanishes when you zoom in, not out."," Minimum and maximum are swapped. Minimum is the largest denominator.",[179,1477,1478,1481,1482,1484],{},[182,1479,1480],{},"A gap where no rule draws."," Two rule ranges do not meet. Set the far bound of each to ",[207,1483,46],{}," so they are unbounded on the outside.",[179,1486,1487,1490,1491,1493],{},[182,1488,1489],{},"Labels disappear with the features."," The label settings inherited the layer range. They have their own ",[207,1492,821],{},"; set it separately.",[179,1495,1496,1499,1500,1502],{},[182,1497,1498],{},"The export is blank at one page size."," That page's scale falls outside every range. Check with a ",[207,1501,1225],{}," pass before exporting.",[179,1504,1505,1508],{},[182,1506,1507],{},"The range is ignored inside a layout."," The map item overrides layer scale ranges. Check the item's own settings.",[171,1510,1512],{"id":1511},"conclusion","Conclusion",[14,1514,1515,1516,1518],{},"Enable the flag, remember that minimum scale is the zoomed-out limit, and use ",[207,1517,46],{}," for \"no limit at this end\". Move to per-rule ranges when a layer should change appearance rather than disappear, give labels their own range, and print the visible set for every scale you will export at before trusting the output.",[171,1520,1522],{"id":1521},"frequently-asked-questions","Frequently Asked Questions",[14,1524,1525,1528],{},[182,1526,1527],{},"Does scale-based visibility make the map faster?","\nYes, measurably. A layer outside its range is skipped before any features are requested, so there is no query, no geometry and no rendering. It is one of the cheapest performance improvements available on a busy project.",[14,1530,1531,1534,1535,1539],{},[182,1532,1533],{},"Can a map theme record a scale range?","\nNo. Scale ranges live on the layer and apply in every ",[21,1536,1538],{"href":1537},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fcreate-map-theme-pyqgis\u002F","theme",". If two themes need different ranges, they need different layers over the same source.",[14,1541,1542,1545],{},[182,1543,1544],{},"What scale does a layout map item use?","\nIts own, derived from its extent and its size on the page — not the canvas scale. That is why a layer visible on screen can be missing from an export, and why the item's scale is worth printing while debugging.",[14,1547,1548,1551,1552,827],{},[182,1549,1550],{},"Is the scale the same in a geographic CRS?","\nQGIS computes a scale denominator even in EPSG:4326 by converting to an approximate ground distance, but it varies with latitude. For any project that leans on scale ranges, work in a ",[21,1553,1555],{"href":1554},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","projected CRS",[171,1557,1559],{"id":1558},"related","Related",[176,1561,1562,1567,1573,1579,1584],{},[179,1563,1564,1566],{},[21,1565,24],{"href":23}," — the guide this recipe belongs to",[179,1568,1569],{},[21,1570,1572],{"href":1571},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Ftoggle-layer-visibility-and-legend-pyqgis\u002F","Toggle Layer Visibility and Legend Entries in PyQGIS",[179,1574,1575],{},[21,1576,1578],{"href":1577},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002F","Create a Rule-Based Renderer in PyQGIS",[179,1580,1581],{},[21,1582,1583],{"href":825},"Control Label Placement and Collisions in PyQGIS",[179,1585,1586],{},[21,1587,1589],{"href":1588},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fzoom-to-layer-extent-pyqgis\u002F","Zoom to a Layer Extent in PyQGIS",[1591,1592,1593],"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 pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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}",{"title":205,"searchDepth":229,"depth":229,"links":1595},[1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608],{"id":173,"depth":229,"text":174},{"id":194,"depth":229,"text":195},{"id":340,"depth":229,"text":341},{"id":512,"depth":229,"text":513},{"id":733,"depth":229,"text":734},{"id":830,"depth":229,"text":831},{"id":1159,"depth":229,"text":1160},{"id":1229,"depth":229,"text":1230},{"id":1367,"depth":229,"text":1368},{"id":1458,"depth":229,"text":1459},{"id":1511,"depth":229,"text":1512},{"id":1521,"depth":229,"text":1522},{"id":1558,"depth":229,"text":1559},"Control which layers draw at which zoom levels from Python — minimum and maximum scale, the naming that trips everyone up, per-rule scale ranges, and label scale limits.","md",{"slug":1612,"type":1613,"breadcrumb":1614,"datePublished":1615,"dateModified":1615},"set-scale-based-visibility-pyqgis","article","Scale-Based Visibility","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fset-scale-based-visibility-pyqgis",{"title":5,"description":1609},"pyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fset-scale-based-visibility-pyqgis\u002Findex","8cvXF2QXypO0Iy8Z7bw9U_Ew-cNDmOxuLXDfwMDrXpU",1787823360559]