[{"data":1,"prerenderedAt":2392},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fplot-layer-data-with-matplotlib-pyqgis":3},{"id":4,"title":5,"body":6,"description":2381,"extension":2382,"meta":2383,"navigation":262,"path":2388,"seo":2389,"stem":2390,"__hash__":2391},"docs\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fplot-layer-data-with-matplotlib-pyqgis\u002Findex.md","Plot Layer Data with Matplotlib in PyQGIS",{"type":7,"value":8,"toc":2366},"minimark",[9,13,17,26,173,178,197,201,204,350,376,380,383,725,734,738,741,828,1112,1117,1121,1124,1463,1477,1481,1484,1774,1790,1794,1797,1870,2115,2129,2133,2136,2223,2233,2237,2251,2255,2290,2294,2297,2301,2307,2317,2323,2329,2333,2362],[10,11,5],"h1",{"id":12},"plot-layer-data-with-matplotlib-in-pyqgis",[14,15,16],"p",{},"A map shows where; a chart shows how much, how often and how it changed. Most reports need both: the distribution of building heights next to the map of buildings, the monthly count of incidents next to the hot spot map, the elevation profile next to the route. Matplotlib ships with QGIS on every platform, reads straight from Python lists and NumPy arrays, and writes publication-quality SVG and PNG — so charts can be produced by the same script that produced the analysis.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002F","PyQGIS and the Python Data Stack",". It sets matplotlib up so it does not interfere with QGIS, plots attribute distributions, categories, relationships and time series, plots raster values, and saves figures ready for print layouts.",[14,27,28],{},[29,30,35,39,43,50,59,69,76,81,85,90,95,101,104,111,113,116,119,123,126,134,137,140,143,146,150,153,157,161,168],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","Four chart types matched to questions: histogram for distributions, bar chart for categories, scatter plot for relationships and line chart for change over time","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Which chart for which question",[40,41,42],"desc",{},"Four common questions and their charts. How are values distributed: a histogram of a numeric field. How do categories compare: a horizontal bar chart of counts or sums by category. Are two variables related: a scatter plot of one numeric field against another. How did something change: a line chart of counts per month from a date field.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","280","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Pick the chart from the question",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","52","168","196","8","#fffdf7","#59645f","stroke-width:1.5",[44,70],{"x":71,"y":72,"width":73,"height":74,"rx":46,"fill":75,"stroke":75,"style":68},"44","180","20","40","#0f766e",[44,77],{"x":78,"y":79,"width":73,"height":80,"rx":46,"fill":75,"stroke":75,"style":68},"68","130","90",[44,82],{"x":83,"y":84,"width":73,"height":84,"rx":46,"fill":75,"stroke":75,"style":68},"92","110",[44,86],{"x":87,"y":88,"width":73,"height":89,"rx":46,"fill":75,"stroke":75,"style":68},"116","150","70",[44,91],{"x":92,"y":93,"width":73,"height":94,"rx":46,"fill":75,"stroke":75,"style":68},"140","190","30",[51,96,100],{"x":97,"y":98,"style":99,"fill":75,"textAnchor":57},"108","238","text-anchor:middle;font-size:10.5px;font-family:sans-serif","distribution",[44,102],{"x":103,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"206",[44,105],{"x":106,"y":107,"width":108,"height":109,"rx":46,"fill":110,"stroke":110,"style":68},"226","80","120","18","#2563eb",[44,112],{"x":106,"y":84,"width":80,"height":109,"rx":46,"fill":110,"stroke":110,"style":68},[44,114],{"x":106,"y":92,"width":115,"height":109,"rx":46,"fill":110,"stroke":110,"style":68},"60",[44,117],{"x":106,"y":118,"width":94,"height":109,"rx":46,"fill":110,"stroke":110,"style":68},"170",[51,120,122],{"x":121,"y":98,"style":99,"fill":110,"textAnchor":57},"290","categories",[44,124],{"x":125,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"388",[127,128],"circle",{"cx":129,"cy":130,"r":131,"fill":132,"stroke":132,"style":133},"415","200","4","#b45309","stroke-width:1",[127,135],{"cx":136,"cy":72,"r":131,"fill":132,"stroke":132,"style":133},"440",[127,138],{"cx":139,"cy":118,"r":131,"fill":132,"stroke":132,"style":133},"460",[127,141],{"cx":142,"cy":92,"r":131,"fill":132,"stroke":132,"style":133},"480",[127,144],{"cx":145,"cy":79,"r":131,"fill":132,"stroke":132,"style":133},"500",[127,147],{"cx":148,"cy":149,"r":131,"fill":132,"stroke":132,"style":133},"525","100",[127,151],{"cx":152,"cy":84,"r":131,"fill":132,"stroke":132,"style":133},"470",[51,154,156],{"x":155,"y":98,"style":99,"fill":132,"textAnchor":57},"472","relationship",[44,158],{"x":159,"y":62,"width":160,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"570","166",[162,163],"polyline",{"points":164,"fill":165,"stroke":166,"style":167},"585,190 610,170 635,180 660,130 685,140 710,90","none","#15803d","stroke-width:2.5",[51,169,172],{"x":170,"y":98,"style":99,"fill":171,"textAnchor":57},"653","#166534","change over time",[174,175,177],"h2",{"id":176},"prerequisites","Prerequisites",[179,180,181,190],"ul",{},[182,183,184,185,189],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series. Matplotlib is included with the official installers; check with ",[186,187,188],"code",{},"import matplotlib; matplotlib.__version__",".",[182,191,192,193,189],{},"Familiarity with reading attributes into Python, as in ",[21,194,196],{"href":195},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fanalyse-attribute-table-with-pandas-pyqgis\u002F","analysing an attribute table with pandas",[174,198,200],{"id":199},"set-matplotlib-up-for-qgis","Set matplotlib up for QGIS",[14,202,203],{},"Matplotlib normally opens windows with its own GUI toolkit. Inside QGIS that can freeze the interface or crash it, because two event loops compete. Using the non-interactive Agg backend and saving figures to files avoids the problem entirely.",[205,206,211],"pre",{"className":207,"code":208,"language":209,"meta":210,"style":210},"language-python shiki shiki-themes github-dark","import matplotlib\nmatplotlib.use(\"Agg\")                 # render to files only, no windows\nimport matplotlib.pyplot as plt\n\nplt.rcParams.update({\n    \"figure.dpi\": 100, \"savefig.dpi\": 200,\n    \"font.size\": 9, \"axes.spines.top\": False, \"axes.spines.right\": False,\n    \"svg.fonttype\": \"none\",           # keep text as text in SVG output\n})\n","python","",[186,212,213,226,243,257,264,270,295,327,344],{"__ignoreMap":210},[214,215,218,222],"span",{"class":216,"line":217},"line",1,[214,219,221],{"class":220},"snl16","import",[214,223,225],{"class":224},"s95oV"," matplotlib\n",[214,227,229,232,236,239],{"class":216,"line":228},2,[214,230,231],{"class":224},"matplotlib.use(",[214,233,235],{"class":234},"sU2Wk","\"Agg\"",[214,237,238],{"class":224},")                 ",[214,240,242],{"class":241},"sjoCn","# render to files only, no windows\n",[214,244,246,248,251,254],{"class":216,"line":245},3,[214,247,221],{"class":220},[214,249,250],{"class":224}," matplotlib.pyplot ",[214,252,253],{"class":220},"as",[214,255,256],{"class":224}," plt\n",[214,258,260],{"class":216,"line":259},4,[214,261,263],{"emptyLinePlaceholder":262},true,"\n",[214,265,267],{"class":216,"line":266},5,[214,268,269],{"class":224},"plt.rcParams.update({\n",[214,271,273,276,279,282,285,288,290,292],{"class":216,"line":272},6,[214,274,275],{"class":234},"    \"figure.dpi\"",[214,277,278],{"class":224},": ",[214,280,149],{"class":281},"sDLfK",[214,283,284],{"class":224},", ",[214,286,287],{"class":234},"\"savefig.dpi\"",[214,289,278],{"class":224},[214,291,130],{"class":281},[214,293,294],{"class":224},",\n",[214,296,298,301,303,306,308,311,313,316,318,321,323,325],{"class":216,"line":297},7,[214,299,300],{"class":234},"    \"font.size\"",[214,302,278],{"class":224},[214,304,305],{"class":281},"9",[214,307,284],{"class":224},[214,309,310],{"class":234},"\"axes.spines.top\"",[214,312,278],{"class":224},[214,314,315],{"class":281},"False",[214,317,284],{"class":224},[214,319,320],{"class":234},"\"axes.spines.right\"",[214,322,278],{"class":224},[214,324,315],{"class":281},[214,326,294],{"class":224},[214,328,330,333,335,338,341],{"class":216,"line":329},8,[214,331,332],{"class":234},"    \"svg.fonttype\"",[214,334,278],{"class":224},[214,336,337],{"class":234},"\"none\"",[214,339,340],{"class":224},",           ",[214,342,343],{"class":241},"# keep text as text in SVG output\n",[214,345,347],{"class":216,"line":346},9,[214,348,349],{"class":224},"})\n",[14,351,352,356,357,360,361,364,365,368,369,372,373,189],{},[353,354,355],"strong",{},"Breakdown:"," ",[186,358,359],{},"matplotlib.use(\"Agg\")"," must run before ",[186,362,363],{},"pyplot"," is imported for the first time in the session; putting it at the top of every plotting script is the safe habit. Saving to files works identically inside QGIS, in a standalone script and in a scheduled job, which is exactly what automated reporting needs. ",[186,366,367],{},"svg.fonttype = \"none\""," keeps labels as real text in SVG files, so they stay sharp, searchable and editable in a layout. To show a chart inside a plugin, embed it in a Qt widget with the ",[186,370,371],{},"backend_qtagg"," canvas instead of calling ",[186,374,375],{},"plt.show()",[174,377,379],{"id":378},"plot-a-distribution","Plot a distribution",[14,381,382],{},"A histogram is the first chart to draw for any numeric field: it shows the range, the typical values, skew and outliers at a glance.",[205,384,386],{"className":207,"code":385,"language":209,"meta":210,"style":210},"from qgis.core import QgsProject, QgsFeatureRequest\n\nbuildings = QgsProject.instance().mapLayersByName(\"buildings\")[0]\nreq = (QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry)\n       .setSubsetOfAttributes([\"height_m\"], buildings.fields()))\nheights = [f[\"height_m\"] for f in buildings.getFeatures(req)\n           if f[\"height_m\"] is not None and not (hasattr(f[\"height_m\"], \"isNull\") and f[\"height_m\"].isNull())]\n\nfig, ax = plt.subplots(figsize=(5, 3))\nax.hist(heights, bins=40, color=\"#0f766e\", edgecolor=\"white\", linewidth=0.4)\nax.axvline(sorted(heights)[len(heights) \u002F\u002F 2], color=\"#b45309\", linestyle=\"--\", label=\"median\")\nax.set_xlabel(\"building height (m)\")\nax.set_ylabel(\"buildings\")\nax.legend(frameon=False)\nfig.tight_layout()\nfig.savefig(\"\u002Fdata\u002Freports\u002Fheight_hist.svg\")\nplt.close(fig)\n",[186,387,388,401,405,427,437,448,475,531,535,565,611,666,677,687,702,708,719],{"__ignoreMap":210},[214,389,390,393,396,398],{"class":216,"line":217},[214,391,392],{"class":220},"from",[214,394,395],{"class":224}," qgis.core ",[214,397,221],{"class":220},[214,399,400],{"class":224}," QgsProject, QgsFeatureRequest\n",[214,402,403],{"class":216,"line":228},[214,404,263],{"emptyLinePlaceholder":262},[214,406,407,410,413,416,419,422,424],{"class":216,"line":245},[214,408,409],{"class":224},"buildings ",[214,411,412],{"class":220},"=",[214,414,415],{"class":224}," QgsProject.instance().mapLayersByName(",[214,417,418],{"class":234},"\"buildings\"",[214,420,421],{"class":224},")[",[214,423,46],{"class":281},[214,425,426],{"class":224},"]\n",[214,428,429,432,434],{"class":216,"line":259},[214,430,431],{"class":224},"req ",[214,433,412],{"class":220},[214,435,436],{"class":224}," (QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry)\n",[214,438,439,442,445],{"class":216,"line":266},[214,440,441],{"class":224},"       .setSubsetOfAttributes([",[214,443,444],{"class":234},"\"height_m\"",[214,446,447],{"class":224},"], buildings.fields()))\n",[214,449,450,453,455,458,460,463,466,469,472],{"class":216,"line":272},[214,451,452],{"class":224},"heights ",[214,454,412],{"class":220},[214,456,457],{"class":224}," [f[",[214,459,444],{"class":234},[214,461,462],{"class":224},"] ",[214,464,465],{"class":220},"for",[214,467,468],{"class":224}," f ",[214,470,471],{"class":220},"in",[214,473,474],{"class":224}," buildings.getFeatures(req)\n",[214,476,477,480,483,485,487,490,493,496,499,501,504,507,510,512,515,518,521,524,526,528],{"class":216,"line":297},[214,478,479],{"class":220},"           if",[214,481,482],{"class":224}," f[",[214,484,444],{"class":234},[214,486,462],{"class":224},[214,488,489],{"class":220},"is",[214,491,492],{"class":220}," not",[214,494,495],{"class":281}," None",[214,497,498],{"class":220}," and",[214,500,492],{"class":220},[214,502,503],{"class":224}," (",[214,505,506],{"class":281},"hasattr",[214,508,509],{"class":224},"(f[",[214,511,444],{"class":234},[214,513,514],{"class":224},"], ",[214,516,517],{"class":234},"\"isNull\"",[214,519,520],{"class":224},") ",[214,522,523],{"class":220},"and",[214,525,482],{"class":224},[214,527,444],{"class":234},[214,529,530],{"class":224},"].isNull())]\n",[214,532,533],{"class":216,"line":329},[214,534,263],{"emptyLinePlaceholder":262},[214,536,537,540,542,545,549,551,554,557,559,562],{"class":216,"line":346},[214,538,539],{"class":224},"fig, ax ",[214,541,412],{"class":220},[214,543,544],{"class":224}," plt.subplots(",[214,546,548],{"class":547},"s9osk","figsize",[214,550,412],{"class":220},[214,552,553],{"class":224},"(",[214,555,556],{"class":281},"5",[214,558,284],{"class":224},[214,560,561],{"class":281},"3",[214,563,564],{"class":224},"))\n",[214,566,568,571,574,576,578,580,583,585,588,590,593,595,598,600,603,605,608],{"class":216,"line":567},10,[214,569,570],{"class":224},"ax.hist(heights, ",[214,572,573],{"class":547},"bins",[214,575,412],{"class":220},[214,577,74],{"class":281},[214,579,284],{"class":224},[214,581,582],{"class":547},"color",[214,584,412],{"class":220},[214,586,587],{"class":234},"\"#0f766e\"",[214,589,284],{"class":224},[214,591,592],{"class":547},"edgecolor",[214,594,412],{"class":220},[214,596,597],{"class":234},"\"white\"",[214,599,284],{"class":224},[214,601,602],{"class":547},"linewidth",[214,604,412],{"class":220},[214,606,607],{"class":281},"0.4",[214,609,610],{"class":224},")\n",[214,612,614,617,620,623,626,629,632,635,637,639,641,644,646,649,651,654,656,659,661,664],{"class":216,"line":613},11,[214,615,616],{"class":224},"ax.axvline(",[214,618,619],{"class":281},"sorted",[214,621,622],{"class":224},"(heights)[",[214,624,625],{"class":281},"len",[214,627,628],{"class":224},"(heights) ",[214,630,631],{"class":220},"\u002F\u002F",[214,633,634],{"class":281}," 2",[214,636,514],{"class":224},[214,638,582],{"class":547},[214,640,412],{"class":220},[214,642,643],{"class":234},"\"#b45309\"",[214,645,284],{"class":224},[214,647,648],{"class":547},"linestyle",[214,650,412],{"class":220},[214,652,653],{"class":234},"\"--\"",[214,655,284],{"class":224},[214,657,658],{"class":547},"label",[214,660,412],{"class":220},[214,662,663],{"class":234},"\"median\"",[214,665,610],{"class":224},[214,667,669,672,675],{"class":216,"line":668},12,[214,670,671],{"class":224},"ax.set_xlabel(",[214,673,674],{"class":234},"\"building height (m)\"",[214,676,610],{"class":224},[214,678,680,683,685],{"class":216,"line":679},13,[214,681,682],{"class":224},"ax.set_ylabel(",[214,684,418],{"class":234},[214,686,610],{"class":224},[214,688,690,693,696,698,700],{"class":216,"line":689},14,[214,691,692],{"class":224},"ax.legend(",[214,694,695],{"class":547},"frameon",[214,697,412],{"class":220},[214,699,315],{"class":281},[214,701,610],{"class":224},[214,703,705],{"class":216,"line":704},15,[214,706,707],{"class":224},"fig.tight_layout()\n",[214,709,711,714,717],{"class":216,"line":710},16,[214,712,713],{"class":224},"fig.savefig(",[214,715,716],{"class":234},"\"\u002Fdata\u002Freports\u002Fheight_hist.svg\"",[214,718,610],{"class":224},[214,720,722],{"class":216,"line":721},17,[214,723,724],{"class":224},"plt.close(fig)\n",[14,726,727,729,730,733],{},[353,728,355],{}," Reading only the one field without geometry keeps this fast on large layers. NULL heights are excluded explicitly rather than turned into zeros, which would create a false spike at the left. Marking the median gives the reader an anchor; for skewed data it is more representative than the mean. ",[186,731,732],{},"plt.close(fig)"," releases memory — important in a loop that produces many figures, where unclosed figures accumulate until the process runs out.",[174,735,737],{"id":736},"compare-categories","Compare categories",[14,739,740],{},"Horizontal bar charts suit categories with long names — land uses, species, road classes — and make ranking obvious when sorted.",[14,742,743],{},[29,744,747,750,753,756,759,763,765,767,769,772,777,782,785,792,796,799,802,806,810,813,816,820,824],{"viewBox":745,"role":32,"ariaLabel":746,"xmlns":34},"0 0 760 240","Unsorted vertical bars with rotated labels compared with sorted horizontal bars that are easier to read",[36,748,749],{},"Sorted horizontal bars read best",[40,751,752],{},"Two versions of the same bar chart of floor area by building use. Unsorted vertical bars with rotated labels make comparison and reading hard. Sorted horizontal bars with labels at the left let the eye run down the ranking and read every category name without tilting the head.",[44,754],{"x":46,"y":46,"width":47,"height":755,"fill":49},"240",[51,757,758],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Sort, and turn the bars sideways",[44,760],{"x":61,"y":62,"width":761,"height":762,"rx":65,"fill":66,"stroke":67,"style":68},"330","160",[44,764],{"x":115,"y":108,"width":94,"height":89,"rx":46,"fill":67,"stroke":67,"style":68},[44,766],{"x":84,"y":107,"width":94,"height":84,"rx":46,"fill":67,"stroke":67,"style":68},[44,768],{"x":762,"y":88,"width":94,"height":74,"rx":46,"fill":67,"stroke":67,"style":68},[44,770],{"x":771,"y":149,"width":94,"height":80,"rx":46,"fill":67,"stroke":67,"style":68},"210",[44,773],{"x":774,"y":775,"width":94,"height":776,"rx":46,"fill":67,"stroke":67,"style":68},"260","165","25",[51,778,781],{"x":779,"y":780,"style":99,"fill":67,"textAnchor":57},"189","232","unsorted, rotated labels",[44,783],{"x":784,"y":62,"width":761,"height":762,"rx":65,"fill":66,"stroke":67,"style":68},"406",[51,786,791],{"x":145,"y":787,"style":788,"fill":789,"textAnchor":790},"82","text-anchor:end;font-size:10.0px;font-family:sans-serif","#2f3b35","end","residential",[44,793],{"x":794,"y":89,"width":130,"height":795,"rx":46,"fill":110,"stroke":110,"style":68},"506","16",[51,797,798],{"x":145,"y":97,"style":788,"fill":789,"textAnchor":790},"commercial",[44,800],{"x":794,"y":801,"width":762,"height":795,"rx":46,"fill":110,"stroke":110,"style":68},"96",[51,803,805],{"x":145,"y":804,"style":788,"fill":789,"textAnchor":790},"134","industrial",[44,807],{"x":794,"y":808,"width":809,"height":795,"rx":46,"fill":110,"stroke":110,"style":68},"122","128",[51,811,812],{"x":145,"y":762,"style":788,"fill":789,"textAnchor":790},"public",[44,814],{"x":794,"y":815,"width":107,"height":795,"rx":46,"fill":110,"stroke":110,"style":68},"148",[51,817,819],{"x":145,"y":818,"style":788,"fill":789,"textAnchor":790},"186","other",[44,821],{"x":794,"y":822,"width":823,"height":795,"rx":46,"fill":110,"stroke":110,"style":68},"174","46",[51,825,827],{"x":826,"y":780,"style":99,"fill":110,"textAnchor":57},"571","sorted, horizontal",[205,829,831],{"className":207,"code":830,"language":209,"meta":210,"style":210},"from collections import defaultdict\n\narea_by_use = defaultdict(float)\nreq = QgsFeatureRequest().setSubsetOfAttributes([\"use\"], buildings.fields())\nfor f in buildings.getFeatures(req):\n    area_by_use[str(f[\"use\"])] += f.geometry().area() \u002F 1e4     # hectares\n\nitems = sorted(area_by_use.items(), key=lambda kv: kv[1])\nfig, ax = plt.subplots(figsize=(5, 0.35 * len(items) + 0.8))\nax.barh([k for k, _ in items], [v for _, v in items], color=\"#2563eb\")\nax.set_xlabel(\"footprint area (ha)\")\nfor y, (_, v) in enumerate(items):\n    ax.text(v, y, f\" {v:,.0f}\", va=\"center\", fontsize=8)\nfig.tight_layout()\nfig.savefig(\"\u002Fdata\u002Freports\u002Farea_by_use.svg\")\nplt.close(fig)\n",[186,832,833,845,849,864,879,890,920,924,952,990,1024,1033,1048,1095,1099,1108],{"__ignoreMap":210},[214,834,835,837,840,842],{"class":216,"line":217},[214,836,392],{"class":220},[214,838,839],{"class":224}," collections ",[214,841,221],{"class":220},[214,843,844],{"class":224}," defaultdict\n",[214,846,847],{"class":216,"line":228},[214,848,263],{"emptyLinePlaceholder":262},[214,850,851,854,856,859,862],{"class":216,"line":245},[214,852,853],{"class":224},"area_by_use ",[214,855,412],{"class":220},[214,857,858],{"class":224}," defaultdict(",[214,860,861],{"class":281},"float",[214,863,610],{"class":224},[214,865,866,868,870,873,876],{"class":216,"line":259},[214,867,431],{"class":224},[214,869,412],{"class":220},[214,871,872],{"class":224}," QgsFeatureRequest().setSubsetOfAttributes([",[214,874,875],{"class":234},"\"use\"",[214,877,878],{"class":224},"], buildings.fields())\n",[214,880,881,883,885,887],{"class":216,"line":266},[214,882,465],{"class":220},[214,884,468],{"class":224},[214,886,471],{"class":220},[214,888,889],{"class":224}," buildings.getFeatures(req):\n",[214,891,892,895,898,900,902,905,908,911,914,917],{"class":216,"line":272},[214,893,894],{"class":224},"    area_by_use[",[214,896,897],{"class":281},"str",[214,899,509],{"class":224},[214,901,875],{"class":234},[214,903,904],{"class":224},"])] ",[214,906,907],{"class":220},"+=",[214,909,910],{"class":224}," f.geometry().area() ",[214,912,913],{"class":220},"\u002F",[214,915,916],{"class":281}," 1e4",[214,918,919],{"class":241},"     # hectares\n",[214,921,922],{"class":216,"line":297},[214,923,263],{"emptyLinePlaceholder":262},[214,925,926,929,931,934,937,940,943,946,949],{"class":216,"line":329},[214,927,928],{"class":224},"items ",[214,930,412],{"class":220},[214,932,933],{"class":281}," sorted",[214,935,936],{"class":224},"(area_by_use.items(), ",[214,938,939],{"class":547},"key",[214,941,942],{"class":220},"=lambda",[214,944,945],{"class":224}," kv: kv[",[214,947,948],{"class":281},"1",[214,950,951],{"class":224},"])\n",[214,953,954,956,958,960,962,964,966,968,970,973,976,979,982,985,988],{"class":216,"line":346},[214,955,539],{"class":224},[214,957,412],{"class":220},[214,959,544],{"class":224},[214,961,548],{"class":547},[214,963,412],{"class":220},[214,965,553],{"class":224},[214,967,556],{"class":281},[214,969,284],{"class":224},[214,971,972],{"class":281},"0.35",[214,974,975],{"class":220}," *",[214,977,978],{"class":281}," len",[214,980,981],{"class":224},"(items) ",[214,983,984],{"class":220},"+",[214,986,987],{"class":281}," 0.8",[214,989,564],{"class":224},[214,991,992,995,997,1000,1002,1005,1007,1010,1012,1015,1017,1019,1022],{"class":216,"line":567},[214,993,994],{"class":224},"ax.barh([k ",[214,996,465],{"class":220},[214,998,999],{"class":224}," k, _ ",[214,1001,471],{"class":220},[214,1003,1004],{"class":224}," items], [v ",[214,1006,465],{"class":220},[214,1008,1009],{"class":224}," _, v ",[214,1011,471],{"class":220},[214,1013,1014],{"class":224}," items], ",[214,1016,582],{"class":547},[214,1018,412],{"class":220},[214,1020,1021],{"class":234},"\"#2563eb\"",[214,1023,610],{"class":224},[214,1025,1026,1028,1031],{"class":216,"line":613},[214,1027,671],{"class":224},[214,1029,1030],{"class":234},"\"footprint area (ha)\"",[214,1032,610],{"class":224},[214,1034,1035,1037,1040,1042,1045],{"class":216,"line":668},[214,1036,465],{"class":220},[214,1038,1039],{"class":224}," y, (_, v) ",[214,1041,471],{"class":220},[214,1043,1044],{"class":281}," enumerate",[214,1046,1047],{"class":224},"(items):\n",[214,1049,1050,1053,1056,1059,1062,1065,1068,1071,1074,1076,1079,1081,1084,1086,1089,1091,1093],{"class":216,"line":679},[214,1051,1052],{"class":224},"    ax.text(v, y, ",[214,1054,1055],{"class":220},"f",[214,1057,1058],{"class":234},"\" ",[214,1060,1061],{"class":281},"{",[214,1063,1064],{"class":224},"v",[214,1066,1067],{"class":220},":,.0f",[214,1069,1070],{"class":281},"}",[214,1072,1073],{"class":234},"\"",[214,1075,284],{"class":224},[214,1077,1078],{"class":547},"va",[214,1080,412],{"class":220},[214,1082,1083],{"class":234},"\"center\"",[214,1085,284],{"class":224},[214,1087,1088],{"class":547},"fontsize",[214,1090,412],{"class":220},[214,1092,65],{"class":281},[214,1094,610],{"class":224},[214,1096,1097],{"class":216,"line":689},[214,1098,707],{"class":224},[214,1100,1101,1103,1106],{"class":216,"line":704},[214,1102,713],{"class":224},[214,1104,1105],{"class":234},"\"\u002Fdata\u002Freports\u002Farea_by_use.svg\"",[214,1107,610],{"class":224},[214,1109,1110],{"class":216,"line":710},[214,1111,724],{"class":224},[14,1113,1114,1116],{},[353,1115,355],{}," This chart needs geometry for the area, so the request keeps geometry but trims attributes to the one field. Sorting ascending puts the largest category at the top of a horizontal chart. Scaling the figure height with the number of categories keeps bar thickness constant whether there are five uses or twenty-five. Value labels at the end of each bar remove the need to read values off the axis.",[174,1118,1120],{"id":1119},"show-a-relationship","Show a relationship",[14,1122,1123],{},"A scatter plot shows whether two numeric fields move together — building height against floor count, pipe age against burst count. Colouring by a category adds a third variable.",[205,1125,1127],{"className":207,"code":1126,"language":209,"meta":210,"style":210},"req = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes(\n    [\"height_m\", \"floors\", \"use\"], buildings.fields())\nrows = [(f[\"height_m\"], f[\"floors\"], f[\"use\"]) for f in buildings.getFeatures(req)]\nrows = [r for r in rows if all(v is not None and not (hasattr(v, \"isNull\") and v.isNull()) for v in r)]\n\nfig, ax = plt.subplots(figsize=(5, 3.5))\nfor use, colour in ((\"residential\", \"#0f766e\"), (\"commercial\", \"#b45309\")):\n    pts = [(h, n) for h, n, u in rows if u == use]\n    ax.scatter([p[1] for p in pts], [p[0] for p in pts], s=6, alpha=0.5, color=colour, label=use)\nax.set_xlabel(\"floors\")\nax.set_ylabel(\"height (m)\")\nax.legend(frameon=False, markerscale=3)\nfig.tight_layout()\nfig.savefig(\"\u002Fdata\u002Freports\u002Fheight_vs_floors.png\")\nplt.close(fig)\n",[186,1128,1129,1138,1156,1189,1253,1257,1280,1312,1342,1408,1416,1425,1446,1450,1459],{"__ignoreMap":210},[214,1130,1131,1133,1135],{"class":216,"line":217},[214,1132,431],{"class":224},[214,1134,412],{"class":220},[214,1136,1137],{"class":224}," QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes(\n",[214,1139,1140,1143,1145,1147,1150,1152,1154],{"class":216,"line":228},[214,1141,1142],{"class":224},"    [",[214,1144,444],{"class":234},[214,1146,284],{"class":224},[214,1148,1149],{"class":234},"\"floors\"",[214,1151,284],{"class":224},[214,1153,875],{"class":234},[214,1155,878],{"class":224},[214,1157,1158,1161,1163,1166,1168,1171,1173,1175,1177,1180,1182,1184,1186],{"class":216,"line":245},[214,1159,1160],{"class":224},"rows ",[214,1162,412],{"class":220},[214,1164,1165],{"class":224}," [(f[",[214,1167,444],{"class":234},[214,1169,1170],{"class":224},"], f[",[214,1172,1149],{"class":234},[214,1174,1170],{"class":224},[214,1176,875],{"class":234},[214,1178,1179],{"class":224},"]) ",[214,1181,465],{"class":220},[214,1183,468],{"class":224},[214,1185,471],{"class":220},[214,1187,1188],{"class":224}," buildings.getFeatures(req)]\n",[214,1190,1191,1193,1195,1198,1200,1203,1205,1208,1211,1214,1217,1219,1221,1223,1225,1227,1229,1231,1234,1236,1238,1240,1243,1245,1248,1250],{"class":216,"line":259},[214,1192,1160],{"class":224},[214,1194,412],{"class":220},[214,1196,1197],{"class":224}," [r ",[214,1199,465],{"class":220},[214,1201,1202],{"class":224}," r ",[214,1204,471],{"class":220},[214,1206,1207],{"class":224}," rows ",[214,1209,1210],{"class":220},"if",[214,1212,1213],{"class":281}," all",[214,1215,1216],{"class":224},"(v ",[214,1218,489],{"class":220},[214,1220,492],{"class":220},[214,1222,495],{"class":281},[214,1224,498],{"class":220},[214,1226,492],{"class":220},[214,1228,503],{"class":224},[214,1230,506],{"class":281},[214,1232,1233],{"class":224},"(v, ",[214,1235,517],{"class":234},[214,1237,520],{"class":224},[214,1239,523],{"class":220},[214,1241,1242],{"class":224}," v.isNull()) ",[214,1244,465],{"class":220},[214,1246,1247],{"class":224}," v ",[214,1249,471],{"class":220},[214,1251,1252],{"class":224}," r)]\n",[214,1254,1255],{"class":216,"line":266},[214,1256,263],{"emptyLinePlaceholder":262},[214,1258,1259,1261,1263,1265,1267,1269,1271,1273,1275,1278],{"class":216,"line":272},[214,1260,539],{"class":224},[214,1262,412],{"class":220},[214,1264,544],{"class":224},[214,1266,548],{"class":547},[214,1268,412],{"class":220},[214,1270,553],{"class":224},[214,1272,556],{"class":281},[214,1274,284],{"class":224},[214,1276,1277],{"class":281},"3.5",[214,1279,564],{"class":224},[214,1281,1282,1284,1287,1289,1292,1295,1297,1299,1302,1305,1307,1309],{"class":216,"line":297},[214,1283,465],{"class":220},[214,1285,1286],{"class":224}," use, colour ",[214,1288,471],{"class":220},[214,1290,1291],{"class":224}," ((",[214,1293,1294],{"class":234},"\"residential\"",[214,1296,284],{"class":224},[214,1298,587],{"class":234},[214,1300,1301],{"class":224},"), (",[214,1303,1304],{"class":234},"\"commercial\"",[214,1306,284],{"class":224},[214,1308,643],{"class":234},[214,1310,1311],{"class":224},")):\n",[214,1313,1314,1317,1319,1322,1324,1327,1329,1331,1333,1336,1339],{"class":216,"line":329},[214,1315,1316],{"class":224},"    pts ",[214,1318,412],{"class":220},[214,1320,1321],{"class":224}," [(h, n) ",[214,1323,465],{"class":220},[214,1325,1326],{"class":224}," h, n, u ",[214,1328,471],{"class":220},[214,1330,1207],{"class":224},[214,1332,1210],{"class":220},[214,1334,1335],{"class":224}," u ",[214,1337,1338],{"class":220},"==",[214,1340,1341],{"class":224}," use]\n",[214,1343,1344,1347,1349,1351,1353,1356,1358,1361,1363,1365,1367,1369,1371,1374,1377,1379,1382,1384,1387,1389,1392,1394,1396,1398,1401,1403,1405],{"class":216,"line":346},[214,1345,1346],{"class":224},"    ax.scatter([p[",[214,1348,948],{"class":281},[214,1350,462],{"class":224},[214,1352,465],{"class":220},[214,1354,1355],{"class":224}," p ",[214,1357,471],{"class":220},[214,1359,1360],{"class":224}," pts], [p[",[214,1362,46],{"class":281},[214,1364,462],{"class":224},[214,1366,465],{"class":220},[214,1368,1355],{"class":224},[214,1370,471],{"class":220},[214,1372,1373],{"class":224}," pts], ",[214,1375,1376],{"class":547},"s",[214,1378,412],{"class":220},[214,1380,1381],{"class":281},"6",[214,1383,284],{"class":224},[214,1385,1386],{"class":547},"alpha",[214,1388,412],{"class":220},[214,1390,1391],{"class":281},"0.5",[214,1393,284],{"class":224},[214,1395,582],{"class":547},[214,1397,412],{"class":220},[214,1399,1400],{"class":224},"colour, ",[214,1402,658],{"class":547},[214,1404,412],{"class":220},[214,1406,1407],{"class":224},"use)\n",[214,1409,1410,1412,1414],{"class":216,"line":567},[214,1411,671],{"class":224},[214,1413,1149],{"class":234},[214,1415,610],{"class":224},[214,1417,1418,1420,1423],{"class":216,"line":613},[214,1419,682],{"class":224},[214,1421,1422],{"class":234},"\"height (m)\"",[214,1424,610],{"class":224},[214,1426,1427,1429,1431,1433,1435,1437,1440,1442,1444],{"class":216,"line":668},[214,1428,692],{"class":224},[214,1430,695],{"class":547},[214,1432,412],{"class":220},[214,1434,315],{"class":281},[214,1436,284],{"class":224},[214,1438,1439],{"class":547},"markerscale",[214,1441,412],{"class":220},[214,1443,561],{"class":281},[214,1445,610],{"class":224},[214,1447,1448],{"class":216,"line":679},[214,1449,707],{"class":224},[214,1451,1452,1454,1457],{"class":216,"line":689},[214,1453,713],{"class":224},[214,1455,1456],{"class":234},"\"\u002Fdata\u002Freports\u002Fheight_vs_floors.png\"",[214,1458,610],{"class":224},[214,1460,1461],{"class":216,"line":704},[214,1462,724],{"class":224},[14,1464,1465,1467,1468,1471,1472,1476],{},[353,1466,355],{}," Small markers with transparency reveal density where thousands of points overlap; with very large layers, ",[186,1469,1470],{},"ax.hexbin"," is clearer still. Points far from the main trend — a one-floor building 40 m high — are often data errors, which makes a scatter plot a useful quality check as well as an analysis; the ",[21,1473,1475],{"href":1474},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fvalidate-attribute-values-against-rules-pyqgis\u002F","attribute validation recipe"," can turn such observations into rules. PNG suits plots with many thousands of marks, where SVG files become large.",[174,1478,1480],{"id":1479},"plot-change-over-time","Plot change over time",[14,1482,1483],{},"Date fields become time series by counting or summing per period. Pandas does the resampling; matplotlib draws the line.",[205,1485,1487],{"className":207,"code":1486,"language":209,"meta":210,"style":210},"import pandas as pd\n\nincidents = QgsProject.instance().mapLayersByName(\"incidents\")[0]\nreq = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes(\n    [\"reported_at\"], incidents.fields())\ntimes = [f[\"reported_at\"].toPyDateTime() for f in incidents.getFeatures(req)\n         if hasattr(f[\"reported_at\"], \"toPyDateTime\") and not f[\"reported_at\"].isNull()]\nmonthly = pd.Series(1, index=pd.DatetimeIndex(times)).resample(\"MS\").sum()\n\nfig, ax = plt.subplots(figsize=(6, 2.6))\nax.plot(monthly.index, monthly.values, color=\"#15803d\", linewidth=1.6)\nax.plot(monthly.index, monthly.rolling(3, center=True).mean(), color=\"#2f3b35\",\n        linewidth=1, linestyle=\"--\", label=\"3-month mean\")\nax.set_ylabel(\"incidents per month\")\nax.legend(frameon=False)\nfig.autofmt_xdate()\nfig.tight_layout()\nfig.savefig(\"\u002Fdata\u002Freports\u002Fincidents_monthly.svg\")\nplt.close(fig)\n",[186,1488,1489,1501,1505,1523,1531,1541,1564,1594,1622,1626,1649,1672,1701,1729,1738,1750,1755,1759,1769],{"__ignoreMap":210},[214,1490,1491,1493,1496,1498],{"class":216,"line":217},[214,1492,221],{"class":220},[214,1494,1495],{"class":224}," pandas ",[214,1497,253],{"class":220},[214,1499,1500],{"class":224}," pd\n",[214,1502,1503],{"class":216,"line":228},[214,1504,263],{"emptyLinePlaceholder":262},[214,1506,1507,1510,1512,1514,1517,1519,1521],{"class":216,"line":245},[214,1508,1509],{"class":224},"incidents ",[214,1511,412],{"class":220},[214,1513,415],{"class":224},[214,1515,1516],{"class":234},"\"incidents\"",[214,1518,421],{"class":224},[214,1520,46],{"class":281},[214,1522,426],{"class":224},[214,1524,1525,1527,1529],{"class":216,"line":259},[214,1526,431],{"class":224},[214,1528,412],{"class":220},[214,1530,1137],{"class":224},[214,1532,1533,1535,1538],{"class":216,"line":266},[214,1534,1142],{"class":224},[214,1536,1537],{"class":234},"\"reported_at\"",[214,1539,1540],{"class":224},"], incidents.fields())\n",[214,1542,1543,1546,1548,1550,1552,1555,1557,1559,1561],{"class":216,"line":272},[214,1544,1545],{"class":224},"times ",[214,1547,412],{"class":220},[214,1549,457],{"class":224},[214,1551,1537],{"class":234},[214,1553,1554],{"class":224},"].toPyDateTime() ",[214,1556,465],{"class":220},[214,1558,468],{"class":224},[214,1560,471],{"class":220},[214,1562,1563],{"class":224}," incidents.getFeatures(req)\n",[214,1565,1566,1569,1572,1574,1576,1578,1581,1583,1585,1587,1589,1591],{"class":216,"line":297},[214,1567,1568],{"class":220},"         if",[214,1570,1571],{"class":281}," hasattr",[214,1573,509],{"class":224},[214,1575,1537],{"class":234},[214,1577,514],{"class":224},[214,1579,1580],{"class":234},"\"toPyDateTime\"",[214,1582,520],{"class":224},[214,1584,523],{"class":220},[214,1586,492],{"class":220},[214,1588,482],{"class":224},[214,1590,1537],{"class":234},[214,1592,1593],{"class":224},"].isNull()]\n",[214,1595,1596,1599,1601,1604,1606,1608,1611,1613,1616,1619],{"class":216,"line":329},[214,1597,1598],{"class":224},"monthly ",[214,1600,412],{"class":220},[214,1602,1603],{"class":224}," pd.Series(",[214,1605,948],{"class":281},[214,1607,284],{"class":224},[214,1609,1610],{"class":547},"index",[214,1612,412],{"class":220},[214,1614,1615],{"class":224},"pd.DatetimeIndex(times)).resample(",[214,1617,1618],{"class":234},"\"MS\"",[214,1620,1621],{"class":224},").sum()\n",[214,1623,1624],{"class":216,"line":346},[214,1625,263],{"emptyLinePlaceholder":262},[214,1627,1628,1630,1632,1634,1636,1638,1640,1642,1644,1647],{"class":216,"line":567},[214,1629,539],{"class":224},[214,1631,412],{"class":220},[214,1633,544],{"class":224},[214,1635,548],{"class":547},[214,1637,412],{"class":220},[214,1639,553],{"class":224},[214,1641,1381],{"class":281},[214,1643,284],{"class":224},[214,1645,1646],{"class":281},"2.6",[214,1648,564],{"class":224},[214,1650,1651,1654,1656,1658,1661,1663,1665,1667,1670],{"class":216,"line":613},[214,1652,1653],{"class":224},"ax.plot(monthly.index, monthly.values, ",[214,1655,582],{"class":547},[214,1657,412],{"class":220},[214,1659,1660],{"class":234},"\"#15803d\"",[214,1662,284],{"class":224},[214,1664,602],{"class":547},[214,1666,412],{"class":220},[214,1668,1669],{"class":281},"1.6",[214,1671,610],{"class":224},[214,1673,1674,1677,1679,1681,1684,1686,1689,1692,1694,1696,1699],{"class":216,"line":668},[214,1675,1676],{"class":224},"ax.plot(monthly.index, monthly.rolling(",[214,1678,561],{"class":281},[214,1680,284],{"class":224},[214,1682,1683],{"class":547},"center",[214,1685,412],{"class":220},[214,1687,1688],{"class":281},"True",[214,1690,1691],{"class":224},").mean(), ",[214,1693,582],{"class":547},[214,1695,412],{"class":220},[214,1697,1698],{"class":234},"\"#2f3b35\"",[214,1700,294],{"class":224},[214,1702,1703,1706,1708,1710,1712,1714,1716,1718,1720,1722,1724,1727],{"class":216,"line":679},[214,1704,1705],{"class":547},"        linewidth",[214,1707,412],{"class":220},[214,1709,948],{"class":281},[214,1711,284],{"class":224},[214,1713,648],{"class":547},[214,1715,412],{"class":220},[214,1717,653],{"class":234},[214,1719,284],{"class":224},[214,1721,658],{"class":547},[214,1723,412],{"class":220},[214,1725,1726],{"class":234},"\"3-month mean\"",[214,1728,610],{"class":224},[214,1730,1731,1733,1736],{"class":216,"line":689},[214,1732,682],{"class":224},[214,1734,1735],{"class":234},"\"incidents per month\"",[214,1737,610],{"class":224},[214,1739,1740,1742,1744,1746,1748],{"class":216,"line":704},[214,1741,692],{"class":224},[214,1743,695],{"class":547},[214,1745,412],{"class":220},[214,1747,315],{"class":281},[214,1749,610],{"class":224},[214,1751,1752],{"class":216,"line":710},[214,1753,1754],{"class":224},"fig.autofmt_xdate()\n",[214,1756,1757],{"class":216,"line":721},[214,1758,707],{"class":224},[214,1760,1762,1764,1767],{"class":216,"line":1761},18,[214,1763,713],{"class":224},[214,1765,1766],{"class":234},"\"\u002Fdata\u002Freports\u002Fincidents_monthly.svg\"",[214,1768,610],{"class":224},[214,1770,1772],{"class":216,"line":1771},19,[214,1773,724],{"class":224},[14,1775,1776,1778,1779,1781,1782,1785,1786,1789],{},[353,1777,355],{}," A series of ones indexed by timestamp, resampled to month starts (",[186,1780,1618],{},"), counts events per month including months with none — which a group-by on month strings would silently omit. A centred rolling mean shows the trend through month-to-month noise. ",[186,1783,1784],{},"autofmt_xdate"," rotates date labels so they do not overlap. For QGIS 4, where dates may arrive as Python datetimes, check for ",[186,1787,1788],{},"toPyDateTime"," as shown or convert with the cleaning helper from the pandas recipe.",[174,1791,1793],{"id":1792},"plot-raster-values","Plot raster values",[14,1795,1796],{},"Raster bands are NumPy arrays away from a histogram. Reading a band, masking NoData, and plotting the distribution is a quick way to understand a DEM or a classification before styling it.",[14,1798,1799],{},[29,1800,1803,1806,1809,1812,1826,1829,1836,1842,1846,1850,1854,1858,1861,1864],{"viewBox":1801,"role":32,"ariaLabel":1802,"xmlns":34},"0 0 760 212","A raster histogram without masking dominated by a NoData spike, and the same histogram after masking showing the real distribution",[36,1804,1805],{},"Masking NoData before plotting",[40,1807,1808],{},"A raster band read as an array contains valid elevations and NoData pixels, for example minus 9999. Plotting without a mask produces a histogram with one huge bar far to the left and everything else squashed. Masking NoData first gives a meaningful distribution of elevations.",[44,1810],{"x":46,"y":46,"width":47,"height":1811,"fill":49},"212",[1813,1814,1815],"defs",{},[1816,1817,1822],"marker",{"id":1818,"viewBox":1819,"refX":65,"refY":556,"markerWidth":1820,"markerHeight":1820,"orient":1821},"mplRasterArrow","0 0 10 10","7","auto-start-reverse",[1823,1824],"path",{"d":1825,"fill":789},"M0 0 L10 5 L0 10 z",[51,1827,1828],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Mask NoData first",[44,1830],{"x":61,"y":1831,"width":1832,"height":92,"rx":65,"fill":1833,"stroke":1834,"style":1835},"56","340","#fdf2e2","#b91c1c","stroke-width:2",[51,1837,1841],{"x":1838,"y":1839,"style":1840,"fill":1834,"textAnchor":57},"194","101.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","unmasked",[51,1843,1845],{"x":1838,"y":1844,"style":99,"fill":789,"textAnchor":57},"129.78","spike at −9999",[51,1847,1849],{"x":1838,"y":1848,"style":99,"fill":789,"textAnchor":57},"157.78","real values squashed",[44,1851],{"x":1852,"y":1831,"width":1832,"height":92,"rx":65,"fill":1853,"stroke":166,"style":1835},"396","#e8efe6",[51,1855,1857],{"x":1856,"y":1839,"style":1840,"fill":171,"textAnchor":57},"566","masked",[51,1859,1860],{"x":1856,"y":1844,"style":99,"fill":789,"textAnchor":57},"values only",[51,1862,1863],{"x":1856,"y":1848,"style":99,"fill":789,"textAnchor":57},"readable distribution",[216,1865],{"x1":1866,"y1":1867,"x2":1868,"y2":1867,"stroke":789,"style":1869},"364","126","392","stroke-width:1.8;marker-end:url(#mplRasterArrow)",[205,1871,1873],{"className":207,"code":1872,"language":209,"meta":210,"style":210},"import numpy as np\nfrom qgis.core import QgsRasterLayer\n\ndem = QgsRasterLayer(\"\u002Fdata\u002Fterrain\u002Fdem_10m.tif\", \"dem\")\nprov = dem.dataProvider()\nblock = prov.block(1, dem.extent(), dem.width(), dem.height())\narr = np.frombuffer(bytes(block.data()), dtype=np.float32).reshape(dem.height(), dem.width())\nnodata = prov.sourceNoDataValue(1) if prov.sourceHasNoDataValue(1) else None\nvalues = arr[arr != nodata] if nodata is not None else arr.ravel()\n\nfig, ax = plt.subplots(figsize=(5, 3))\nax.hist(values, bins=60, color=\"#b45309\")\nax.set_xlabel(\"elevation (m)\")\nax.set_ylabel(\"pixels\")\nfig.tight_layout()\nfig.savefig(\"\u002Fdata\u002Freports\u002Fdem_hist.svg\")\nplt.close(fig)\n",[186,1874,1875,1887,1898,1902,1922,1932,1947,1971,2000,2033,2037,2059,2080,2089,2098,2102,2111],{"__ignoreMap":210},[214,1876,1877,1879,1882,1884],{"class":216,"line":217},[214,1878,221],{"class":220},[214,1880,1881],{"class":224}," numpy ",[214,1883,253],{"class":220},[214,1885,1886],{"class":224}," np\n",[214,1888,1889,1891,1893,1895],{"class":216,"line":228},[214,1890,392],{"class":220},[214,1892,395],{"class":224},[214,1894,221],{"class":220},[214,1896,1897],{"class":224}," QgsRasterLayer\n",[214,1899,1900],{"class":216,"line":245},[214,1901,263],{"emptyLinePlaceholder":262},[214,1903,1904,1907,1909,1912,1915,1917,1920],{"class":216,"line":259},[214,1905,1906],{"class":224},"dem ",[214,1908,412],{"class":220},[214,1910,1911],{"class":224}," QgsRasterLayer(",[214,1913,1914],{"class":234},"\"\u002Fdata\u002Fterrain\u002Fdem_10m.tif\"",[214,1916,284],{"class":224},[214,1918,1919],{"class":234},"\"dem\"",[214,1921,610],{"class":224},[214,1923,1924,1927,1929],{"class":216,"line":266},[214,1925,1926],{"class":224},"prov ",[214,1928,412],{"class":220},[214,1930,1931],{"class":224}," dem.dataProvider()\n",[214,1933,1934,1937,1939,1942,1944],{"class":216,"line":272},[214,1935,1936],{"class":224},"block ",[214,1938,412],{"class":220},[214,1940,1941],{"class":224}," prov.block(",[214,1943,948],{"class":281},[214,1945,1946],{"class":224},", dem.extent(), dem.width(), dem.height())\n",[214,1948,1949,1952,1954,1957,1960,1963,1966,1968],{"class":216,"line":297},[214,1950,1951],{"class":224},"arr ",[214,1953,412],{"class":220},[214,1955,1956],{"class":224}," np.frombuffer(",[214,1958,1959],{"class":281},"bytes",[214,1961,1962],{"class":224},"(block.data()), ",[214,1964,1965],{"class":547},"dtype",[214,1967,412],{"class":220},[214,1969,1970],{"class":224},"np.float32).reshape(dem.height(), dem.width())\n",[214,1972,1973,1976,1978,1981,1983,1985,1987,1990,1992,1994,1997],{"class":216,"line":329},[214,1974,1975],{"class":224},"nodata ",[214,1977,412],{"class":220},[214,1979,1980],{"class":224}," prov.sourceNoDataValue(",[214,1982,948],{"class":281},[214,1984,520],{"class":224},[214,1986,1210],{"class":220},[214,1988,1989],{"class":224}," prov.sourceHasNoDataValue(",[214,1991,948],{"class":281},[214,1993,520],{"class":224},[214,1995,1996],{"class":220},"else",[214,1998,1999],{"class":281}," None\n",[214,2001,2002,2005,2007,2010,2013,2016,2018,2021,2023,2025,2027,2030],{"class":216,"line":346},[214,2003,2004],{"class":224},"values ",[214,2006,412],{"class":220},[214,2008,2009],{"class":224}," arr[arr ",[214,2011,2012],{"class":220},"!=",[214,2014,2015],{"class":224}," nodata] ",[214,2017,1210],{"class":220},[214,2019,2020],{"class":224}," nodata ",[214,2022,489],{"class":220},[214,2024,492],{"class":220},[214,2026,495],{"class":281},[214,2028,2029],{"class":220}," else",[214,2031,2032],{"class":224}," arr.ravel()\n",[214,2034,2035],{"class":216,"line":567},[214,2036,263],{"emptyLinePlaceholder":262},[214,2038,2039,2041,2043,2045,2047,2049,2051,2053,2055,2057],{"class":216,"line":613},[214,2040,539],{"class":224},[214,2042,412],{"class":220},[214,2044,544],{"class":224},[214,2046,548],{"class":547},[214,2048,412],{"class":220},[214,2050,553],{"class":224},[214,2052,556],{"class":281},[214,2054,284],{"class":224},[214,2056,561],{"class":281},[214,2058,564],{"class":224},[214,2060,2061,2064,2066,2068,2070,2072,2074,2076,2078],{"class":216,"line":668},[214,2062,2063],{"class":224},"ax.hist(values, ",[214,2065,573],{"class":547},[214,2067,412],{"class":220},[214,2069,115],{"class":281},[214,2071,284],{"class":224},[214,2073,582],{"class":547},[214,2075,412],{"class":220},[214,2077,643],{"class":234},[214,2079,610],{"class":224},[214,2081,2082,2084,2087],{"class":216,"line":679},[214,2083,671],{"class":224},[214,2085,2086],{"class":234},"\"elevation (m)\"",[214,2088,610],{"class":224},[214,2090,2091,2093,2096],{"class":216,"line":689},[214,2092,682],{"class":224},[214,2094,2095],{"class":234},"\"pixels\"",[214,2097,610],{"class":224},[214,2099,2100],{"class":216,"line":704},[214,2101,707],{"class":224},[214,2103,2104,2106,2109],{"class":216,"line":710},[214,2105,713],{"class":224},[214,2107,2108],{"class":234},"\"\u002Fdata\u002Freports\u002Fdem_hist.svg\"",[214,2110,610],{"class":224},[214,2112,2113],{"class":216,"line":721},[214,2114,724],{"class":224},[14,2116,2117,2119,2120,2123,2124,2128],{},[353,2118,355],{}," Reading the full extent at native resolution gives one value per pixel; for very large rasters, read a reduced width and height to sample instead. The dtype must match the band's data type — ",[186,2121,2122],{},"Float32"," here; ",[21,2125,2127],{"href":2126},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fread-raster-pixels-with-qgsrasterblock-pyqgis\u002F","reading pixels with QgsRasterBlock"," covers the mapping and tiling for large files. Masking NoData is essential: without it, a −9999 fill value dominates the histogram.",[174,2130,2132],{"id":2131},"put-charts-into-a-print-layout","Put charts into a print layout",[14,2134,2135],{},"Saved SVG charts drop straight into a QGIS print layout as picture items, so maps and charts share one page.",[205,2137,2139],{"className":207,"code":2138,"language":209,"meta":210,"style":210},"from qgis.core import QgsLayoutItemPicture, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes\n\nlayout = QgsProject.instance().layoutManager().layoutByName(\"Report\")\npic = QgsLayoutItemPicture(layout)\npic.setPicturePath(\"\u002Fdata\u002Freports\u002Fheight_hist.svg\")\npic.attemptMove(QgsLayoutPoint(200, 20, QgsUnitTypes.LayoutMillimeters))\npic.attemptResize(QgsLayoutSize(80, 50, QgsUnitTypes.LayoutMillimeters))\nlayout.addLayoutItem(pic)\n",[186,2140,2141,2152,2156,2171,2181,2190,2204,2218],{"__ignoreMap":210},[214,2142,2143,2145,2147,2149],{"class":216,"line":217},[214,2144,392],{"class":220},[214,2146,395],{"class":224},[214,2148,221],{"class":220},[214,2150,2151],{"class":224}," QgsLayoutItemPicture, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes\n",[214,2153,2154],{"class":216,"line":228},[214,2155,263],{"emptyLinePlaceholder":262},[214,2157,2158,2161,2163,2166,2169],{"class":216,"line":245},[214,2159,2160],{"class":224},"layout ",[214,2162,412],{"class":220},[214,2164,2165],{"class":224}," QgsProject.instance().layoutManager().layoutByName(",[214,2167,2168],{"class":234},"\"Report\"",[214,2170,610],{"class":224},[214,2172,2173,2176,2178],{"class":216,"line":259},[214,2174,2175],{"class":224},"pic ",[214,2177,412],{"class":220},[214,2179,2180],{"class":224}," QgsLayoutItemPicture(layout)\n",[214,2182,2183,2186,2188],{"class":216,"line":266},[214,2184,2185],{"class":224},"pic.setPicturePath(",[214,2187,716],{"class":234},[214,2189,610],{"class":224},[214,2191,2192,2195,2197,2199,2201],{"class":216,"line":272},[214,2193,2194],{"class":224},"pic.attemptMove(QgsLayoutPoint(",[214,2196,130],{"class":281},[214,2198,284],{"class":224},[214,2200,73],{"class":281},[214,2202,2203],{"class":224},", QgsUnitTypes.LayoutMillimeters))\n",[214,2205,2206,2209,2211,2213,2216],{"class":216,"line":297},[214,2207,2208],{"class":224},"pic.attemptResize(QgsLayoutSize(",[214,2210,107],{"class":281},[214,2212,284],{"class":224},[214,2214,2215],{"class":281},"50",[214,2217,2203],{"class":224},[214,2219,2220],{"class":216,"line":329},[214,2221,2222],{"class":224},"layout.addLayoutItem(pic)\n",[14,2224,2225,2227,2228,2232],{},[353,2226,355],{}," SVG pictures scale without blurring in PDF exports, which is why saving charts as SVG pays off. The picture path is stored in the layout, so regenerating the SVG with new data and re-exporting updates the report. ",[21,2229,2231],{"href":2230},"\u002Fpyqgis-cartography-visualization\u002Fdiagrams-and-charts-on-maps\u002Fadd-chart-to-print-layout-pyqgis\u002F","Adding a chart to a print layout"," covers sizing, data-driven charts per atlas page, and QGIS's own chart item.",[174,2234,2236],{"id":2235},"qgis-version-compatibility","QGIS version compatibility",[14,2238,2239,2240,2243,2244,2247,2248,189],{},"Matplotlib with the Agg backend works on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. For embedding in Qt widgets, QGIS 3 uses ",[186,2241,2242],{},"matplotlib.backends.backend_qtagg"," with PyQt5 and QGIS 4 the same module with PyQt6; matplotlib 3.5 or newer selects the right bindings. On QGIS 4, ",[186,2245,2246],{},"QgsUnitTypes.LayoutMillimeters"," is ",[186,2249,2250],{},"Qgis.LayoutUnit.Millimeters",[174,2252,2254],{"id":2253},"troubleshooting","Troubleshooting",[179,2256,2257,2266,2274,2284],{},[182,2258,2259,2262,2263,2265],{},[353,2260,2261],{},"QGIS freezes when plotting."," An interactive backend opened a window; call ",[186,2264,359],{}," first and save to files.",[182,2267,2268,2271,2272,189],{},[353,2269,2270],{},"Memory grows with every chart."," Figures were not closed; call ",[186,2273,732],{},[182,2275,2276,2279,2280,2283],{},[353,2277,2278],{},"Text in SVG looks different in the layout."," The font is not installed where QGIS runs; use a common font or keep ",[186,2281,2282],{},"svg.fonttype"," as paths.",[182,2285,2286,2289],{},[353,2287,2288],{},"Histogram is a single bar."," NoData or a placeholder value was not masked.",[174,2291,2293],{"id":2292},"conclusion","Conclusion",[14,2295,2296],{},"Use the Agg backend and save to files, read only the fields you need, exclude NULLs rather than zero them, choose histograms, sorted horizontal bars, scatter plots and resampled lines to match the question, mask NoData for rasters, save SVG for layouts and close every figure.",[174,2298,2300],{"id":2299},"frequently-asked-questions","Frequently Asked Questions",[14,2302,2303,2306],{},[353,2304,2305],{},"Can I use seaborn or plotly?","\nYes. Seaborn builds on matplotlib and works the same way; plotly writes HTML, which suits web output rather than print layouts.",[14,2308,2309,2312,2313,2316],{},[353,2310,2311],{},"How do I make charts match the map's colours?","\nRead symbol colours from the renderer — for a categorized renderer, each category's ",[186,2314,2315],{},"symbol().color().name()"," — and pass them to matplotlib.",[14,2318,2319,2322],{},[353,2320,2321],{},"Can charts update automatically when data changes?","\nRe-run the script; or use the Data Plotly plugin for interactive charts inside QGIS.",[14,2324,2325,2328],{},[353,2326,2327],{},"What DPI should PNG charts use?","\n200–300 for print; 100 is enough for screens.",[174,2330,2332],{"id":2331},"related","Related",[179,2334,2335,2340,2345,2350,2356],{},[182,2336,2337,2339],{},[21,2338,24],{"href":23}," — the guide this recipe belongs to",[182,2341,2342],{},[21,2343,2344],{"href":195},"Analyse an Attribute Table with Pandas in PyQGIS",[182,2346,2347],{},[21,2348,2349],{"href":2230},"Add a Chart to a Print Layout in PyQGIS",[182,2351,2352],{},[21,2353,2355],{"href":2354},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fextract-elevation-profile-along-line-pyqgis\u002F","Extract an Elevation Profile Along a Line in PyQGIS",[182,2357,2358],{},[21,2359,2361],{"href":2360},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fcalculate-raster-statistics-pyqgis\u002F","Calculate Raster Statistics in PyQGIS",[2363,2364,2365],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":210,"searchDepth":228,"depth":228,"links":2367},[2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380],{"id":176,"depth":228,"text":177},{"id":199,"depth":228,"text":200},{"id":378,"depth":228,"text":379},{"id":736,"depth":228,"text":737},{"id":1119,"depth":228,"text":1120},{"id":1479,"depth":228,"text":1480},{"id":1792,"depth":228,"text":1793},{"id":2131,"depth":228,"text":2132},{"id":2235,"depth":228,"text":2236},{"id":2253,"depth":228,"text":2254},{"id":2292,"depth":228,"text":2293},{"id":2299,"depth":228,"text":2300},{"id":2331,"depth":228,"text":2332},"Make charts from QGIS layers with matplotlib — histograms, bar charts, scatter plots and time series from attributes, raster value distributions, elevation profiles — saved as SVG or PNG for reports and layouts, without blocking or crashing the QGIS interface.","md",{"slug":2384,"type":2385,"breadcrumb":2386,"datePublished":2387,"dateModified":2387},"plot-layer-data-with-matplotlib-pyqgis","article","Plot Layer Data with Matplotlib","2026-10-02","\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fplot-layer-data-with-matplotlib-pyqgis",{"title":5,"description":2381},"spatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fplot-layer-data-with-matplotlib-pyqgis\u002Findex","VizKHTN6ri1b04m0TigimfI-SGvpUIZWe2yZZm2lGZ0",1790966264402]