[{"data":1,"prerenderedAt":1206},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fcreate-heatmap-renderer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1195,"extension":1196,"meta":1197,"navigation":295,"path":1202,"seo":1203,"stem":1204,"__hash__":1205},"docs\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fcreate-heatmap-renderer-pyqgis\u002Findex.md","Build a Heatmap Renderer in PyQGIS",{"type":7,"value":8,"toc":1182},"minimark",[9,13,31,40,216,221,242,246,251,395,409,419,476,485,489,492,522,540,544,547,562,567,647,651,654,657,660,744,752,756,759,900,921,929,933,939,1029,1033,1096,1100,1106,1110,1120,1126,1136,1147,1151,1179],[10,11,5],"h1",{"id":12},"build-a-heatmap-renderer-in-pyqgis",[14,15,16,17,21,22,26,27,30],"p",{},"A heatmap turns a scatter of points into a continuous surface of density, and QGIS offers two quite different ways to make one. ",[18,19,20],"code",{},"QgsHeatmapRenderer"," is a ",[23,24,25],"em",{},"style",": it draws the density live on the canvas, follows the current extent, and disappears the moment you switch the renderer. The Heatmap processing algorithm is an ",[23,28,29],{},"analysis",": it produces a raster you can classify, clip, sample and hand to somebody else. Choosing the wrong one leads to hours spent trying to get numbers out of something that was only ever a picture.",[14,32,33,34,39],{},"This recipe belongs to ",[35,36,38],"a",{"href":37},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002F","Symbol Layers & Advanced Symbology in PyQGIS",". It covers configuring the renderer from Python, weighting points by an attribute, controlling the radius in ground units so the map means the same thing at every zoom, and when to switch to the raster route instead.",[14,41,42],{},[43,44,49,53,57,64,93,102,112,118,124,128,132,136,139,144,147,150,155,158,161,164,167,170,174,178,183,187,191,195,198,201,207,211],"svg",{"viewBox":45,"role":46,"ariaLabel":47,"xmlns":48},"0 0 760 316","img","How a heatmap renderer converts points into a density surface: each point contributes a kernel, the kernels sum, and the summed values are mapped through a colour ramp","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[50,51,52],"title",{},"From points to a density surface",[54,55,56],"desc",{},"Each point contributes a circular kernel whose influence falls off with distance out to the chosen radius. Where kernels overlap their values add. The resulting continuous surface is mapped through a colour ramp, with the maximum value either detected automatically from the visible extent or fixed by the author.",[58,59],"rect",{"x":60,"y":60,"width":61,"height":62,"fill":63},"0","760","316","#f6f3ea",[65,66,67,81],"defs",{},[68,69,76],"marker",{"id":70,"viewBox":71,"refX":72,"refY":73,"markerWidth":74,"markerHeight":74,"orient":75},"hmArrow","0 0 10 10","8","5","7","auto-start-reverse",[77,78],"path",{"d":79,"fill":80},"M0 0 L10 5 L0 10 z","#2f3b35",[82,83,85,90],"radial-gradient",{"id":84},"hmKernel",[86,87],"stop",{"offset":60,"stopColor":88,"stopOpacity":89},"#b45309","0.75",[86,91],{"offset":92,"stopColor":88,"stopOpacity":60},"1",[94,95,101],"text",{"x":96,"y":97,"style":98,"fill":99,"textAnchor":100},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Radius sets the kernel; the ramp only recolours the result",[58,103],{"x":104,"y":105,"width":106,"height":107,"rx":108,"fill":109,"stroke":110,"style":111},"20","54","212","200","10","#fffdf7","#2563eb","stroke-width:2.5",[94,113,117],{"x":114,"y":115,"style":116,"fill":110,"textAnchor":100},"126","80","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","1 · the points",[119,120],"circle",{"cx":121,"cy":122,"r":123,"fill":110},"90","130","4",[119,125],{"cx":126,"cy":127,"r":123,"fill":110},"118","150",[119,129],{"cx":130,"cy":131,"r":123,"fill":110},"104","172",[119,133],{"cx":134,"cy":135,"r":123,"fill":110},"170","196",[119,137],{"cx":127,"cy":138,"r":123,"fill":110},"120",[94,140,143],{"x":114,"y":141,"style":142,"fill":80,"textAnchor":100},"234","text-anchor:middle;font-size:11px;font-family:sans-serif","a weight field, optionally",[58,145],{"x":146,"y":105,"width":106,"height":107,"rx":108,"fill":109,"stroke":88,"style":111},"274",[94,148,149],{"x":96,"y":115,"style":116,"fill":88,"textAnchor":100},"2 · kernels sum",[119,151],{"cx":152,"cy":122,"r":153,"fill":154},"344","34","url(#hmKernel)",[119,156],{"cx":157,"cy":127,"r":153,"fill":154},"372",[119,159],{"cx":160,"cy":131,"r":153,"fill":154},"358",[119,162],{"cx":163,"cy":135,"r":153,"fill":154},"424",[119,165],{"cx":166,"cy":138,"r":153,"fill":154},"404",[94,168,169],{"x":96,"y":141,"style":142,"fill":80,"textAnchor":100},"overlap adds, radius decides how much",[58,171],{"x":172,"y":105,"width":106,"height":107,"rx":108,"fill":109,"stroke":173,"style":111},"528","#15803d",[94,175,177],{"x":176,"y":115,"style":116,"fill":173,"textAnchor":100},"634","3 · ramp applied",[58,179],{"x":180,"y":130,"width":181,"height":104,"fill":182},"556","156","#dbeafe",[58,184],{"x":180,"y":185,"width":181,"height":104,"fill":186},"124","#edf8e9",[58,188],{"x":180,"y":189,"width":181,"height":104,"fill":190},"144","#fdf2e2",[58,192],{"x":180,"y":193,"width":181,"height":104,"fill":194},"164","#e8b184",[58,196],{"x":180,"y":197,"width":181,"height":104,"fill":88},"184",[94,199,200],{"x":176,"y":141,"style":142,"fill":80,"textAnchor":100},"low density to high density",[202,203],"line",{"x1":141,"y1":204,"x2":205,"y2":204,"stroke":80,"style":206},"154","268","stroke-width:2;marker-end:url(#hmArrow)",[202,208],{"x1":209,"y1":204,"x2":210,"y2":204,"stroke":80,"style":206},"488","522",[94,212,215],{"x":96,"y":213,"style":142,"fill":214,"textAnchor":100},"292","#59645f","with an auto maximum, the same data recolours itself as you pan",[217,218,220],"h2",{"id":219},"prerequisites","Prerequisites",[222,223,224,232,239],"ul",{},[225,226,227,231],"li",{},[228,229,230],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[225,233,234,235,238],{},"A point layer with enough points to have density worth showing; the examples use one called ",[18,236,237],{},"incidents",".",[225,240,241],{},"A projected CRS if the radius should mean metres. In EPSG:4326 a radius in map units is degrees, and the heatmap will be visibly stretched north–south.",[217,243,245],{"id":244},"configure-the-renderer","Configure the renderer",[14,247,248,250],{},[18,249,20],{}," has a small surface: radius, weight, colour ramp, maximum value and render quality.",[252,253,258],"pre",{"className":254,"code":255,"language":256,"meta":257,"style":257},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsProject, QgsHeatmapRenderer, QgsStyle, QgsUnitTypes,\n)\n\nlayer = QgsProject.instance().mapLayersByName(\"incidents\")[0]\n\nrenderer = QgsHeatmapRenderer()\nrenderer.setRadius(500)\nrenderer.setRadiusUnit(QgsUnitTypes.RenderMapUnits)\nrenderer.setColorRamp(QgsStyle.defaultStyle().colorRamp(\"Magma\"))\nrenderer.setRenderQuality(3)\n\nlayer.setRenderer(renderer)\nlayer.triggerRepaint()\n","python","",[18,259,260,278,284,290,297,322,327,338,349,355,367,378,383,389],{"__ignoreMap":257},[261,262,264,268,272,275],"span",{"class":202,"line":263},1,[261,265,267],{"class":266},"snl16","from",[261,269,271],{"class":270},"s95oV"," qgis.core ",[261,273,274],{"class":266},"import",[261,276,277],{"class":270}," (\n",[261,279,281],{"class":202,"line":280},2,[261,282,283],{"class":270},"    QgsProject, QgsHeatmapRenderer, QgsStyle, QgsUnitTypes,\n",[261,285,287],{"class":202,"line":286},3,[261,288,289],{"class":270},")\n",[261,291,293],{"class":202,"line":292},4,[261,294,296],{"emptyLinePlaceholder":295},true,"\n",[261,298,300,303,306,309,313,316,319],{"class":202,"line":299},5,[261,301,302],{"class":270},"layer ",[261,304,305],{"class":266},"=",[261,307,308],{"class":270}," QgsProject.instance().mapLayersByName(",[261,310,312],{"class":311},"sU2Wk","\"incidents\"",[261,314,315],{"class":270},")[",[261,317,60],{"class":318},"sDLfK",[261,320,321],{"class":270},"]\n",[261,323,325],{"class":202,"line":324},6,[261,326,296],{"emptyLinePlaceholder":295},[261,328,330,333,335],{"class":202,"line":329},7,[261,331,332],{"class":270},"renderer ",[261,334,305],{"class":266},[261,336,337],{"class":270}," QgsHeatmapRenderer()\n",[261,339,341,344,347],{"class":202,"line":340},8,[261,342,343],{"class":270},"renderer.setRadius(",[261,345,346],{"class":318},"500",[261,348,289],{"class":270},[261,350,352],{"class":202,"line":351},9,[261,353,354],{"class":270},"renderer.setRadiusUnit(QgsUnitTypes.RenderMapUnits)\n",[261,356,358,361,364],{"class":202,"line":357},10,[261,359,360],{"class":270},"renderer.setColorRamp(QgsStyle.defaultStyle().colorRamp(",[261,362,363],{"class":311},"\"Magma\"",[261,365,366],{"class":270},"))\n",[261,368,370,373,376],{"class":202,"line":369},11,[261,371,372],{"class":270},"renderer.setRenderQuality(",[261,374,375],{"class":318},"3",[261,377,289],{"class":270},[261,379,381],{"class":202,"line":380},12,[261,382,296],{"emptyLinePlaceholder":295},[261,384,386],{"class":202,"line":385},13,[261,387,388],{"class":270},"layer.setRenderer(renderer)\n",[261,390,392],{"class":202,"line":391},14,[261,393,394],{"class":270},"layer.triggerRepaint()\n",[14,396,397,400,401,404,405,408],{},[228,398,399],{},"Breakdown:"," ",[18,402,403],{},"setRadiusUnit(RenderMapUnits)"," is the single most important line. In the default millimetre mode the radius is a paper distance, so the heatmap tells a different story at every zoom level and cannot be compared between two exports at different scales. In map units it is a ground distance — five hundred metres here — and the map means the same thing everywhere. ",[18,406,407],{},"setRenderQuality()"," is a downsampling factor where 1 is exact and larger numbers are faster and blockier; 3 is a good interactive setting, and 1 is right for the final export.",[14,410,411,414,415,418],{},[18,412,413],{},"QgsStyle.defaultStyle().colorRamp(name)"," pulls a ramp from the user's style database. The name must match exactly, and it returns ",[18,416,417],{},"None"," silently when it does not — so check it before assigning, or a typo leaves you with a black rectangle:",[252,420,422],{"className":254,"code":421,"language":256,"meta":257,"style":257},"ramp = QgsStyle.defaultStyle().colorRamp(\"Magma\")\nif ramp is None:\n    raise RuntimeError(\"colour ramp not found in the style database\")\nrenderer.setColorRamp(ramp)\n",[18,423,424,438,455,471],{"__ignoreMap":257},[261,425,426,429,431,434,436],{"class":202,"line":263},[261,427,428],{"class":270},"ramp ",[261,430,305],{"class":266},[261,432,433],{"class":270}," QgsStyle.defaultStyle().colorRamp(",[261,435,363],{"class":311},[261,437,289],{"class":270},[261,439,440,443,446,449,452],{"class":202,"line":280},[261,441,442],{"class":266},"if",[261,444,445],{"class":270}," ramp ",[261,447,448],{"class":266},"is",[261,450,451],{"class":318}," None",[261,453,454],{"class":270},":\n",[261,456,457,460,463,466,469],{"class":202,"line":286},[261,458,459],{"class":266},"    raise",[261,461,462],{"class":318}," RuntimeError",[261,464,465],{"class":270},"(",[261,467,468],{"class":311},"\"colour ramp not found in the style database\"",[261,470,289],{"class":270},[261,472,473],{"class":202,"line":292},[261,474,475],{"class":270},"renderer.setColorRamp(ramp)\n",[14,477,478,480,481,484],{},[228,479,399],{}," Listing ",[18,482,483],{},"QgsStyle.defaultStyle().colorRampNames()"," in the console is the quick way to find the exact spelling; the set differs slightly between QGIS versions and between users who have imported extra styles.",[217,486,488],{"id":487},"weight-the-points","Weight the points",[14,490,491],{},"By default every point counts as one. A weight expression makes some points count for more, which is what turns \"where are the shops\" into \"where is the floor space\".",[252,493,495],{"className":254,"code":494,"language":256,"meta":257,"style":257},"renderer.setWeightExpression(\"coalesce(\\\"severity\\\", 1)\")\nlayer.triggerRepaint()\n",[18,496,497,518],{"__ignoreMap":257},[261,498,499,502,505,508,511,513,516],{"class":202,"line":263},[261,500,501],{"class":270},"renderer.setWeightExpression(",[261,503,504],{"class":311},"\"coalesce(",[261,506,507],{"class":318},"\\\"",[261,509,510],{"class":311},"severity",[261,512,507],{"class":318},[261,514,515],{"class":311},", 1)\"",[261,517,289],{"class":270},[261,519,520],{"class":202,"line":280},[261,521,394],{"class":270},[14,523,524,526,527,531,532,535,536,539],{},[228,525,399],{}," Any ",[35,528,530],{"href":529},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","QGIS expression"," is accepted, so this can be a plain field name, arithmetic, or a ",[18,533,534],{},"case"," statement. ",[18,537,538],{},"coalesce()"," matters more here than almost anywhere else: a null weight contributes nothing, so a field with gaps quietly produces a heatmap of \"records where severity was filled in\" rather than of the phenomenon. Where a weight is genuinely unknown, decide whether the right answer is 1 or 0 and say so in the expression instead of letting the null decide.",[217,541,543],{"id":542},"fix-the-maximum-or-the-map-lies","Fix the maximum, or the map lies",[14,545,546],{},"Left alone, the renderer finds the maximum density in the current view and stretches the ramp to it. Pan somewhere quiet and the quiet area lights up as though it were busy.",[252,548,550],{"className":254,"code":549,"language":256,"meta":257,"style":257},"renderer.setMaximumValue(40.0)\n",[18,551,552],{"__ignoreMap":257},[261,553,554,557,560],{"class":202,"line":263},[261,555,556],{"class":270},"renderer.setMaximumValue(",[261,558,559],{"class":318},"40.0",[261,561,289],{"class":270},[14,563,564,566],{},[228,565,399],{}," Setting an explicit maximum pins the top of the ramp to a fixed density, so colours are comparable across extents, across exports and between two maps of different periods. Zero — the default — means automatic. Choosing the number is the work: run the layer once with automatic scaling, note the value QGIS reports in the renderer's properties at the extent you care about, round it to something defensible, and write that into the script with a comment explaining the choice. Any map that will be compared with another map needs this line.",[14,568,569],{},[43,570,573,576,579,581,584,590,595,600,605,609,611,615,618,622,624,628,631,634,636,639,642,644],{"viewBox":571,"role":46,"ariaLabel":572,"xmlns":48},"0 0 760 292","The same heatmap rendered with an automatic maximum in two different map extents, producing different colours for the same density, compared with a fixed maximum which keeps the colours comparable",[50,574,575],{},"Why an automatic maximum is not comparable",[54,577,578],{},"With an automatic maximum, a sparse extent stretches its colour ramp to its own local peak, so a low density is painted the same colour as a high density elsewhere. With a fixed maximum both extents use the same scale and the sparse area correctly appears cool.",[58,580],{"x":60,"y":60,"width":61,"height":213,"fill":63},[94,582,583],{"x":96,"y":97,"style":98,"fill":99,"textAnchor":100},"Automatic scaling makes every extent look like a hotspot",[58,585],{"x":586,"y":587,"width":588,"height":589,"rx":108,"fill":190,"stroke":88,"style":111},"26","52","330","204",[94,591,594],{"x":592,"y":593,"style":116,"fill":88,"textAnchor":100},"191","78","maximum = 0 (automatic)",[58,596],{"x":587,"y":597,"width":122,"height":597,"rx":598,"fill":88,"stroke":88,"style":599},"96","6","stroke-width:1.6",[94,601,604],{"x":602,"y":603,"style":142,"fill":109,"textAnchor":100},"117","140","busy area",[94,606,608],{"x":602,"y":607,"style":142,"fill":109,"textAnchor":100},"160","peak 38",[58,610],{"x":107,"y":597,"width":122,"height":597,"rx":598,"fill":88,"stroke":88,"style":599},[94,612,614],{"x":613,"y":603,"style":142,"fill":109,"textAnchor":100},"265","quiet area",[94,616,617],{"x":613,"y":607,"style":142,"fill":109,"textAnchor":100},"peak 4",[94,619,621],{"x":592,"y":620,"style":142,"fill":80,"textAnchor":100},"222","identical colour, ninefold difference",[58,623],{"x":166,"y":587,"width":588,"height":589,"rx":108,"fill":186,"stroke":173,"style":111},[94,625,627],{"x":626,"y":593,"style":116,"fill":173,"textAnchor":100},"569","maximum = 40 (fixed)",[58,629],{"x":630,"y":597,"width":122,"height":597,"rx":598,"fill":88,"stroke":88,"style":599},"430",[94,632,604],{"x":633,"y":603,"style":142,"fill":109,"textAnchor":100},"495",[94,635,608],{"x":633,"y":607,"style":142,"fill":109,"textAnchor":100},[58,637],{"x":638,"y":597,"width":122,"height":597,"rx":598,"fill":190,"stroke":88,"style":599},"578",[94,640,614],{"x":641,"y":603,"style":142,"fill":99,"textAnchor":100},"643",[94,643,617],{"x":641,"y":607,"style":142,"fill":99,"textAnchor":100},[94,645,646],{"x":626,"y":620,"style":142,"fill":80,"textAnchor":100},"the difference survives the map",[217,648,650],{"id":649},"choosing-the-radius","Choosing the radius",[14,652,653],{},"The radius is the only parameter that changes what the map claims, and it is chosen far too often by dragging a slider until the picture looks nice.",[14,655,656],{},"A radius is a statement about the distance over which one event influences its surroundings. For street crime, a couple of hundred metres is a walkable block and defensible. For retail catchment, a kilometre or two matches how far people will walk to a shop. For disease reporting aggregated to a postcode, the radius should be at least the size of the postcode area or the map invents precision the data never had. Say the number out loud as a sentence — \"each incident is spread over a 500 metre neighbourhood\" — and if the sentence sounds wrong, the map is wrong.",[14,658,659],{},"Two failure modes bracket the range. Too small and the heatmap is a scatter of separate dots that adds nothing to plotting the points. Too large and everything merges into one smooth blob centred on the middle of the study area, which looks authoritative and says nothing. A practical approach is to render three candidate radii and keep the one where clusters you can independently justify are visible and clusters you cannot are not.",[252,661,663],{"className":254,"code":662,"language":256,"meta":257,"style":257},"for radius in (250, 500, 1000):\n    renderer = QgsHeatmapRenderer()\n    renderer.setRadius(radius)\n    renderer.setRadiusUnit(QgsUnitTypes.RenderMapUnits)\n    renderer.setMaximumValue(40.0)\n    renderer.setColorRamp(ramp.clone())\n    layer.setRenderer(renderer)\n    layer.triggerRepaint()\n    # export here, then compare the three side by side\n",[18,664,665,695,704,709,714,723,728,733,738],{"__ignoreMap":257},[261,666,667,670,673,676,679,682,685,687,689,692],{"class":202,"line":263},[261,668,669],{"class":266},"for",[261,671,672],{"class":270}," radius ",[261,674,675],{"class":266},"in",[261,677,678],{"class":270}," (",[261,680,681],{"class":318},"250",[261,683,684],{"class":270},", ",[261,686,346],{"class":318},[261,688,684],{"class":270},[261,690,691],{"class":318},"1000",[261,693,694],{"class":270},"):\n",[261,696,697,700,702],{"class":202,"line":280},[261,698,699],{"class":270},"    renderer ",[261,701,305],{"class":266},[261,703,337],{"class":270},[261,705,706],{"class":202,"line":286},[261,707,708],{"class":270},"    renderer.setRadius(radius)\n",[261,710,711],{"class":202,"line":292},[261,712,713],{"class":270},"    renderer.setRadiusUnit(QgsUnitTypes.RenderMapUnits)\n",[261,715,716,719,721],{"class":202,"line":299},[261,717,718],{"class":270},"    renderer.setMaximumValue(",[261,720,559],{"class":318},[261,722,289],{"class":270},[261,724,725],{"class":202,"line":324},[261,726,727],{"class":270},"    renderer.setColorRamp(ramp.clone())\n",[261,729,730],{"class":202,"line":329},[261,731,732],{"class":270},"    layer.setRenderer(renderer)\n",[261,734,735],{"class":202,"line":340},[261,736,737],{"class":270},"    layer.triggerRepaint()\n",[261,739,740],{"class":202,"line":351},[261,741,743],{"class":742},"sjoCn","    # export here, then compare the three side by side\n",[14,745,746,400,748,751],{},[228,747,399],{},[18,749,750],{},"ramp.clone()"," inside the loop is not optional — a colour ramp assigned to a renderer is owned by it, and reusing the same object across renderers leads to a use-after-free that shows up as a crash rather than a wrong colour. Fixing the maximum across all three is what makes the comparison meaningful; with automatic scaling each image renormalises and the three look deceptively similar.",[217,753,755],{"id":754},"when-to-use-the-raster-instead","When to use the raster instead",[14,757,758],{},"The renderer is a style, so its output cannot be queried, classified, clipped or exported as data. If any of those verbs appear in the requirement, run the algorithm.",[252,760,762],{"className":254,"code":761,"language":256,"meta":257,"style":257},"import processing\n\nprocessing.run(\"qgis:heatmapkerneldensityestimation\", {\n    \"INPUT\": layer,\n    \"RADIUS\": 500,\n    \"RADIUS_FIELD\": None,\n    \"PIXEL_SIZE\": 50,\n    \"WEIGHT_FIELD\": \"severity\",\n    \"KERNEL\": 0,                       # quartic\n    \"DECAY\": 0,\n    \"OUTPUT_VALUE\": 0,                 # raw values\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fdensity.tif\",\n})\n",[18,763,764,771,775,786,794,807,818,830,842,857,868,883,895],{"__ignoreMap":257},[261,765,766,768],{"class":202,"line":263},[261,767,274],{"class":266},[261,769,770],{"class":270}," processing\n",[261,772,773],{"class":202,"line":280},[261,774,296],{"emptyLinePlaceholder":295},[261,776,777,780,783],{"class":202,"line":286},[261,778,779],{"class":270},"processing.run(",[261,781,782],{"class":311},"\"qgis:heatmapkerneldensityestimation\"",[261,784,785],{"class":270},", {\n",[261,787,788,791],{"class":202,"line":292},[261,789,790],{"class":311},"    \"INPUT\"",[261,792,793],{"class":270},": layer,\n",[261,795,796,799,802,804],{"class":202,"line":299},[261,797,798],{"class":311},"    \"RADIUS\"",[261,800,801],{"class":270},": ",[261,803,346],{"class":318},[261,805,806],{"class":270},",\n",[261,808,809,812,814,816],{"class":202,"line":324},[261,810,811],{"class":311},"    \"RADIUS_FIELD\"",[261,813,801],{"class":270},[261,815,417],{"class":318},[261,817,806],{"class":270},[261,819,820,823,825,828],{"class":202,"line":329},[261,821,822],{"class":311},"    \"PIXEL_SIZE\"",[261,824,801],{"class":270},[261,826,827],{"class":318},"50",[261,829,806],{"class":270},[261,831,832,835,837,840],{"class":202,"line":340},[261,833,834],{"class":311},"    \"WEIGHT_FIELD\"",[261,836,801],{"class":270},[261,838,839],{"class":311},"\"severity\"",[261,841,806],{"class":270},[261,843,844,847,849,851,854],{"class":202,"line":351},[261,845,846],{"class":311},"    \"KERNEL\"",[261,848,801],{"class":270},[261,850,60],{"class":318},[261,852,853],{"class":270},",                       ",[261,855,856],{"class":742},"# quartic\n",[261,858,859,862,864,866],{"class":202,"line":357},[261,860,861],{"class":311},"    \"DECAY\"",[261,863,801],{"class":270},[261,865,60],{"class":318},[261,867,806],{"class":270},[261,869,870,873,875,877,880],{"class":202,"line":369},[261,871,872],{"class":311},"    \"OUTPUT_VALUE\"",[261,874,801],{"class":270},[261,876,60],{"class":318},[261,878,879],{"class":270},",                 ",[261,881,882],{"class":742},"# raw values\n",[261,884,885,888,890,893],{"class":202,"line":380},[261,886,887],{"class":311},"    \"OUTPUT\"",[261,889,801],{"class":270},[261,891,892],{"class":311},"\"\u002Fdata\u002Foutput\u002Fdensity.tif\"",[261,894,806],{"class":270},[261,896,897],{"class":202,"line":385},[261,898,899],{"class":270},"})\n",[14,901,902,400,904,907,908,911,912,915,916,920],{},[228,903,399],{},[18,905,906],{},"PIXEL_SIZE"," is the resolution of the output grid in layer units and is the parameter with the largest effect on both quality and run time — fifty metres against a five-hundred-metre radius is a reasonable ten-to-one ratio. ",[18,909,910],{},"KERNEL"," selects the falloff shape, with quartic the usual default and triweight producing a tighter core. ",[18,913,914],{},"OUTPUT_VALUE"," of 0 gives raw summed weights, which is what you want if the numbers will be ",[35,917,919],{"href":918},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002F","sampled at points"," or compared between runs; scaled output normalises and destroys that comparability.",[14,922,923,924,928],{},"Once written, the raster is an ordinary layer: classify it with a ",[35,925,927],{"href":926},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fapply-color-ramp-to-raster-pyqgis\u002F","singleband pseudocolour renderer",", clip it to a study area, or push it through further analysis.",[217,930,932],{"id":931},"qgis-version-compatibility","QGIS version compatibility",[14,934,935,936,938],{},"The examples target ",[228,937,230],{}," (Python 3.12).",[940,941,942,958],"table",{},[943,944,945],"thead",{},[946,947,948,952,955],"tr",{},[949,950,951],"th",{},"QGIS version",[949,953,954],{},"Python",[949,956,957],{},"Notes",[959,960,961,979,994,1004,1015],"tbody",{},[946,962,963,967,970],{},[964,965,966],"td",{},"3.16 LTR",[964,968,969],{},"3.7",[964,971,972,974,975,978],{},[18,973,20],{}," complete; ",[18,976,977],{},"setRadiusUnit"," available.",[946,980,981,984,987],{},[964,982,983],{},"3.22 LTR",[964,985,986],{},"3.9",[964,988,989,990,993],{},"Heatmap algorithm id ",[18,991,992],{},"qgis:heatmapkerneldensityestimation"," stable.",[946,995,996,999,1001],{},[964,997,998],{},"3.28 LTR",[964,1000,986],{},[964,1002,1003],{},"Render quality behaviour unchanged; ramp names largely stable.",[946,1005,1006,1009,1012],{},[964,1007,1008],{},"3.34 LTR",[964,1010,1011],{},"3.12",[964,1013,1014],{},"Baseline for this page.",[946,1016,1017,1020,1022],{},[964,1018,1019],{},"3.40+",[964,1021,1011],{},[964,1023,1024,1025,1028],{},"Ramp database gains additional named ramps; check ",[18,1026,1027],{},"colorRampNames()"," before hard-coding.",[217,1030,1032],{"id":1031},"troubleshooting","Troubleshooting",[222,1034,1035,1049,1058,1068,1074,1088],{},[225,1036,1037,400,1040,1043,1044,1046,1047,238],{},[228,1038,1039],{},"The layer draws as a black rectangle.",[18,1041,1042],{},"setColorRamp()"," received ",[18,1045,417],{}," because the ramp name was wrong. Check ",[18,1048,483],{},[225,1050,1051,1054,1055,238],{},[228,1052,1053],{},"The heatmap changes every time you zoom."," The radius is in millimetres. Switch to ",[18,1056,1057],{},"RenderMapUnits",[225,1059,1060,1063,1064,1067],{},[228,1061,1062],{},"The colours change when you pan."," The maximum is automatic. Set ",[18,1065,1066],{},"setMaximumValue()"," to a fixed number.",[225,1069,1070,1073],{},[228,1071,1072],{},"The heatmap is stretched vertically."," The layer is in a geographic CRS, so a degree of longitude is shorter than a degree of latitude. Reproject to a projected CRS first.",[225,1075,1076,1079,1080,1082,1083,1087],{},[228,1077,1078],{},"The canvas is very slow."," Lower ",[18,1081,407],{}," while working and raise it for export, and consider whether the layer needs a ",[35,1084,1086],{"href":1085},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002F","feature filter"," rather than rendering every point.",[225,1089,1090,1093,1094,238],{},[228,1091,1092],{},"The heatmap looks the same with and without a weight field."," The weight expression returned null for most features. Wrap it in ",[18,1095,538],{},[217,1097,1099],{"id":1098},"conclusion","Conclusion",[14,1101,1102,1103,1105],{},"Set the radius in map units, weight with ",[18,1104,538],{}," around the field, pin the maximum so the colours mean something, and drop render quality only while you are working. When the density has to be measured rather than looked at, produce the kernel-density raster instead and treat the result as data.",[217,1107,1109],{"id":1108},"frequently-asked-questions","Frequently Asked Questions",[14,1111,1112,1115,1116,1119],{},[228,1113,1114],{},"Can the radius vary per point?","\nNot in the renderer, but the processing algorithm accepts ",[18,1117,1118],{},"RADIUS_FIELD",", so each point can carry its own influence distance. That is the right route for modelling things with genuinely different reach, such as transmitters of different power.",[14,1121,1122,1125],{},[228,1123,1124],{},"Does the heatmap renderer respect a layer filter?","\nYes. Only features passing the layer's subset string or the current selection filter contribute, which makes a filtered heatmap an easy way to compare periods without duplicating the layer.",[14,1127,1128,1131,1132,238],{},[228,1129,1130],{},"Why is the heatmap invisible over a dark basemap?","\nThe default ramps start at a dark colour with no transparency at the low end. Either choose a ramp whose first stop is transparent, or set a blend mode as described in ",[35,1133,1135],{"href":1134},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fset-layer-opacity-and-blend-mode-pyqgis\u002F","opacity and blend modes",[14,1137,1138,1141,1142,1146],{},[228,1139,1140],{},"Can I export the heatmap renderer to an image?","\nYes — it renders like any other style, so the ",[35,1143,1145],{"href":1144},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002F","canvas export"," and layout routes both work. Set render quality back to 1 first, or the export inherits the blocky preview.",[217,1148,1150],{"id":1149},"related","Related",[222,1152,1153,1158,1163,1168,1173],{},[225,1154,1155,1157],{},[35,1156,38],{"href":37}," — the guide this recipe belongs to",[225,1159,1160],{},[35,1161,1162],{"href":1134},"Set Layer Opacity and Blend Modes in PyQGIS",[225,1164,1165],{},[35,1166,1167],{"href":926},"Apply a Color Ramp to a Raster in PyQGIS",[225,1169,1170],{},[35,1171,1172],{"href":918},"Sample Raster Values at Points in PyQGIS",[225,1174,1175],{},[35,1176,1178],{"href":1177},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS",[25,1180,1181],{},"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":257,"searchDepth":280,"depth":280,"links":1183},[1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194],{"id":219,"depth":280,"text":220},{"id":244,"depth":280,"text":245},{"id":487,"depth":280,"text":488},{"id":542,"depth":280,"text":543},{"id":649,"depth":280,"text":650},{"id":754,"depth":280,"text":755},{"id":931,"depth":280,"text":932},{"id":1031,"depth":280,"text":1032},{"id":1098,"depth":280,"text":1099},{"id":1108,"depth":280,"text":1109},{"id":1149,"depth":280,"text":1150},"Configure QgsHeatmapRenderer from Python — radius units, weight expressions, colour ramps and maximum value — and know when the interpolated heatmap raster is the better tool.","md",{"slug":1198,"type":1199,"breadcrumb":1200,"datePublished":1201,"dateModified":1201},"create-heatmap-renderer-pyqgis","article","Heatmap Renderer","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fcreate-heatmap-renderer-pyqgis",{"title":5,"description":1195},"pyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fcreate-heatmap-renderer-pyqgis\u002Findex","1ZPqPU2OW0OE_-bFO16ZXlYZeP5GifUe37QBgy2TbLI",1787823360561]