[{"data":1,"prerenderedAt":1573},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fload-and-style-mesh-layer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1562,"extension":1563,"meta":1564,"navigation":260,"path":1569,"seo":1570,"stem":1571,"__hash__":1572},"docs\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fload-and-style-mesh-layer-pyqgis\u002Findex.md","Load and Style a Mesh Layer in PyQGIS",{"type":7,"value":8,"toc":1548},"minimark",[9,13,17,26,204,209,223,227,493,506,513,552,557,561,655,668,672,825,857,861,967,1127,1132,1136,1225,1230,1234,1237,1384,1389,1393,1400,1404,1460,1464,1467,1471,1486,1492,1503,1509,1513,1544],[10,11,5],"h1",{"id":12},"load-and-style-a-mesh-layer-in-pyqgis",[14,15,16],"p",{},"A mesh layer is what a numerical model actually produces: values on an unstructured grid of triangles or quadrilaterals, usually several variables, usually many timesteps. QGIS reads them through MDAL and renders them natively, and the whole thing is scriptable — but the addressing scheme is unlike anything else in the API, and the renderer settings follow a copy-modify-write pattern that silently does nothing if you get it wrong.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002F","Temporal & 3D Visualization in PyQGIS",". It covers loading, enumerating dataset groups and timesteps, sampling values at a point, and configuring the scalar and vector renderers.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,97,100,103,106,134,138,145,151,155,163,169,172,176,181,185,194,199],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 330","img","The structure of a mesh file: a fixed grid of faces, and dataset groups each holding a value per vertex for each timestep","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"One grid, several variables, many timesteps",[40,41,42],"desc",{},"The mesh geometry is fixed: a set of vertices joined into triangular or quadrilateral faces. On top of it sit dataset groups, each one a variable such as depth or velocity. Each group holds one dataset per timestep, and each dataset holds one value per vertex or per face.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","330","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"mshStructArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","QgsMeshDatasetIndex(group, dataset) addresses everything",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"24","46","248","196","10","#efeadd","#59645f","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":73,"textAnchor":74},"148","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","the grid — fixed",[63,93],{"d":94,"fill":95,"stroke":84,"style":96},"M52 200 L100 128 L156 190 Z","#e7e2d4","stroke-width:1.8",[63,98],{"d":99,"fill":95,"stroke":84,"style":96},"M100 128 L164 112 L156 190 Z",[63,101],{"d":102,"fill":95,"stroke":84,"style":96},"M156 190 L164 112 L232 148 Z",[63,104],{"d":105,"fill":95,"stroke":84,"style":96},"M156 190 L232 148 L228 208 Z",[107,108,109,115,119,123,127,130],"g",{"fill":66},[110,111],"circle",{"cx":112,"cy":113,"r":114},"52","200","3",[110,116],{"cx":117,"cy":118,"r":114},"100","128",[110,120],{"cx":121,"cy":122,"r":114},"156","190",[110,124],{"cx":125,"cy":126,"r":114},"164","112",[110,128],{"cx":129,"cy":88,"r":114},"232",[110,131],{"cx":132,"cy":133,"r":114},"228","208",[68,135,137],{"x":88,"y":132,"style":136,"fill":84,"textAnchor":74},"text-anchor:middle;font-size:10px;font-family:sans-serif","vertices and faces, never change",[139,140],"line",{"x1":141,"y1":142,"x2":143,"y2":142,"stroke":66,"style":144},"272","144","298","stroke-width:2;marker-end:url(#mshStructArrow)",[44,146],{"x":147,"y":79,"width":148,"height":81,"rx":82,"fill":149,"stroke":150,"style":85},"306","430","#fffdf7","#0f766e",[68,152,154],{"x":153,"y":89,"style":90,"fill":150,"textAnchor":74},"521","dataset groups",[44,156],{"x":157,"y":158,"width":159,"height":160,"rx":161,"fill":162,"stroke":150,"style":96},"326","88","390","40","6","#eef7f4",[68,164,168],{"x":165,"y":166,"style":167,"fill":66},"344","113","font-size:10.5px;font-family:sans-serif","group 0 · \"Bed Elevation\" · scalar · 1 dataset",[44,170],{"x":157,"y":171,"width":159,"height":160,"rx":161,"fill":162,"stroke":150,"style":96},"134",[68,173,175],{"x":165,"y":174,"style":167,"fill":66},"159","group 1 · \"Water Depth\" · scalar · 72 datasets",[44,177],{"x":157,"y":178,"width":159,"height":160,"rx":161,"fill":179,"stroke":180,"style":96},"180","#eff3ff","#2563eb",[68,182,184],{"x":165,"y":183,"style":167,"fill":66},"205","group 2 · \"Velocity\" · vector · 72 datasets",[44,186],{"x":187,"y":188,"width":189,"height":190,"rx":58,"fill":191,"stroke":192,"style":193},"80","262","600","56","#fdf2e2","#b45309","stroke-width:2",[68,195,198],{"x":70,"y":196,"style":197,"fill":66,"textAnchor":74},"286","text-anchor:middle;font-size:11px;font-family:sans-serif","group indices are file order — always enumerate, never hard-code",[68,200,203],{"x":70,"y":201,"style":202,"fill":84,"textAnchor":74},"308","text-anchor:middle;font-size:10.5px;font-family:sans-serif","a re-run of the model with an extra output variable renumbers everything after it",[205,206,208],"h2",{"id":207},"prerequisites","Prerequisites",[210,211,212,220],"ul",{},[213,214,215,219],"li",{},[216,217,218],"strong",{},"QGIS 3.34 LTR"," or newer. Mesh support arrived in 3.4 and matured through 3.14.",[213,221,222],{},"A mesh file — NetCDF, GRIB, UGRID, XMDF, 2DM, or another format MDAL reads.",[205,224,226],{"id":225},"load-and-enumerate","Load and enumerate",[228,229,234],"pre",{"className":230,"code":231,"language":232,"meta":233,"style":233},"language-python shiki shiki-themes github-dark","from qgis.core import QgsMeshLayer, QgsMeshDatasetIndex, QgsProject\n\nmesh = QgsMeshLayer(\"\u002Fdata\u002Fmodel\u002Fflood.nc\", \"flood\", \"mdal\")\nif not mesh.isValid():\n    raise SystemExit(\"MDAL could not read this file\")\n\nprint(mesh.crs().authid(), mesh.extent().toString(0))\n\nfor group in range(mesh.datasetGroupCount()):\n    index = QgsMeshDatasetIndex(group, 0)\n    meta = mesh.datasetGroupMetadata(index)\n    steps = mesh.datasetCount(index)\n    kind = \"vector\" if meta.isVector() else \"scalar\"\n    print(f\"{group}: {meta.name()!r} {kind}, {steps} timestep(s)\")\n\nQgsProject.instance().addMapLayer(mesh)\n","python","",[235,236,237,255,262,292,304,322,327,341,346,364,379,390,401,424,482,487],"code",{"__ignoreMap":233},[238,239,241,245,249,252],"span",{"class":139,"line":240},1,[238,242,244],{"class":243},"snl16","from",[238,246,248],{"class":247},"s95oV"," qgis.core ",[238,250,251],{"class":243},"import",[238,253,254],{"class":247}," QgsMeshLayer, QgsMeshDatasetIndex, QgsProject\n",[238,256,258],{"class":139,"line":257},2,[238,259,261],{"emptyLinePlaceholder":260},true,"\n",[238,263,265,268,271,274,278,281,284,286,289],{"class":139,"line":264},3,[238,266,267],{"class":247},"mesh ",[238,269,270],{"class":243},"=",[238,272,273],{"class":247}," QgsMeshLayer(",[238,275,277],{"class":276},"sU2Wk","\"\u002Fdata\u002Fmodel\u002Fflood.nc\"",[238,279,280],{"class":247},", ",[238,282,283],{"class":276},"\"flood\"",[238,285,280],{"class":247},[238,287,288],{"class":276},"\"mdal\"",[238,290,291],{"class":247},")\n",[238,293,295,298,301],{"class":139,"line":294},4,[238,296,297],{"class":243},"if",[238,299,300],{"class":243}," not",[238,302,303],{"class":247}," mesh.isValid():\n",[238,305,307,310,314,317,320],{"class":139,"line":306},5,[238,308,309],{"class":243},"    raise",[238,311,313],{"class":312},"sDLfK"," SystemExit",[238,315,316],{"class":247},"(",[238,318,319],{"class":276},"\"MDAL could not read this file\"",[238,321,291],{"class":247},[238,323,325],{"class":139,"line":324},6,[238,326,261],{"emptyLinePlaceholder":260},[238,328,330,333,336,338],{"class":139,"line":329},7,[238,331,332],{"class":312},"print",[238,334,335],{"class":247},"(mesh.crs().authid(), mesh.extent().toString(",[238,337,46],{"class":312},[238,339,340],{"class":247},"))\n",[238,342,344],{"class":139,"line":343},8,[238,345,261],{"emptyLinePlaceholder":260},[238,347,349,352,355,358,361],{"class":139,"line":348},9,[238,350,351],{"class":243},"for",[238,353,354],{"class":247}," group ",[238,356,357],{"class":243},"in",[238,359,360],{"class":312}," range",[238,362,363],{"class":247},"(mesh.datasetGroupCount()):\n",[238,365,367,370,372,375,377],{"class":139,"line":366},10,[238,368,369],{"class":247},"    index ",[238,371,270],{"class":243},[238,373,374],{"class":247}," QgsMeshDatasetIndex(group, ",[238,376,46],{"class":312},[238,378,291],{"class":247},[238,380,382,385,387],{"class":139,"line":381},11,[238,383,384],{"class":247},"    meta ",[238,386,270],{"class":243},[238,388,389],{"class":247}," mesh.datasetGroupMetadata(index)\n",[238,391,393,396,398],{"class":139,"line":392},12,[238,394,395],{"class":247},"    steps ",[238,397,270],{"class":243},[238,399,400],{"class":247}," mesh.datasetCount(index)\n",[238,402,404,407,409,412,415,418,421],{"class":139,"line":403},13,[238,405,406],{"class":247},"    kind ",[238,408,270],{"class":243},[238,410,411],{"class":276}," \"vector\"",[238,413,414],{"class":243}," if",[238,416,417],{"class":247}," meta.isVector() ",[238,419,420],{"class":243},"else",[238,422,423],{"class":276}," \"scalar\"\n",[238,425,427,430,432,435,438,441,444,447,450,452,455,458,460,463,466,468,470,472,475,477,480],{"class":139,"line":426},14,[238,428,429],{"class":312},"    print",[238,431,316],{"class":247},[238,433,434],{"class":243},"f",[238,436,437],{"class":276},"\"",[238,439,440],{"class":312},"{",[238,442,443],{"class":247},"group",[238,445,446],{"class":312},"}",[238,448,449],{"class":276},": ",[238,451,440],{"class":312},[238,453,454],{"class":247},"meta.name()",[238,456,457],{"class":243},"!r",[238,459,446],{"class":312},[238,461,462],{"class":312}," {",[238,464,465],{"class":247},"kind",[238,467,446],{"class":312},[238,469,280],{"class":276},[238,471,440],{"class":312},[238,473,474],{"class":247},"steps",[238,476,446],{"class":312},[238,478,479],{"class":276}," timestep(s)\"",[238,481,291],{"class":247},[238,483,485],{"class":139,"line":484},15,[238,486,261],{"emptyLinePlaceholder":260},[238,488,490],{"class":139,"line":489},16,[238,491,492],{"class":247},"QgsProject.instance().addMapLayer(mesh)\n",[14,494,495,498,499,501,502,505],{},[216,496,497],{},"Breakdown:"," The provider key is always ",[235,500,288],{}," — the specific driver is chosen from the file, so there is no per-format constructor to remember. ",[235,503,504],{},"QgsMeshDatasetIndex(group, dataset)"," is the addressing pair used everywhere in this API; group metadata is the same for every timestep, so passing dataset 0 to fetch it is the convention. A group with one dataset is static — bed elevation, roughness, a mask — and one with many is a time series. Enumerating rather than hard-coding group numbers is not defensive programming, it is necessary: the indices are file order, and a model re-run with one extra output variable shifts every group after it.",[14,507,508,509,512],{},"Many meshes carry no CRS, in which case ",[235,510,511],{},"authid()"," comes back empty and the layer lands wherever the project puts it. Set it explicitly:",[228,514,516],{"className":230,"code":515,"language":232,"meta":233,"style":233},"from qgis.core import QgsCoordinateReferenceSystem\n\nif not mesh.crs().isValid():\n    mesh.setCrs(QgsCoordinateReferenceSystem(\"EPSG:27700\"))\n",[235,517,518,529,533,542],{"__ignoreMap":233},[238,519,520,522,524,526],{"class":139,"line":240},[238,521,244],{"class":243},[238,523,248],{"class":247},[238,525,251],{"class":243},[238,527,528],{"class":247}," QgsCoordinateReferenceSystem\n",[238,530,531],{"class":139,"line":257},[238,532,261],{"emptyLinePlaceholder":260},[238,534,535,537,539],{"class":139,"line":264},[238,536,297],{"class":243},[238,538,300],{"class":243},[238,540,541],{"class":247}," mesh.crs().isValid():\n",[238,543,544,547,550],{"class":139,"line":294},[238,545,546],{"class":247},"    mesh.setCrs(QgsCoordinateReferenceSystem(",[238,548,549],{"class":276},"\"EPSG:27700\"",[238,551,340],{"class":247},[14,553,554,556],{},[216,555,497],{}," Unlike a point cloud, where the fix belongs in the file, setting the CRS on a mesh layer is the normal remedy — most model formats have no place to store one, so QGIS expects you to declare it, and it is saved with the project. Get it wrong and the mesh is in the right shape in the wrong place, which is obvious against a basemap and invisible on its own.",[205,558,560],{"id":559},"timesteps-and-reference-time","Timesteps and reference time",[228,562,564],{"className":230,"code":563,"language":232,"meta":233,"style":233},"group = 1\ncount = mesh.datasetCount(QgsMeshDatasetIndex(group, 0))\n\nfor step in range(min(count, 5)):\n    meta = mesh.datasetMetadata(QgsMeshDatasetIndex(group, step))\n    print(step, meta.time(), \"hours after reference\")\n\nprint(\"reference time:\", mesh.temporalProperties().referenceTime())\n",[235,565,566,576,590,594,618,627,639,643],{"__ignoreMap":233},[238,567,568,571,573],{"class":139,"line":240},[238,569,570],{"class":247},"group ",[238,572,270],{"class":243},[238,574,575],{"class":312}," 1\n",[238,577,578,581,583,586,588],{"class":139,"line":257},[238,579,580],{"class":247},"count ",[238,582,270],{"class":243},[238,584,585],{"class":247}," mesh.datasetCount(QgsMeshDatasetIndex(group, ",[238,587,46],{"class":312},[238,589,340],{"class":247},[238,591,592],{"class":139,"line":264},[238,593,261],{"emptyLinePlaceholder":260},[238,595,596,598,601,603,605,607,610,613,615],{"class":139,"line":294},[238,597,351],{"class":243},[238,599,600],{"class":247}," step ",[238,602,357],{"class":243},[238,604,360],{"class":312},[238,606,316],{"class":247},[238,608,609],{"class":312},"min",[238,611,612],{"class":247},"(count, ",[238,614,59],{"class":312},[238,616,617],{"class":247},")):\n",[238,619,620,622,624],{"class":139,"line":306},[238,621,384],{"class":247},[238,623,270],{"class":243},[238,625,626],{"class":247}," mesh.datasetMetadata(QgsMeshDatasetIndex(group, step))\n",[238,628,629,631,634,637],{"class":139,"line":324},[238,630,429],{"class":312},[238,632,633],{"class":247},"(step, meta.time(), ",[238,635,636],{"class":276},"\"hours after reference\"",[238,638,291],{"class":247},[238,640,641],{"class":139,"line":329},[238,642,261],{"emptyLinePlaceholder":260},[238,644,645,647,649,652],{"class":139,"line":343},[238,646,332],{"class":312},[238,648,316],{"class":247},[238,650,651],{"class":276},"\"reference time:\"",[238,653,654],{"class":247},", mesh.temporalProperties().referenceTime())\n",[14,656,657,659,660,663,664,667],{},[216,658,497],{}," ",[235,661,662],{},"meta.time()"," returns hours since the layer's reference time, not an absolute datetime — that is the crucial detail, and it is why setting the reference time correctly matters. Model files usually carry one; where they do not, QGIS defaults to something arbitrary and every timestep is offset by the difference. ",[235,665,666],{},"mesh.temporalProperties().setReferenceTime()"," fixes it, and doing so shifts the whole animation onto the real calendar so the mesh lines up with rainfall records, tide gauges or anything else time-aware in the project.",[205,669,671],{"id":670},"sample-a-value","Sample a value",[228,673,675],{"className":230,"code":674,"language":232,"meta":233,"style":233},"import math\nfrom qgis.core import QgsPointXY\n\npoint = QgsPointXY(445120, 187330)\nseries = []\nfor step in range(count):\n    value = mesh.datasetValue(QgsMeshDatasetIndex(group, step), point)\n    scalar = value.scalar()\n    series.append(None if math.isnan(scalar) else scalar)\n\nprint(\"peak depth:\", max(v for v in series if v is not None))\n",[235,676,677,684,695,699,719,729,742,752,762,780,784],{"__ignoreMap":233},[238,678,679,681],{"class":139,"line":240},[238,680,251],{"class":243},[238,682,683],{"class":247}," math\n",[238,685,686,688,690,692],{"class":139,"line":257},[238,687,244],{"class":243},[238,689,248],{"class":247},[238,691,251],{"class":243},[238,693,694],{"class":247}," QgsPointXY\n",[238,696,697],{"class":139,"line":264},[238,698,261],{"emptyLinePlaceholder":260},[238,700,701,704,706,709,712,714,717],{"class":139,"line":294},[238,702,703],{"class":247},"point ",[238,705,270],{"class":243},[238,707,708],{"class":247}," QgsPointXY(",[238,710,711],{"class":312},"445120",[238,713,280],{"class":247},[238,715,716],{"class":312},"187330",[238,718,291],{"class":247},[238,720,721,724,726],{"class":139,"line":306},[238,722,723],{"class":247},"series ",[238,725,270],{"class":243},[238,727,728],{"class":247}," []\n",[238,730,731,733,735,737,739],{"class":139,"line":324},[238,732,351],{"class":243},[238,734,600],{"class":247},[238,736,357],{"class":243},[238,738,360],{"class":312},[238,740,741],{"class":247},"(count):\n",[238,743,744,747,749],{"class":139,"line":329},[238,745,746],{"class":247},"    value ",[238,748,270],{"class":243},[238,750,751],{"class":247}," mesh.datasetValue(QgsMeshDatasetIndex(group, step), point)\n",[238,753,754,757,759],{"class":139,"line":343},[238,755,756],{"class":247},"    scalar ",[238,758,270],{"class":243},[238,760,761],{"class":247}," value.scalar()\n",[238,763,764,767,770,772,775,777],{"class":139,"line":348},[238,765,766],{"class":247},"    series.append(",[238,768,769],{"class":312},"None",[238,771,414],{"class":243},[238,773,774],{"class":247}," math.isnan(scalar) ",[238,776,420],{"class":243},[238,778,779],{"class":247}," scalar)\n",[238,781,782],{"class":139,"line":366},[238,783,261],{"emptyLinePlaceholder":260},[238,785,786,788,790,793,795,798,801,803,806,808,811,813,815,818,820,823],{"class":139,"line":381},[238,787,332],{"class":312},[238,789,316],{"class":247},[238,791,792],{"class":276},"\"peak depth:\"",[238,794,280],{"class":247},[238,796,797],{"class":312},"max",[238,799,800],{"class":247},"(v ",[238,802,351],{"class":243},[238,804,805],{"class":247}," v ",[238,807,357],{"class":243},[238,809,810],{"class":247}," series ",[238,812,297],{"class":243},[238,814,805],{"class":247},[238,816,817],{"class":243},"is",[238,819,300],{"class":243},[238,821,822],{"class":312}," None",[238,824,340],{"class":247},[14,826,827,659,829,832,833,836,837,840,841,844,845,848,849,852,853,856],{},[216,828,497],{},[235,830,831],{},"datasetValue"," interpolates within the face containing the point, so it works anywhere inside the mesh rather than only at vertices. Outside the mesh it returns NaN, which is why the ",[235,834,835],{},"isnan"," check is required — NaN compares false against everything, so a ",[235,838,839],{},"max()"," over raw values silently returns a real number while a ",[235,842,843],{},"sum()"," returns NaN, and neither tells you the point was outside. For a vector group, ",[235,846,847],{},"value.x()"," and ",[235,850,851],{},"value.y()"," give the components and ",[235,854,855],{},"value.scalar()"," gives the magnitude. Sampling a whole series at one point like this is the fastest route from a model output to a chart, and it is a loop rather than a bulk call.",[205,858,860],{"id":859},"style-the-scalar-surface","Style the scalar surface",[14,862,863],{},[29,864,867,870,873,875,882,885,889,895,899,904,907,910,913,916,919,923,926,929,935,939,942,947,951,954,958,963],{"viewBox":865,"role":32,"ariaLabel":866,"xmlns":34},"0 0 760 306","The copy modify write pattern for mesh renderer settings, showing that a modified copy has no effect until it is written back to the layer",[36,868,869],{},"Settings are copies until you put them back",[40,871,872],{},"Calling rendererSettings returns a copy of the layer's settings. Modifying that copy changes nothing on the layer. Only calling setRendererSettings with the modified copy applies the change, and the same pattern applies one level down to the per-group scalar settings.",[44,874],{"x":46,"y":46,"width":47,"height":147,"fill":49},[51,876,877],{},[54,878,880],{"id":879,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"mshRmwArrow",[63,881],{"d":65,"fill":66},[68,883,884],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Forget the last step and nothing happens, quietly",[44,886],{"x":887,"y":112,"width":125,"height":888,"rx":58,"fill":162,"stroke":150,"style":85},"20","76",[68,890,894],{"x":891,"y":892,"style":893,"fill":150,"textAnchor":74},"102","82","text-anchor:middle;font-size:10.5px;font-family:monospace","rendererSettings()",[68,896,898],{"x":891,"y":897,"style":136,"fill":66,"textAnchor":74},"108","returns a copy",[139,900],{"x1":901,"y1":902,"x2":133,"y2":902,"stroke":66,"style":903},"184","90","stroke-width:2;marker-end:url(#mshRmwArrow)",[44,905],{"x":906,"y":112,"width":125,"height":888,"rx":58,"fill":179,"stroke":180,"style":85},"216",[68,908,909],{"x":143,"y":892,"style":893,"fill":180,"textAnchor":74},"scalarSettings(g)",[68,911,912],{"x":143,"y":897,"style":136,"fill":66,"textAnchor":74},"a copy of a copy",[139,914],{"x1":70,"y1":902,"x2":915,"y2":902,"stroke":66,"style":903},"404",[44,917],{"x":918,"y":112,"width":125,"height":888,"rx":58,"fill":191,"stroke":192,"style":85},"412",[68,920,922],{"x":921,"y":892,"style":202,"fill":192,"textAnchor":74},"494","modify the ramp",[68,924,925],{"x":921,"y":897,"style":136,"fill":66,"textAnchor":74},"still no effect",[139,927],{"x1":928,"y1":902,"x2":189,"y2":902,"stroke":66,"style":903},"576",[44,930],{"x":931,"y":112,"width":932,"height":888,"rx":58,"fill":933,"stroke":934,"style":85},"608","132","#edf8e9","#15803d",[68,936,938],{"x":937,"y":892,"style":202,"fill":934,"textAnchor":74},"674","write both back",[68,940,941],{"x":937,"y":897,"style":136,"fill":66,"textAnchor":74},"now it applies",[44,943],{"x":944,"y":945,"width":946,"height":888,"rx":82,"fill":149,"stroke":84,"style":193},"60","158","640",[68,948,950],{"x":70,"y":901,"style":949,"fill":73,"textAnchor":74},"text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","both writes are needed, innermost first",[68,952,953],{"x":70,"y":133,"style":893,"fill":66,"textAnchor":74},"settings.setScalarSettings(g, scalar)",[68,955,957],{"x":70,"y":956,"style":893,"fill":66,"textAnchor":74},"226","mesh.setRendererSettings(settings)",[44,959],{"x":960,"y":961,"width":962,"height":160,"rx":58,"fill":191,"stroke":192,"style":193},"120","254","520",[68,964,966],{"x":70,"y":965,"style":202,"fill":66,"textAnchor":74},"279","the same pattern governs QgsRasterLayer renderers and layout item properties",[228,968,970],{"className":230,"code":969,"language":232,"meta":233,"style":233},"from qgis.core import QgsColorRampShader, QgsStyle\n\ndepth_group = 1\n\nsettings = mesh.rendererSettings()\nsettings.setActiveScalarDatasetGroup(depth_group)\n\nscalar = settings.scalarSettings(depth_group)\nshader = scalar.colorRampShader()\nshader.setColorRampType(QgsColorRampShader.Interpolated)\nshader.setSourceColorRamp(QgsStyle.defaultStyle().colorRamp(\"Blues\"))\nshader.setMinimumValue(0.0)\nshader.setMaximumValue(3.5)\nshader.classifyColorRamp(classes=8, band=-1)\nscalar.setColorRampShader(shader)\n\nsettings.setScalarSettings(depth_group, scalar)\nmesh.setRendererSettings(settings)\nmesh.triggerRepaint()\n",[235,971,972,983,987,996,1000,1010,1015,1019,1029,1039,1044,1054,1064,1074,1100,1105,1109,1115,1121],{"__ignoreMap":233},[238,973,974,976,978,980],{"class":139,"line":240},[238,975,244],{"class":243},[238,977,248],{"class":247},[238,979,251],{"class":243},[238,981,982],{"class":247}," QgsColorRampShader, QgsStyle\n",[238,984,985],{"class":139,"line":257},[238,986,261],{"emptyLinePlaceholder":260},[238,988,989,992,994],{"class":139,"line":264},[238,990,991],{"class":247},"depth_group ",[238,993,270],{"class":243},[238,995,575],{"class":312},[238,997,998],{"class":139,"line":294},[238,999,261],{"emptyLinePlaceholder":260},[238,1001,1002,1005,1007],{"class":139,"line":306},[238,1003,1004],{"class":247},"settings ",[238,1006,270],{"class":243},[238,1008,1009],{"class":247}," mesh.rendererSettings()\n",[238,1011,1012],{"class":139,"line":324},[238,1013,1014],{"class":247},"settings.setActiveScalarDatasetGroup(depth_group)\n",[238,1016,1017],{"class":139,"line":329},[238,1018,261],{"emptyLinePlaceholder":260},[238,1020,1021,1024,1026],{"class":139,"line":343},[238,1022,1023],{"class":247},"scalar ",[238,1025,270],{"class":243},[238,1027,1028],{"class":247}," settings.scalarSettings(depth_group)\n",[238,1030,1031,1034,1036],{"class":139,"line":348},[238,1032,1033],{"class":247},"shader ",[238,1035,270],{"class":243},[238,1037,1038],{"class":247}," scalar.colorRampShader()\n",[238,1040,1041],{"class":139,"line":366},[238,1042,1043],{"class":247},"shader.setColorRampType(QgsColorRampShader.Interpolated)\n",[238,1045,1046,1049,1052],{"class":139,"line":381},[238,1047,1048],{"class":247},"shader.setSourceColorRamp(QgsStyle.defaultStyle().colorRamp(",[238,1050,1051],{"class":276},"\"Blues\"",[238,1053,340],{"class":247},[238,1055,1056,1059,1062],{"class":139,"line":392},[238,1057,1058],{"class":247},"shader.setMinimumValue(",[238,1060,1061],{"class":312},"0.0",[238,1063,291],{"class":247},[238,1065,1066,1069,1072],{"class":139,"line":403},[238,1067,1068],{"class":247},"shader.setMaximumValue(",[238,1070,1071],{"class":312},"3.5",[238,1073,291],{"class":247},[238,1075,1076,1079,1083,1085,1087,1089,1092,1095,1098],{"class":139,"line":426},[238,1077,1078],{"class":247},"shader.classifyColorRamp(",[238,1080,1082],{"class":1081},"s9osk","classes",[238,1084,270],{"class":243},[238,1086,58],{"class":312},[238,1088,280],{"class":247},[238,1090,1091],{"class":1081},"band",[238,1093,1094],{"class":243},"=-",[238,1096,1097],{"class":312},"1",[238,1099,291],{"class":247},[238,1101,1102],{"class":139,"line":484},[238,1103,1104],{"class":247},"scalar.setColorRampShader(shader)\n",[238,1106,1107],{"class":139,"line":489},[238,1108,261],{"emptyLinePlaceholder":260},[238,1110,1112],{"class":139,"line":1111},17,[238,1113,1114],{"class":247},"settings.setScalarSettings(depth_group, scalar)\n",[238,1116,1118],{"class":139,"line":1117},18,[238,1119,1120],{"class":247},"mesh.setRendererSettings(settings)\n",[238,1122,1124],{"class":139,"line":1123},19,[238,1125,1126],{"class":247},"mesh.triggerRepaint()\n",[14,1128,1129,1131],{},[216,1130,497],{}," Both write-backs are required and in that order — modifying the scalar settings alone changes a temporary object. Setting the minimum and maximum explicitly rather than letting QGIS classify from the current timestep is what keeps colours comparable across an animation; classified per frame, a rising flood stays the same colour while the legend silently changes underneath it. A minimum of exactly zero on a depth variable renders dry ground as the ramp's lightest colour rather than as nothing, which is usually not what you want — setting the minimum slightly above zero and enabling the \"below minimum\" transparency is the conventional fix.",[205,1133,1135],{"id":1134},"style-the-vector-field","Style the vector field",[228,1137,1139],{"className":230,"code":1138,"language":232,"meta":233,"style":233},"vector_group = 2\n\nsettings = mesh.rendererSettings()\nsettings.setActiveVectorDatasetGroup(vector_group)\n\nvector = settings.vectorSettings(vector_group)\nvector.setLineWidth(0.6)\nvector.setColor(QColor(\"#1e3a5f\"))\nvector.setShaftLengthMethod(0)          # 0 = min\u002Fmax scaling\nsettings.setVectorSettings(vector_group, vector)\nmesh.setRendererSettings(settings)\n",[235,1140,1141,1151,1155,1163,1168,1172,1182,1192,1202,1216,1221],{"__ignoreMap":233},[238,1142,1143,1146,1148],{"class":139,"line":240},[238,1144,1145],{"class":247},"vector_group ",[238,1147,270],{"class":243},[238,1149,1150],{"class":312}," 2\n",[238,1152,1153],{"class":139,"line":257},[238,1154,261],{"emptyLinePlaceholder":260},[238,1156,1157,1159,1161],{"class":139,"line":264},[238,1158,1004],{"class":247},[238,1160,270],{"class":243},[238,1162,1009],{"class":247},[238,1164,1165],{"class":139,"line":294},[238,1166,1167],{"class":247},"settings.setActiveVectorDatasetGroup(vector_group)\n",[238,1169,1170],{"class":139,"line":306},[238,1171,261],{"emptyLinePlaceholder":260},[238,1173,1174,1177,1179],{"class":139,"line":324},[238,1175,1176],{"class":247},"vector ",[238,1178,270],{"class":243},[238,1180,1181],{"class":247}," settings.vectorSettings(vector_group)\n",[238,1183,1184,1187,1190],{"class":139,"line":329},[238,1185,1186],{"class":247},"vector.setLineWidth(",[238,1188,1189],{"class":312},"0.6",[238,1191,291],{"class":247},[238,1193,1194,1197,1200],{"class":139,"line":343},[238,1195,1196],{"class":247},"vector.setColor(QColor(",[238,1198,1199],{"class":276},"\"#1e3a5f\"",[238,1201,340],{"class":247},[238,1203,1204,1207,1209,1212],{"class":139,"line":348},[238,1205,1206],{"class":247},"vector.setShaftLengthMethod(",[238,1208,46],{"class":312},[238,1210,1211],{"class":247},")          ",[238,1213,1215],{"class":1214},"sjoCn","# 0 = min\u002Fmax scaling\n",[238,1217,1218],{"class":139,"line":366},[238,1219,1220],{"class":247},"settings.setVectorSettings(vector_group, vector)\n",[238,1222,1223],{"class":139,"line":381},[238,1224,1120],{"class":247},[14,1226,1227,1229],{},[216,1228,497],{}," A scalar group and a vector group can be active at once, which is exactly the flood map convention: depth as a colour surface, velocity as arrows over it. The shaft length method decides how magnitude becomes arrow length — scaled between the data's minimum and maximum, fixed, or scaled by a factor — and min\u002Fmax scaling is the readable default until the magnitudes span orders of magnitude, at which point a fixed length coloured by magnitude reads better. Arrow density is a separate setting and matters more than any of this: a mesh with fifty thousand faces drawn with one arrow each is a solid block of ink.",[205,1231,1233],{"id":1232},"finding-the-range-across-the-whole-run","Finding the range across the whole run",[14,1235,1236],{},"Fixing the colour range needs the run's true minimum and maximum, and the group metadata carries them without a scan.",[228,1238,1240],{"className":230,"code":1239,"language":232,"meta":233,"style":233},"meta = mesh.datasetGroupMetadata(QgsMeshDatasetIndex(depth_group, 0))\nprint(\"group range:\", meta.minimum(), \"to\", meta.maximum())\n\npeaks = []\nfor step in range(count):\n    step_meta = mesh.datasetMetadata(QgsMeshDatasetIndex(depth_group, step))\n    peaks.append((step_meta.time(), step_meta.maximum()))\n\nworst_time, worst_value = max(peaks, key=lambda pair: pair[1])\nprint(f\"peak of {worst_value:.2f} at {worst_time:.1f} h\")\n",[235,1241,1242,1256,1274,1278,1287,1299,1309,1314,1318,1345],{"__ignoreMap":233},[238,1243,1244,1247,1249,1252,1254],{"class":139,"line":240},[238,1245,1246],{"class":247},"meta ",[238,1248,270],{"class":243},[238,1250,1251],{"class":247}," mesh.datasetGroupMetadata(QgsMeshDatasetIndex(depth_group, ",[238,1253,46],{"class":312},[238,1255,340],{"class":247},[238,1257,1258,1260,1262,1265,1268,1271],{"class":139,"line":257},[238,1259,332],{"class":312},[238,1261,316],{"class":247},[238,1263,1264],{"class":276},"\"group range:\"",[238,1266,1267],{"class":247},", meta.minimum(), ",[238,1269,1270],{"class":276},"\"to\"",[238,1272,1273],{"class":247},", meta.maximum())\n",[238,1275,1276],{"class":139,"line":264},[238,1277,261],{"emptyLinePlaceholder":260},[238,1279,1280,1283,1285],{"class":139,"line":294},[238,1281,1282],{"class":247},"peaks ",[238,1284,270],{"class":243},[238,1286,728],{"class":247},[238,1288,1289,1291,1293,1295,1297],{"class":139,"line":306},[238,1290,351],{"class":243},[238,1292,600],{"class":247},[238,1294,357],{"class":243},[238,1296,360],{"class":312},[238,1298,741],{"class":247},[238,1300,1301,1304,1306],{"class":139,"line":324},[238,1302,1303],{"class":247},"    step_meta ",[238,1305,270],{"class":243},[238,1307,1308],{"class":247}," mesh.datasetMetadata(QgsMeshDatasetIndex(depth_group, step))\n",[238,1310,1311],{"class":139,"line":329},[238,1312,1313],{"class":247},"    peaks.append((step_meta.time(), step_meta.maximum()))\n",[238,1315,1316],{"class":139,"line":343},[238,1317,261],{"emptyLinePlaceholder":260},[238,1319,1320,1323,1325,1328,1331,1334,1337,1340,1342],{"class":139,"line":348},[238,1321,1322],{"class":247},"worst_time, worst_value ",[238,1324,270],{"class":243},[238,1326,1327],{"class":312}," max",[238,1329,1330],{"class":247},"(peaks, ",[238,1332,1333],{"class":1081},"key",[238,1335,1336],{"class":243},"=lambda",[238,1338,1339],{"class":247}," pair: pair[",[238,1341,1097],{"class":312},[238,1343,1344],{"class":247},"])\n",[238,1346,1347,1349,1351,1353,1356,1358,1361,1364,1366,1369,1371,1374,1377,1379,1382],{"class":139,"line":366},[238,1348,332],{"class":312},[238,1350,316],{"class":247},[238,1352,434],{"class":243},[238,1354,1355],{"class":276},"\"peak of ",[238,1357,440],{"class":312},[238,1359,1360],{"class":247},"worst_value",[238,1362,1363],{"class":243},":.2f",[238,1365,446],{"class":312},[238,1367,1368],{"class":276}," at ",[238,1370,440],{"class":312},[238,1372,1373],{"class":247},"worst_time",[238,1375,1376],{"class":243},":.1f",[238,1378,446],{"class":312},[238,1380,1381],{"class":276}," h\"",[238,1383,291],{"class":247},[14,1385,1386,1388],{},[216,1387,497],{}," Group metadata gives the extremes across every timestep, which is exactly what the colour ramp should be classified against. Per-dataset metadata gives them per timestep, which is how you find the peak of a flood without sampling anything — and knowing which hour that was tells you which frame to use as the still image in a report. Both are read from the file's own summary rather than computed, so they are instant even on a large run. A group whose reported maximum is absurd usually means the model wrote a fill value into the data rather than a nodata flag.",[205,1390,1392],{"id":1391},"qgis-version-compatibility","QGIS version compatibility",[14,1394,1395,1396,1399],{},"Mesh layers arrived in QGIS 3.4 and the renderer settings API reached its current shape by 3.14. ",[235,1397,1398],{},"QgsMeshLayer.datasetValue"," and the dataset index have been stable since. Temporal properties for mesh layers, including the reference time, arrived with the wider temporal framework in 3.14. The available MDAL drivers grow with each release, so a file that will not open on 3.34 is worth retrying on a newer build.",[205,1401,1403],{"id":1402},"troubleshooting","Troubleshooting",[210,1405,1406,1415,1421,1431,1445,1454],{},[213,1407,1408,1414],{},[216,1409,1410,1413],{},[235,1411,1412],{},"isValid()"," is False."," MDAL has no driver for this file, or the file is a NetCDF whose variables MDAL cannot interpret as a mesh.",[213,1416,1417,1420],{},[216,1418,1419],{},"The mesh is in the wrong place."," No CRS in the file. Set it on the layer.",[213,1422,1423,1426,1427,1430],{},[216,1424,1425],{},"Every sampled value is NaN."," The point is outside the mesh, or the group is a vector group being read with ",[235,1428,1429],{},".scalar()"," when the components are null.",[213,1432,1433,1436,1437,1440,1441,1444],{},[216,1434,1435],{},"Styling changes have no effect."," A missing ",[235,1438,1439],{},"setRendererSettings"," or ",[235,1442,1443],{},"setScalarSettings"," write-back.",[213,1446,1447,1450,1451,1453],{},[216,1448,1449],{},"Timesteps are decades off."," The reference time is wrong; ",[235,1452,662],{}," is relative, not absolute.",[213,1455,1456,1459],{},[216,1457,1458],{},"The map is solid with arrows."," Reduce arrow density, or switch to streamlines for a dense field.",[205,1461,1463],{"id":1462},"conclusion","Conclusion",[14,1465,1466],{},"Enumerate the groups rather than assuming their numbers, set the CRS and the reference time before anything else, check for NaN on every sample, and remember that renderer settings are copies until written back. Mesh support in QGIS is genuinely good; the API just asks you to be explicit about things other layer types infer.",[205,1468,1470],{"id":1469},"frequently-asked-questions","Frequently Asked Questions",[14,1472,1473,1476,1477,1480,1481,1485],{},[216,1474,1475],{},"Can I convert a mesh to a raster?","\nYes — ",[235,1478,1479],{},"native:meshrasterize"," grids a chosen dataset group and timestep into a raster, after which everything in ",[21,1482,1484],{"href":1483},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002F","raster analysis workflows"," applies. There are matching algorithms for exporting contours and vectors from a mesh.",[14,1487,1488,1491],{},[216,1489,1490],{},"How do I animate a mesh?","\nSet the reference time, activate the layer's temporal properties, and drive the controller as usual — the layer picks the dataset matching each frame automatically.",[14,1493,1494,1497,1498,1502],{},[216,1495,1496],{},"Can I edit mesh values from PyQGIS?","\nQGIS supports editing the mesh ",[1499,1500,1501],"em",{},"frame"," — adding and removing vertices and faces — but dataset values are read-only. Changing values means regenerating the file from the model.",[14,1504,1505,1508],{},[216,1506,1507],{},"Why do two dataset groups have the same name?","\nSome formats store maximum and instantaneous variants under similar names. Print the group index alongside the name and select by index once you have identified the right one.",[205,1510,1512],{"id":1511},"related","Related",[210,1514,1515,1520,1526,1532,1538],{},[213,1516,1517,1519],{},[21,1518,24],{"href":23}," — the guide this recipe belongs to",[213,1521,1522],{},[21,1523,1525],{"href":1524},"\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fset-layer-temporal-properties-pyqgis\u002F","Set Layer Temporal Properties in PyQGIS",[213,1527,1528],{},[21,1529,1531],{"href":1530},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fapply-color-ramp-to-raster-pyqgis\u002F","Apply a Color Ramp to a Raster in PyQGIS",[213,1533,1534],{},[21,1535,1537],{"href":1536},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002F","Sample Raster Values at Points in PyQGIS",[213,1539,1540],{},[21,1541,1543],{"href":1542},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fcreate-contours-from-dem-pyqgis\u002F","Create Contours from a DEM in PyQGIS",[1545,1546,1547],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":233,"searchDepth":257,"depth":257,"links":1549},[1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561],{"id":207,"depth":257,"text":208},{"id":225,"depth":257,"text":226},{"id":559,"depth":257,"text":560},{"id":670,"depth":257,"text":671},{"id":859,"depth":257,"text":860},{"id":1134,"depth":257,"text":1135},{"id":1232,"depth":257,"text":1233},{"id":1391,"depth":257,"text":1392},{"id":1402,"depth":257,"text":1403},{"id":1462,"depth":257,"text":1463},{"id":1469,"depth":257,"text":1470},{"id":1511,"depth":257,"text":1512},"Open a hydraulic or atmospheric model output as a QgsMeshLayer, enumerate its dataset groups and timesteps, sample values, and configure the scalar and vector renderers.","md",{"slug":1565,"type":1566,"breadcrumb":1567,"datePublished":1568,"dateModified":1568},"load-and-style-mesh-layer-pyqgis","article","Load & Style a Mesh Layer","2026-09-05","\u002Fpyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fload-and-style-mesh-layer-pyqgis",{"title":5,"description":1562},"pyqgis-cartography-visualization\u002Ftemporal-and-3d-visualization\u002Fload-and-style-mesh-layer-pyqgis\u002Findex","MzkbfK7953ayg1BZexe8LbWp7QwO3tct31lritB_Jn8",1788563847735]