[{"data":1,"prerenderedAt":1387},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fzoom-to-layer-extent-pyqgis":3},{"id":4,"title":5,"body":6,"description":1376,"extension":1377,"meta":1378,"navigation":270,"path":1383,"seo":1384,"stem":1385,"__hash__":1386},"docs\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fzoom-to-layer-extent-pyqgis\u002Findex.md","Zoom the Canvas to a Layer Extent in PyQGIS",{"type":7,"value":8,"toc":1364},"minimark",[9,13,31,40,197,202,228,232,238,309,327,331,334,489,508,512,515,697,706,709,822,825,829,832,916,1051,1071,1075,1081,1142,1156,1160,1221,1225,1238,1242,1256,1265,1279,1289,1295,1305,1314,1327,1333,1337,1360],[10,11,5],"h1",{"id":12},"zoom-the-canvas-to-a-layer-extent-in-pyqgis",[14,15,16,17,21,22,26,27,30],"p",{},"Zooming to a layer is two lines when the layer and the project happen to share a coordinate reference system, and a puzzle when they do not — the canvas jumps somewhere in the North Atlantic, or to coordinates in the millions, and nothing about the code looks wrong. ",[18,19,20],"code",{},"layer.extent()"," returns a rectangle in the ",[23,24,25],"em",{},"layer's"," CRS, and the canvas expects one in the ",[23,28,29],{},"project's",". Everything else on this page follows from that single fact.",[14,32,33,34,39],{},"This page is a focused recipe within ",[35,36,38],"a",{"href":37},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002F","Map Canvas Control and Image Export in PyQGIS",". It covers zooming to a layer, transforming the extent when the CRSs differ, padding so features are not clipped at the frame, and the degenerate cases — an empty layer and a single point — that produce nonsense without a guard.",[14,41,42],{},[43,44,49,53,57,64,81,90,100,105,110,114,120,127,132,136,140,144,148,152,156,160,164,168,170,174,178,186,190,194],"svg",{"viewBox":45,"role":46,"ariaLabel":47,"xmlns":48},"0 0 760 254","img","A layer extent in a projected CRS passed unchanged to a canvas in a geographic CRS, showing that the coordinates land far outside the valid range","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[50,51,52],"title",{},"What happens when the extent's CRS is not the canvas CRS",[54,55,56],"desc",{},"A layer extent in metres reads roughly five hundred thousand by five point six million. Passed directly to a canvas whose project CRS is degrees, those numbers fall far outside the valid latitude and longitude range, so the canvas jumps to an empty area. Transforming the rectangle first produces coordinates inside the valid range and the canvas lands on the data.",[58,59],"rect",{"x":60,"y":60,"width":61,"height":62,"fill":63},"0","760","254","#f6f3ea",[65,66,67],"defs",{},[68,69,76],"marker",{"id":70,"viewBox":71,"refX":72,"refY":73,"markerWidth":74,"markerHeight":74,"orient":75},"zoomCrsArrow","0 0 10 10","8","5","7","auto-start-reverse",[77,78],"path",{"d":79,"fill":80},"M0 0 L10 5 L0 10 z","#2f3b35",[82,83,89],"text",{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The rectangle is just four numbers — they mean nothing without a CRS",[58,91],{"x":92,"y":93,"width":94,"height":95,"rx":96,"fill":97,"stroke":98,"style":99},"16","52","200","88","10","#fffdf7","#2563eb","stroke-width:2.5",[82,101,20],{"x":102,"y":103,"style":104,"fill":98,"textAnchor":88},"116","78","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif",[82,106,109],{"x":102,"y":107,"style":108,"fill":80,"textAnchor":88},"100","text-anchor:middle;font-size:11px;font-family:monospace","500 000 … 510 000",[82,111,113],{"x":102,"y":112,"style":108,"fill":80,"textAnchor":88},"118","5 600 000 … 5 610 000",[82,115,119],{"x":102,"y":116,"style":117,"fill":118,"textAnchor":88},"134","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#59645f","EPSG:32633, metres",[58,121],{"x":122,"y":123,"width":124,"height":125,"rx":96,"fill":97,"stroke":126,"style":99},"276","42","212","70","#b91c1c",[82,128,131],{"x":129,"y":130,"style":104,"fill":126,"textAnchor":88},"382","66","passed straight through",[82,133,135],{"x":129,"y":95,"style":134,"fill":80,"textAnchor":88},"text-anchor:middle;font-size:11px;font-family:sans-serif","canvas expects degrees",[82,137,139],{"x":129,"y":138,"style":134,"fill":126,"textAnchor":88},"104","valid range is ±180 \u002F ±90",[58,141],{"x":122,"y":142,"width":124,"height":125,"rx":96,"fill":97,"stroke":143,"style":99},"152","#15803d",[82,145,147],{"x":129,"y":146,"style":104,"fill":143,"textAnchor":88},"176","transformed first",[82,149,151],{"x":129,"y":150,"style":108,"fill":80,"textAnchor":88},"198","12.42 … 12.55",[82,153,155],{"x":129,"y":154,"style":108,"fill":80,"textAnchor":88},"214","50.53 … 50.62",[58,157],{"x":158,"y":123,"width":159,"height":125,"rx":96,"fill":97,"stroke":126,"style":99},"548","196",[82,161,163],{"x":162,"y":125,"style":104,"fill":126,"textAnchor":88},"646","canvas goes nowhere",[82,165,167],{"x":162,"y":166,"style":134,"fill":80,"textAnchor":88},"92","blank view, no error",[58,169],{"x":158,"y":142,"width":159,"height":125,"rx":96,"fill":97,"stroke":143,"style":99},[82,171,173],{"x":162,"y":172,"style":104,"fill":143,"textAnchor":88},"180","canvas lands on the data",[82,175,177],{"x":162,"y":176,"style":134,"fill":80,"textAnchor":88},"202","as intended",[179,180],"line",{"x1":181,"y1":182,"x2":183,"y2":184,"stroke":126,"style":185},"216","82","272","76","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#zoomCrsArrow)",[179,187],{"x1":181,"y1":188,"x2":183,"y2":172,"stroke":143,"style":189},"112","stroke-width:2.5;marker-end:url(#zoomCrsArrow)",[179,191],{"x1":192,"y1":184,"x2":193,"y2":184,"stroke":126,"style":185},"488","544",[179,195],{"x1":192,"y1":196,"x2":193,"y2":196,"stroke":143,"style":189},"186",[198,199,201],"h2",{"id":200},"prerequisites","Prerequisites",[203,204,205,213,220],"ul",{},[206,207,208,212],"li",{},[209,210,211],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, running as a desktop application.",[206,214,215,216,219],{},"A layer loaded in the project. ",[18,217,218],{},"iface"," is available in the Python Console and in a plugin.",[206,221,222,223,227],{},"Some awareness of the project CRS versus layer CRS distinction — see ",[35,224,226],{"href":225},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems in PyQGIS",".",[198,229,231],{"id":230},"the-simple-case","The simple case",[14,233,234,235,237],{},"When layer and project share a CRS, ",[18,236,218],{}," provides a ready-made helper that does everything including the refresh.",[239,240,245],"pre",{"className":241,"code":242,"language":243,"meta":244,"style":244},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\niface.setActiveLayer(layer)\niface.zoomToActiveLayer()\n","python","",[18,246,247,265,272,297,303],{"__ignoreMap":244},[248,249,251,255,259,262],"span",{"class":179,"line":250},1,[248,252,254],{"class":253},"snl16","from",[248,256,258],{"class":257},"s95oV"," qgis.core ",[248,260,261],{"class":253},"import",[248,263,264],{"class":257}," QgsProject\n",[248,266,268],{"class":179,"line":267},2,[248,269,271],{"emptyLinePlaceholder":270},true,"\n",[248,273,275,278,281,284,288,291,294],{"class":179,"line":274},3,[248,276,277],{"class":257},"layer ",[248,279,280],{"class":253},"=",[248,282,283],{"class":257}," QgsProject.instance().mapLayersByName(",[248,285,287],{"class":286},"sU2Wk","\"parcels\"",[248,289,290],{"class":257},")[",[248,292,60],{"class":293},"sDLfK",[248,295,296],{"class":257},"]\n",[248,298,300],{"class":179,"line":299},4,[248,301,302],{"class":257},"iface.setActiveLayer(layer)\n",[248,304,306],{"class":179,"line":305},5,[248,307,308],{"class":257},"iface.zoomToActiveLayer()\n",[14,310,311,314,315,318,319,322,323,326],{},[209,312,313],{},"Breakdown:"," ",[18,316,317],{},"zoomToActiveLayer()"," is the scripted equivalent of the \"Zoom to Layer\" context-menu item, and it handles the CRS transform internally — which is exactly why it is worth preferring over hand-rolling the extent when the layer is already in the project. It also applies a small margin automatically. The only awkwardness is that it works on the ",[23,320,321],{},"active"," layer, so ",[18,324,325],{},"setActiveLayer()"," has to run first, changing the user's selection in the layer panel as a side effect.",[198,328,330],{"id":329},"transform-the-extent-yourself","Transform the extent yourself",[14,332,333],{},"When you need control — a specific margin, a computed extent, or a layer that should not become active — do it explicitly.",[239,335,337],{"className":241,"code":336,"language":243,"meta":244,"style":244},"from qgis.core import (\n    QgsCoordinateTransform,\n    QgsProject,\n)\n\ncanvas = iface.mapCanvas()\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nproject = QgsProject.instance()\n\nextent = layer.extent()\nif layer.crs() != project.crs():\n    transform = QgsCoordinateTransform(layer.crs(), project.crs(), project)\n    extent = transform.transformBoundingBox(extent)\n\nextent.scale(1.1)\ncanvas.setExtent(extent)\ncanvas.refresh()\n",[18,338,339,350,355,360,365,369,380,397,408,413,424,439,450,461,466,477,483],{"__ignoreMap":244},[248,340,341,343,345,347],{"class":179,"line":250},[248,342,254],{"class":253},[248,344,258],{"class":257},[248,346,261],{"class":253},[248,348,349],{"class":257}," (\n",[248,351,352],{"class":179,"line":267},[248,353,354],{"class":257},"    QgsCoordinateTransform,\n",[248,356,357],{"class":179,"line":274},[248,358,359],{"class":257},"    QgsProject,\n",[248,361,362],{"class":179,"line":299},[248,363,364],{"class":257},")\n",[248,366,367],{"class":179,"line":305},[248,368,271],{"emptyLinePlaceholder":270},[248,370,372,375,377],{"class":179,"line":371},6,[248,373,374],{"class":257},"canvas ",[248,376,280],{"class":253},[248,378,379],{"class":257}," iface.mapCanvas()\n",[248,381,383,385,387,389,391,393,395],{"class":179,"line":382},7,[248,384,277],{"class":257},[248,386,280],{"class":253},[248,388,283],{"class":257},[248,390,287],{"class":286},[248,392,290],{"class":257},[248,394,60],{"class":293},[248,396,296],{"class":257},[248,398,400,403,405],{"class":179,"line":399},8,[248,401,402],{"class":257},"project ",[248,404,280],{"class":253},[248,406,407],{"class":257}," QgsProject.instance()\n",[248,409,411],{"class":179,"line":410},9,[248,412,271],{"emptyLinePlaceholder":270},[248,414,416,419,421],{"class":179,"line":415},10,[248,417,418],{"class":257},"extent ",[248,420,280],{"class":253},[248,422,423],{"class":257}," layer.extent()\n",[248,425,427,430,433,436],{"class":179,"line":426},11,[248,428,429],{"class":253},"if",[248,431,432],{"class":257}," layer.crs() ",[248,434,435],{"class":253},"!=",[248,437,438],{"class":257}," project.crs():\n",[248,440,442,445,447],{"class":179,"line":441},12,[248,443,444],{"class":257},"    transform ",[248,446,280],{"class":253},[248,448,449],{"class":257}," QgsCoordinateTransform(layer.crs(), project.crs(), project)\n",[248,451,453,456,458],{"class":179,"line":452},13,[248,454,455],{"class":257},"    extent ",[248,457,280],{"class":253},[248,459,460],{"class":257}," transform.transformBoundingBox(extent)\n",[248,462,464],{"class":179,"line":463},14,[248,465,271],{"emptyLinePlaceholder":270},[248,467,469,472,475],{"class":179,"line":468},15,[248,470,471],{"class":257},"extent.scale(",[248,473,474],{"class":293},"1.1",[248,476,364],{"class":257},[248,478,480],{"class":179,"line":479},16,[248,481,482],{"class":257},"canvas.setExtent(extent)\n",[248,484,486],{"class":179,"line":485},17,[248,487,488],{"class":257},"canvas.refresh()\n",[14,490,491,314,493,496,497,500,501,503,504,507],{},[209,492,313],{},[18,494,495],{},"transformBoundingBox()"," is the correct method here, not ",[18,498,499],{},"transform()",". A rectangle's corners do not map to a rectangle under a projection — the edges curve — so transforming only the two corner points can produce a box that excludes part of the data. ",[18,502,495],{}," samples along the edges and returns a rectangle guaranteed to contain the transformed shape. ",[18,505,506],{},"extent.scale(1.1)"," adds a ten per cent margin in place, which stops symbols and labels being clipped at the frame edge.",[198,509,511],{"id":510},"zoom-to-a-feature-or-a-selection","Zoom to a feature or a selection",[14,513,514],{},"The same pattern narrows to one feature or to whatever the user has selected.",[239,516,518],{"className":241,"code":517,"language":243,"meta":244,"style":244},"from qgis.core import QgsCoordinateTransform, QgsProject\n\ncanvas = iface.mapCanvas()\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nproject = QgsProject.instance()\n\n\ndef zoom_to(geometry, source_crs, margin=1.3):\n    extent = geometry.boundingBox()\n    if source_crs != project.crs():\n        transform = QgsCoordinateTransform(source_crs, project.crs(), project)\n        extent = transform.transformBoundingBox(extent)\n    extent.scale(margin)\n    canvas.setExtent(extent)\n    canvas.refresh()\n\n\nfeature = next(layer.getFeatures())\nzoom_to(feature.geometry(), layer.crs())\n\nif layer.selectedFeatureCount():\n    zoom_to(layer.boundingBoxOfSelected(), layer.crs())\n",[18,519,520,531,535,543,559,567,571,575,595,604,616,626,635,640,645,650,654,658,672,678,683,691],{"__ignoreMap":244},[248,521,522,524,526,528],{"class":179,"line":250},[248,523,254],{"class":253},[248,525,258],{"class":257},[248,527,261],{"class":253},[248,529,530],{"class":257}," QgsCoordinateTransform, QgsProject\n",[248,532,533],{"class":179,"line":267},[248,534,271],{"emptyLinePlaceholder":270},[248,536,537,539,541],{"class":179,"line":274},[248,538,374],{"class":257},[248,540,280],{"class":253},[248,542,379],{"class":257},[248,544,545,547,549,551,553,555,557],{"class":179,"line":299},[248,546,277],{"class":257},[248,548,280],{"class":253},[248,550,283],{"class":257},[248,552,287],{"class":286},[248,554,290],{"class":257},[248,556,60],{"class":293},[248,558,296],{"class":257},[248,560,561,563,565],{"class":179,"line":305},[248,562,402],{"class":257},[248,564,280],{"class":253},[248,566,407],{"class":257},[248,568,569],{"class":179,"line":371},[248,570,271],{"emptyLinePlaceholder":270},[248,572,573],{"class":179,"line":382},[248,574,271],{"emptyLinePlaceholder":270},[248,576,577,580,584,587,589,592],{"class":179,"line":399},[248,578,579],{"class":253},"def",[248,581,583],{"class":582},"svObZ"," zoom_to",[248,585,586],{"class":257},"(geometry, source_crs, margin",[248,588,280],{"class":253},[248,590,591],{"class":293},"1.3",[248,593,594],{"class":257},"):\n",[248,596,597,599,601],{"class":179,"line":410},[248,598,455],{"class":257},[248,600,280],{"class":253},[248,602,603],{"class":257}," geometry.boundingBox()\n",[248,605,606,609,612,614],{"class":179,"line":415},[248,607,608],{"class":253},"    if",[248,610,611],{"class":257}," source_crs ",[248,613,435],{"class":253},[248,615,438],{"class":257},[248,617,618,621,623],{"class":179,"line":426},[248,619,620],{"class":257},"        transform ",[248,622,280],{"class":253},[248,624,625],{"class":257}," QgsCoordinateTransform(source_crs, project.crs(), project)\n",[248,627,628,631,633],{"class":179,"line":441},[248,629,630],{"class":257},"        extent ",[248,632,280],{"class":253},[248,634,460],{"class":257},[248,636,637],{"class":179,"line":452},[248,638,639],{"class":257},"    extent.scale(margin)\n",[248,641,642],{"class":179,"line":463},[248,643,644],{"class":257},"    canvas.setExtent(extent)\n",[248,646,647],{"class":179,"line":468},[248,648,649],{"class":257},"    canvas.refresh()\n",[248,651,652],{"class":179,"line":479},[248,653,271],{"emptyLinePlaceholder":270},[248,655,656],{"class":179,"line":485},[248,657,271],{"emptyLinePlaceholder":270},[248,659,661,664,666,669],{"class":179,"line":660},18,[248,662,663],{"class":257},"feature ",[248,665,280],{"class":253},[248,667,668],{"class":293}," next",[248,670,671],{"class":257},"(layer.getFeatures())\n",[248,673,675],{"class":179,"line":674},19,[248,676,677],{"class":257},"zoom_to(feature.geometry(), layer.crs())\n",[248,679,681],{"class":179,"line":680},20,[248,682,271],{"emptyLinePlaceholder":270},[248,684,686,688],{"class":179,"line":685},21,[248,687,429],{"class":253},[248,689,690],{"class":257}," layer.selectedFeatureCount():\n",[248,692,694],{"class":179,"line":693},22,[248,695,696],{"class":257},"    zoom_to(layer.boundingBoxOfSelected(), layer.crs())\n",[14,698,699,701,702,705],{},[209,700,313],{}," Factoring the transform and padding into one helper means the CRS handling is written once and cannot be forgotten in the second call. A larger margin — 1.3 rather than 1.1 — suits a single feature, which otherwise fills the frame and reads as a diagram rather than a map. ",[18,703,704],{},"boundingBoxOfSelected()"," returns the combined extent of the selection in the layer's CRS, and it returns an empty rectangle when nothing is selected, which is why the count check comes first.",[14,707,708],{},"Choosing the margin is a judgement about what the view is for, and the three common cases want visibly different amounts of context.",[14,710,711],{},[43,712,715,718,721,724,727,765,788],{"viewBox":713,"role":46,"ariaLabel":714,"xmlns":48},"0 0 760 242","The same feature shown at three margin factors: a tight fit that clips symbols, a ten percent margin suited to a whole layer, and a thirty percent margin suited to a single feature",[50,716,717],{},"How much margin each kind of zoom wants",[54,719,720],{},"Three panels show the same subject at increasing margins. At factor one the geometry touches the frame and its symbols are clipped. At factor one point one there is a comfortable border, which suits zooming to a whole layer. At factor one point three the subject sits smaller with surrounding context visible, which suits zooming to a single feature.",[58,722],{"x":60,"y":60,"width":61,"height":723,"fill":63},"242",[82,725,726],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"Match the margin to what the view is for",[728,729,730,735,740,747,752,757,761],"g",{},[58,731],{"x":732,"y":733,"width":734,"height":172,"rx":96,"fill":97,"stroke":126,"style":99},"12","46","236",[82,736,739],{"x":737,"y":125,"style":738,"fill":126,"textAnchor":88},"130","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","scale(1.0)",[58,741],{"x":742,"y":743,"width":744,"height":138,"fill":745,"stroke":118,"style":746},"36","84","188","#e7e2d4","stroke-width:1.5",[77,748],{"d":749,"fill":750,"fillOpacity":751,"stroke":750,"style":99},"M36 172 L36 96 L128 88 L172 132 L124 188 Z","#0f766e",0.28,[753,754],"circle",{"cx":742,"cy":755,"r":74,"fill":756},"96","#b45309",[753,758],{"cx":759,"cy":760,"r":74,"fill":756},"224","140",[82,762,764],{"x":737,"y":763,"style":134,"fill":126,"textAnchor":88},"210","symbols clipped at the frame",[728,766,767,770,773,776,779,782,785],{},[58,768],{"x":769,"y":733,"width":734,"height":172,"rx":96,"fill":97,"stroke":143,"style":99},"262",[82,771,772],{"x":84,"y":125,"style":738,"fill":143,"textAnchor":88},"scale(1.1) — a layer",[58,774],{"x":775,"y":743,"width":744,"height":138,"fill":745,"stroke":118,"style":746},"286",[77,777],{"d":778,"fill":750,"fillOpacity":751,"stroke":750,"style":99},"M300 164 L300 100 L378 93 L416 130 L375 178 Z",[753,780],{"cx":781,"cy":107,"r":74,"fill":756},"300",[753,783],{"cx":784,"cy":760,"r":74,"fill":756},"452",[82,786,787],{"x":84,"y":763,"style":134,"fill":143,"textAnchor":88},"a clean border all round",[728,789,790,793,797,800,803,808,811,816,819],{},[58,791],{"x":792,"y":733,"width":734,"height":172,"rx":96,"fill":97,"stroke":98,"style":99},"512",[82,794,796],{"x":795,"y":125,"style":738,"fill":98,"textAnchor":88},"630","scale(1.3) — a feature",[58,798],{"x":799,"y":743,"width":744,"height":138,"fill":745,"stroke":118,"style":746},"536",[77,801],{"d":802,"fill":750,"fillOpacity":751,"stroke":750,"style":99},"M572 156 L572 108 L630 102 L658 130 L628 166 Z",[753,804],{"cx":805,"cy":806,"r":807,"fill":756},"572","108","6",[753,809],{"cx":810,"cy":760,"r":807,"fill":756},"694",[77,812],{"d":813,"fill":814,"stroke":118,"style":815},"M536 176 L560 168 L590 180 L620 172","none","stroke-width:1.4",[77,817],{"d":818,"fill":814,"stroke":118,"style":815},"M660 96 L688 90 L718 100",[82,820,821],{"x":795,"y":763,"style":134,"fill":98,"textAnchor":88},"surrounding context visible",[14,823,824],{},"A single feature zoomed to a tight fit reads as a diagram rather than a location, which is why the helper above takes the margin as a parameter rather than hard-coding one.",[198,826,828],{"id":827},"guard-the-degenerate-cases","Guard the degenerate cases",[14,830,831],{},"Two extents break a naive zoom: an empty layer and a layer whose extent has zero width or height.",[14,833,834],{},[43,835,838,841,844,847,850,872,894],{"viewBox":836,"role":46,"ariaLabel":837,"xmlns":48},"0 0 760 244","Three extent shapes: a normal rectangle, an empty extent from a layer with no features, and a zero-size extent from a single point, with the guard each needs",[50,839,840],{},"Two extents that need a guard",[54,842,843],{},"A normal extent is a rectangle with real width and height and needs no special handling. An empty layer produces a null extent whose coordinates are inverted, and zooming to it leaves the canvas somewhere arbitrary. A single point produces an extent of zero width and height, and zooming to it puts the canvas at infinite magnification, so it must be grown by a fixed distance first.",[58,845],{"x":60,"y":60,"width":61,"height":846,"fill":63},"244",[82,848,849],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"Check isEmpty() and isNull() before trusting an extent",[728,851,852,855,858,864,868],{},[58,853],{"x":92,"y":733,"width":734,"height":854,"rx":96,"fill":97,"stroke":143,"style":99},"182",[82,856,857],{"x":116,"y":125,"style":738,"fill":143,"textAnchor":88},"normal extent",[58,859],{"x":860,"y":861,"width":862,"height":95,"fill":143,"fillOpacity":863,"stroke":143,"style":99},"56","94","156",0.14,[82,865,867],{"x":116,"y":866,"style":134,"fill":80,"textAnchor":88},"204","width and height > 0",[82,869,871],{"x":116,"y":870,"style":134,"fill":143,"textAnchor":88},"222","zoom directly",[728,873,874,876,879,883,888,891],{},[58,875],{"x":769,"y":733,"width":734,"height":854,"rx":96,"fill":97,"stroke":126,"style":99},[82,877,878],{"x":84,"y":125,"style":738,"fill":126,"textAnchor":88},"empty layer",[58,880],{"x":881,"y":861,"width":862,"height":95,"fill":814,"stroke":126,"style":882},"302","stroke-width:2;stroke-dasharray:6 4",[82,884,887],{"x":84,"y":885,"style":886,"fill":126,"textAnchor":88},"144","text-anchor:middle;font-size:12px;font-family:sans-serif","no features",[82,889,890],{"x":84,"y":866,"style":134,"fill":80,"textAnchor":88},"extent.isNull() is True",[82,892,893],{"x":84,"y":870,"style":134,"fill":126,"textAnchor":88},"skip, do not zoom",[728,895,896,899,903,906,910,913],{},[58,897],{"x":898,"y":733,"width":734,"height":854,"rx":96,"fill":97,"stroke":756,"style":99},"508",[82,900,902],{"x":901,"y":125,"style":738,"fill":756,"textAnchor":88},"626","single point",[753,904],{"cx":901,"cy":905,"r":807,"fill":756},"138",[753,907],{"cx":901,"cy":905,"r":908,"fill":814,"stroke":756,"style":909},"30","stroke-width:2;stroke-dasharray:5 4",[82,911,912],{"x":901,"y":866,"style":134,"fill":80,"textAnchor":88},"width and height are 0",[82,914,915],{"x":901,"y":870,"style":134,"fill":756,"textAnchor":88},"grow by a fixed distance",[239,917,919],{"className":241,"code":918,"language":243,"meta":244,"style":244},"def safe_zoom(canvas, extent, minimum_span=250.0, margin=1.1):\n    \"\"\"Zoom to an extent, handling empty layers and zero-size extents.\"\"\"\n    if extent.isNull() or extent.isEmpty():\n        print(\"nothing to zoom to\")\n        return False\n    if extent.width() == 0 or extent.height() == 0:\n        extent.grow(minimum_span \u002F 2)\n    else:\n        extent.scale(margin)\n    canvas.setExtent(extent)\n    canvas.refresh()\n    return True\n",[18,920,921,945,950,963,976,984,1010,1023,1030,1035,1039,1043],{"__ignoreMap":244},[248,922,923,925,928,931,933,936,939,941,943],{"class":179,"line":250},[248,924,579],{"class":253},[248,926,927],{"class":582}," safe_zoom",[248,929,930],{"class":257},"(canvas, extent, minimum_span",[248,932,280],{"class":253},[248,934,935],{"class":293},"250.0",[248,937,938],{"class":257},", margin",[248,940,280],{"class":253},[248,942,474],{"class":293},[248,944,594],{"class":257},[248,946,947],{"class":179,"line":267},[248,948,949],{"class":286},"    \"\"\"Zoom to an extent, handling empty layers and zero-size extents.\"\"\"\n",[248,951,952,954,957,960],{"class":179,"line":274},[248,953,608],{"class":253},[248,955,956],{"class":257}," extent.isNull() ",[248,958,959],{"class":253},"or",[248,961,962],{"class":257}," extent.isEmpty():\n",[248,964,965,968,971,974],{"class":179,"line":299},[248,966,967],{"class":293},"        print",[248,969,970],{"class":257},"(",[248,972,973],{"class":286},"\"nothing to zoom to\"",[248,975,364],{"class":257},[248,977,978,981],{"class":179,"line":305},[248,979,980],{"class":253},"        return",[248,982,983],{"class":293}," False\n",[248,985,986,988,991,994,997,1000,1003,1005,1007],{"class":179,"line":371},[248,987,608],{"class":253},[248,989,990],{"class":257}," extent.width() ",[248,992,993],{"class":253},"==",[248,995,996],{"class":293}," 0",[248,998,999],{"class":253}," or",[248,1001,1002],{"class":257}," extent.height() ",[248,1004,993],{"class":253},[248,1006,996],{"class":293},[248,1008,1009],{"class":257},":\n",[248,1011,1012,1015,1018,1021],{"class":179,"line":382},[248,1013,1014],{"class":257},"        extent.grow(minimum_span ",[248,1016,1017],{"class":253},"\u002F",[248,1019,1020],{"class":293}," 2",[248,1022,364],{"class":257},[248,1024,1025,1028],{"class":179,"line":399},[248,1026,1027],{"class":253},"    else",[248,1029,1009],{"class":257},[248,1031,1032],{"class":179,"line":410},[248,1033,1034],{"class":257},"        extent.scale(margin)\n",[248,1036,1037],{"class":179,"line":415},[248,1038,644],{"class":257},[248,1040,1041],{"class":179,"line":426},[248,1042,649],{"class":257},[248,1044,1045,1048],{"class":179,"line":441},[248,1046,1047],{"class":253},"    return",[248,1049,1050],{"class":293}," True\n",[14,1052,1053,314,1055,1058,1059,1062,1063,1066,1067,1070],{},[209,1054,313],{},[18,1056,1057],{},"isNull()"," catches an extent that was never set — a layer with no features returns one whose minimum coordinates exceed its maximum. ",[18,1060,1061],{},"width() == 0"," catches the single-point case, where ",[18,1064,1065],{},"scale()"," would multiply zero by 1.1 and leave it at zero, sending the canvas to infinite magnification. ",[18,1068,1069],{},"grow()"," expands by an absolute distance in map units rather than a factor, which is the only thing that works on a degenerate extent. Returning a bool lets the caller distinguish \"zoomed\" from \"nothing to show\" without inspecting the canvas.",[198,1072,1074],{"id":1073},"qgis-version-compatibility","QGIS version compatibility",[14,1076,1077,1078,1080],{},"The examples target ",[209,1079,211],{}," (Python 3.12).",[1082,1083,1084,1100],"table",{},[1085,1086,1087],"thead",{},[1088,1089,1090,1094,1097],"tr",{},[1091,1092,1093],"th",{},"QGIS version",[1091,1095,1096],{},"Python",[1091,1098,1099],{},"Notes",[1101,1102,1103,1118,1129],"tbody",{},[1088,1104,1105,1109,1112],{},[1106,1107,1108],"td",{},"3.28 LTR",[1106,1110,1111],{},"3.9",[1106,1113,1114,1115,1117],{},"Identical API. ",[18,1116,495],{}," available throughout 3.x.",[1088,1119,1120,1123,1126],{},[1106,1121,1122],{},"3.34 LTR",[1106,1124,1125],{},"3.12",[1106,1127,1128],{},"Baseline for this page.",[1088,1130,1131,1134,1136],{},[1106,1132,1133],{},"3.40 \u002F 3.44",[1106,1135,1125],{},[1106,1137,1138,1141],{},[18,1139,1140],{},"QgsMapCanvas.zoomToFeatureExtent()"," handles the transform and padding in one call.",[14,1143,1144,1147,1148,1151,1152,1155],{},[18,1145,1146],{},"setExtent()",", ",[18,1149,1150],{},"QgsCoordinateTransform"," and ",[18,1153,1154],{},"QgsRectangle","'s helpers are unchanged across 3.x.",[198,1157,1159],{"id":1158},"troubleshooting","Troubleshooting",[203,1161,1162,1170,1181,1192,1201,1210],{},[206,1163,1164,1167,1168,227],{},[209,1165,1166],{},"The canvas jumps somewhere blank."," The extent was in the layer's CRS and the project uses a different one. Transform it with ",[18,1169,495],{},[206,1171,1172,314,1175,1177,1178,1180],{},[209,1173,1174],{},"Part of the layer is cut off after a transform.",[18,1176,499],{}," was used on the rectangle instead of ",[18,1179,495],{},". Corner-only transformation does not account for curved edges.",[206,1182,1183,1186,1187,1189,1190,227],{},[209,1184,1185],{},"The canvas zooms to infinity."," The extent has zero width or height — a single point, or a layer with one vertical line. Use ",[18,1188,1069],{}," rather than ",[18,1191,1065],{},[206,1193,1194,314,1197,1200],{},[209,1195,1196],{},"Nothing happens.",[18,1198,1199],{},"refresh()"," was not called, or the extent was identical to the current one. The canvas skips a repaint when nothing changed.",[206,1202,1203,1206,1207,1209],{},[209,1204,1205],{},"Features touch the frame edge."," No margin was applied. ",[18,1208,506],{}," is the habit worth forming.",[206,1211,1212,1217,1218,1220],{},[209,1213,1214,1216],{},[18,1215,317],{}," zooms to the wrong layer."," The active layer is whatever is highlighted in the layer panel. Call ",[18,1219,325],{}," first, or transform the extent yourself.",[198,1222,1224],{"id":1223},"conclusion","Conclusion",[14,1226,1227,1228,1231,1232,1234,1235,1237],{},"Every zoom problem in PyQGIS reduces to the CRS of the rectangle. Use ",[18,1229,1230],{},"iface.zoomToActiveLayer()"," when the convenience is worth changing the active layer; otherwise transform the extent with ",[18,1233,495],{},", pad it with ",[18,1236,1065],{},", and guard the empty and zero-size cases before handing it to the canvas.",[198,1239,1241],{"id":1240},"frequently-asked-questions","Frequently Asked Questions",[14,1243,1244,1247,1249,1250,1253,1254,227],{},[209,1245,1246],{},"Why does zooming to my layer show a blank canvas?",[18,1248,20],{}," is in the layer's CRS, and the canvas expects the project's. Transform the rectangle with ",[18,1251,1252],{},"QgsCoordinateTransform(...).transformBoundingBox()"," before calling ",[18,1255,1146],{},[14,1257,1258,1261,1262,1264],{},[209,1259,1260],{},"Why use transformBoundingBox instead of transform?","\nA rectangle's edges curve under a projection, so transforming only the corners can produce a box that excludes part of the data. ",[18,1263,495],{}," samples along the edges and returns a rectangle guaranteed to contain everything.",[14,1266,1267,1270,1271,1274,1275,1278],{},[209,1268,1269],{},"How do I zoom to the selected features?","\nUse ",[18,1272,1273],{},"layer.boundingBoxOfSelected()",", which returns the combined extent in the layer's CRS. Check ",[18,1276,1277],{},"selectedFeatureCount()"," first — with nothing selected it returns an empty rectangle.",[14,1280,1281,1284,1285,1288],{},[209,1282,1283],{},"How do I zoom to a single point without going to infinite magnification?","\nA point's extent has zero width and height, so multiplying it by a margin factor changes nothing. Call ",[18,1286,1287],{},"extent.grow(distance)"," to expand it by an absolute number of map units instead.",[14,1290,1291,1294],{},[209,1292,1293],{},"Does setExtent guarantee the canvas shows exactly that rectangle?","\nNot quite. The canvas has its own aspect ratio, and when the rectangle you supply does not match it QGIS expands the extent on the shorter axis so nothing is cut off. The result therefore contains your rectangle but is usually a little larger. If an exact framing matters — for a map series where every page must cover the same ground — compute the rectangle from the canvas's own width-to-height ratio before setting it.",[14,1296,1297,1300,1301,1304],{},[209,1298,1299],{},"Why does zooming feel different between two projects with the same data?","\nThe project CRS differs, so the same ground area occupies a different coordinate range and the scale denominator reported by ",[18,1302,1303],{},"canvas.scale()"," is computed against different map units. Zooming by scale rather than by extent makes the behaviour consistent, because a scale denominator means the same thing in any projected CRS whose units are metres.",[14,1306,1307,1270,1310,1313],{},[209,1308,1309],{},"How do I zoom out to show everything in the project?",[18,1311,1312],{},"canvas.zoomToFullExtent()",", which combines the extents of every visible layer and handles the CRS transforms internally — considerably simpler than unioning the rectangles yourself.",[14,1315,1316,1319,1320,1151,1323,1326],{},[209,1317,1318],{},"Does the canvas keep a zoom history?","\nYes. ",[18,1321,1322],{},"canvas.zoomToPreviousExtent()",[18,1324,1325],{},"zoomToNextExtent()"," walk the stack QGIS maintains, which is what the back and forward buttons on the Map Navigation toolbar use.",[14,1328,1329,1332],{},[209,1330,1331],{},"Does zooming trigger a data reload?","\nOnly for providers that fetch by extent, such as WMS or a remote WFS. A local GeoPackage is read from the same file whatever the view, so zooming costs a render rather than a fetch.",[198,1334,1336],{"id":1335},"related","Related",[203,1338,1339,1344,1350,1354],{},[206,1340,1341,1343],{},[35,1342,38],{"href":37}," — the guide this recipe belongs to",[206,1345,1346],{},[35,1347,1349],{"href":1348},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002F","Export the Map Canvas to a PNG with PyQGIS",[206,1351,1352],{},[35,1353,226],{"href":225},[206,1355,1356],{},[35,1357,1359],{"href":1358},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002F","Transform Point Coordinates Between CRS in PyQGIS",[1361,1362,1363],"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}",{"title":244,"searchDepth":267,"depth":267,"links":1365},[1366,1367,1368,1369,1370,1371,1372,1373,1374,1375],{"id":200,"depth":267,"text":201},{"id":230,"depth":267,"text":231},{"id":329,"depth":267,"text":330},{"id":510,"depth":267,"text":511},{"id":827,"depth":267,"text":828},{"id":1073,"depth":267,"text":1074},{"id":1158,"depth":267,"text":1159},{"id":1223,"depth":267,"text":1224},{"id":1240,"depth":267,"text":1241},{"id":1335,"depth":267,"text":1336},"Zoom the QGIS canvas to a layer, a feature or a selection in PyQGIS. Handle CRS differences, pad the extent, and cope with empty or single-point extents.","md",{"slug":1379,"type":1380,"breadcrumb":1381,"datePublished":1382,"dateModified":1382},"zoom-to-layer-extent-pyqgis","article","Zoom to Extent","2026-08-01","\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fzoom-to-layer-extent-pyqgis",{"title":5,"description":1376},"pyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fzoom-to-layer-extent-pyqgis\u002Findex","1W0wyO3wNPJu1fvXVrdzJ10p7GVdgWHIntpSuMMQgD0",1785585990471]