[{"data":1,"prerenderedAt":2002},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis":3},{"id":4,"title":5,"body":6,"description":1990,"extension":1991,"meta":1992,"navigation":345,"path":1998,"seo":1999,"stem":2000,"__hash__":2001},"docs\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002Findex.md","Create a Choropleth Map in PyQGIS",{"type":7,"value":8,"toc":1973},"minimark",[9,13,34,46,49,270,273,278,306,310,313,533,565,569,584,591,670,700,704,707,852,872,876,885,925,954,958,969,1045,1072,1076,1079,1163,1193,1197,1200,1479,1500,1504,1507,1626,1630,1633,1739,1743,1750,1816,1830,1834,1848,1862,1871,1890,1902,1906,1915,1919,1925,1931,1939,1945,1949,1969],[10,11,5],"h1",{"id":12},"create-a-choropleth-map-in-pyqgis",[14,15,16],"p",{},[17,18,19,24,25,24,29,33],"em",{},[20,21,23],"a",{"href":22},"\u002F","Home"," → ",[20,26,28],{"href":27},"\u002Fpyqgis-cartography-visualization\u002F","Cartography & Visualization",[20,30,32],{"href":31},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated & Categorized Renderers"," → Create a Choropleth Map",[14,35,36,37,41,42,45],{},"A choropleth map shades polygons by the value of a numeric attribute, letting readers compare regions at a glance. In PyQGIS the tool for the job is ",[38,39,40],"code",{},"QgsGraduatedSymbolRenderer",": you point it at a field, pick a classification method and class count, apply a sequential color ramp, and let QGIS shade every feature by its class. This guide walks through a complete, runnable recipe — including the crucial normalization step that prevents misleading maps. It builds directly on the ",[20,43,44],{"href":31},"graduated and categorized renderers"," workflow, narrowing that broader material down to the single most common thematic-map task.",[14,47,48],{},"The whole point of scripting a choropleth rather than clicking through the Symbology dialog is reproducibility: a script applies the same field, breaks, and ramp to this year's data and next year's, or to twenty county layers in a loop, without drift. The recipe below is deliberately linear so you can lift it into a larger automation pipeline and swap any single decision — the field, the method, the ramp — in isolation.",[50,51,56,57,56,61,56,65,56,72,56,81,56,56,87,56,56,236],"svg",{"viewBox":52,"role":53,"ariaLabel":54,"xmlns":55},"0 0 760 262","img","The five-stage PyQGIS choropleth pipeline: choose a numeric field, normalize it to a rate or density, classify into ranges, apply a sequential color ramp, then refresh the legend and canvas","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[58,59,60],"title",{},"The five choropleth decisions as a data-flow pipeline",[62,63,64],"desc",{},"Five labelled boxes feed left to right. Stage 1 picks the numeric field \"population\". Stage 2 normalizes it into a per-square-kilometre density. Stage 3 classifies the density into five quantile ranges. Stage 4 maps those ranges to a light-to-dark sequential color ramp. Stage 5 refreshes the legend and canvas so the symbology is visible.",[66,67],"rect",{"x":68,"y":68,"width":69,"height":70,"fill":71},"0","760","262","#f6f3ea",[73,74,80],"text",{"x":75,"y":76,"fill":77,"style":78,"textAnchor":79},"380","30","#17211d","text-anchor:middle;font-family:sans-serif;font-size:16px;font-weight:bold","middle","Five decisions, one choropleth pipeline",[73,82,86],{"x":75,"y":83,"fill":84,"style":85,"textAnchor":79},"50","#2f3b35","text-anchor:middle;font-family:sans-serif;font-size:12px","each stage feeds the next; swap any single decision in isolation",[88,89,91,91,92,91,101,91,108,91,113,91,118,91,123,91,131,91,91,137,91,140,91,143,91,146,91,149,91,152,91,155,91,91,158,91,161,91,163,91,166,91,169,91,172,91,175,91,91,178,91,181,91,184,91,186,91,189,91,192,91,198,91,202,91,206,91,210,91,91,214,91,217,91,220,91,223,91,226,91,229,91,232,56],"g",{"style":90},"font-family:sans-serif","\n    ",[66,93],{"x":94,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":77,"style":100},"16","70","128","164","8","#fffdf7","stroke-width:2",[102,103],"circle",{"cx":104,"cy":105,"r":106,"fill":107},"80","98","14","#0f766e",[73,109,112],{"x":104,"y":110,"fill":99,"style":111,"textAnchor":79},"103","text-anchor:middle;font-size:14px;font-weight:bold","1",[73,114,117],{"x":104,"y":115,"fill":77,"style":116,"textAnchor":79},"136","text-anchor:middle;font-size:13px;font-weight:bold","Field",[73,119,122],{"x":104,"y":120,"fill":84,"style":121,"textAnchor":79},"154","text-anchor:middle;font-size:10.5px","numeric attribute",[66,124],{"x":125,"y":126,"width":127,"height":128,"rx":129,"fill":130},"28","188","104","24","4","#26322d",[73,132,136],{"x":104,"y":133,"fill":134,"style":135,"textAnchor":79},"204","#d9f99d","text-anchor:middle;font-family:monospace;font-size:11px","population",[66,138],{"x":139,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":77,"style":100},"166",[102,141],{"cx":142,"cy":105,"r":106,"fill":107},"230",[73,144,145],{"x":142,"y":110,"fill":99,"style":111,"textAnchor":79},"2",[73,147,148],{"x":142,"y":115,"fill":77,"style":116,"textAnchor":79},"Normalize",[73,150,151],{"x":142,"y":120,"fill":84,"style":121,"textAnchor":79},"count → rate",[66,153],{"x":154,"y":126,"width":127,"height":128,"rx":129,"fill":130},"178",[73,156,157],{"x":142,"y":133,"fill":134,"style":135,"textAnchor":79},"pop \u002F $area",[66,159],{"x":160,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":77,"style":100},"316",[102,162],{"cx":75,"cy":105,"r":106,"fill":107},[73,164,165],{"x":75,"y":110,"fill":99,"style":111,"textAnchor":79},"3",[73,167,168],{"x":75,"y":115,"fill":77,"style":116,"textAnchor":79},"Classify",[73,170,171],{"x":75,"y":120,"fill":84,"style":121,"textAnchor":79},"method + count",[66,173],{"x":174,"y":126,"width":127,"height":128,"rx":129,"fill":130},"328",[73,176,177],{"x":75,"y":133,"fill":134,"style":135,"textAnchor":79},"quantile · 5",[66,179],{"x":180,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":77,"style":100},"466",[102,182],{"cx":183,"cy":105,"r":106,"fill":107},"530",[73,185,129],{"x":183,"y":110,"fill":99,"style":111,"textAnchor":79},[73,187,188],{"x":183,"y":115,"fill":77,"style":116,"textAnchor":79},"Color ramp",[73,190,191],{"x":183,"y":120,"fill":84,"style":121,"textAnchor":79},"sequential",[66,193],{"x":194,"y":126,"width":195,"height":128,"fill":196,"stroke":77,"style":197},"480","20","#eff3ff","stroke-width:0.75",[66,199],{"x":200,"y":126,"width":195,"height":128,"fill":201,"stroke":77,"style":197},"500","#bdd7e7",[66,203],{"x":204,"y":126,"width":195,"height":128,"fill":205,"stroke":77,"style":197},"520","#6baed6",[66,207],{"x":208,"y":126,"width":195,"height":128,"fill":209,"stroke":77,"style":197},"540","#3182bd",[66,211],{"x":212,"y":126,"width":195,"height":128,"fill":213,"stroke":77,"style":197},"560","#08519c",[66,215],{"x":216,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":77,"style":100},"616",[102,218],{"cx":219,"cy":105,"r":106,"fill":107},"680",[73,221,222],{"x":219,"y":110,"fill":99,"style":111,"textAnchor":79},"5",[73,224,225],{"x":219,"y":115,"fill":77,"style":116,"textAnchor":79},"Refresh",[73,227,228],{"x":219,"y":120,"fill":84,"style":121,"textAnchor":79},"legend + canvas",[66,230],{"x":231,"y":126,"width":127,"height":128,"rx":129,"fill":130},"628",[73,233,235],{"x":219,"y":133,"fill":134,"style":234,"textAnchor":79},"text-anchor:middle;font-family:monospace;font-size:10px","triggerRepaint()",[88,237,91,238,91,244,91,249,91,253,91,256,91,260,91,263,91,267,56],{"fill":107,"stroke":107},[239,240],"line",{"x1":241,"y1":242,"x2":243,"y2":242,"style":100},"146","152","160",[245,246],"polygon",{"points":247,"stroke":248},"160,147 166,152 160,157","none",[239,250],{"x1":251,"y1":242,"x2":252,"y2":242,"style":100},"296","310",[245,254],{"points":255,"stroke":248},"310,147 316,152 310,157",[239,257],{"x1":258,"y1":242,"x2":259,"y2":242,"style":100},"446","460",[245,261],{"points":262,"stroke":248},"460,147 466,152 460,157",[239,264],{"x1":265,"y1":242,"x2":266,"y2":242,"style":100},"596","610",[245,268],{"points":269,"stroke":248},"610,147 616,152 610,157",[14,271,272],{},"The pipeline has five decision points, and each maps to one step below: choose the numeric field, normalize it into a rate or density, classify it into a handful of ranges, apply a sequential color ramp, and refresh the legend and canvas so the finished symbology is visible.",[274,275,277],"h2",{"id":276},"prerequisites","Prerequisites",[279,280,281,289,292,303],"ul",{},[282,283,284,288],"li",{},[285,286,287],"strong",{},"QGIS 3.34 LTR"," (Python 3.12) with PyQGIS available, or another 3.x release.",[282,290,291],{},"A polygon layer (e.g. counties, census tracts) loaded in the project with a numeric field to map and, ideally, an area or population field for normalization.",[282,293,294,295,298,299,302],{},"The ",[285,296,297],{},"QGIS Python Console"," open (",[38,300,301],{},"Ctrl+Alt+P",").",[282,304,305],{},"A grasp of why raw counts mislead on choropleths — covered in step 2 below.",[274,307,309],{"id":308},"step-1-load-the-layer-and-inspect-the-field","Step 1: Load the Layer and Inspect the Field",[14,311,312],{},"Start by grabbing the layer and confirming the target field is numeric and reasonably populated. A choropleth on a field full of nulls or zeros produces a flat, uninformative map.",[314,315,320],"pre",{"className":316,"code":317,"language":318,"meta":319,"style":319},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"counties\")[0]\nfield_name = \"population\"\n\n# Sanity-check the field: type, range, and null count.\nidx = layer.fields().indexFromName(field_name)\nvalues = [f[field_name] for f in layer.getFeatures()\n          if f[field_name] not in (None, \"\")]\nprint(f\"Features: {layer.featureCount()}, valid values: {len(values)}\")\nprint(f\"Min: {min(values)}, Max: {max(values)}\")\n","python","",[38,321,322,340,347,372,383,388,395,406,429,459,500],{"__ignoreMap":319},[323,324,326,330,334,337],"span",{"class":239,"line":325},1,[323,327,329],{"class":328},"snl16","from",[323,331,333],{"class":332},"s95oV"," qgis.core ",[323,335,336],{"class":328},"import",[323,338,339],{"class":332}," QgsProject\n",[323,341,343],{"class":239,"line":342},2,[323,344,346],{"emptyLinePlaceholder":345},true,"\n",[323,348,350,353,356,359,363,366,369],{"class":239,"line":349},3,[323,351,352],{"class":332},"layer ",[323,354,355],{"class":328},"=",[323,357,358],{"class":332}," QgsProject.instance().mapLayersByName(",[323,360,362],{"class":361},"sU2Wk","\"counties\"",[323,364,365],{"class":332},")[",[323,367,68],{"class":368},"sDLfK",[323,370,371],{"class":332},"]\n",[323,373,375,378,380],{"class":239,"line":374},4,[323,376,377],{"class":332},"field_name ",[323,379,355],{"class":328},[323,381,382],{"class":361}," \"population\"\n",[323,384,386],{"class":239,"line":385},5,[323,387,346],{"emptyLinePlaceholder":345},[323,389,391],{"class":239,"line":390},6,[323,392,394],{"class":393},"sjoCn","# Sanity-check the field: type, range, and null count.\n",[323,396,398,401,403],{"class":239,"line":397},7,[323,399,400],{"class":332},"idx ",[323,402,355],{"class":328},[323,404,405],{"class":332}," layer.fields().indexFromName(field_name)\n",[323,407,409,412,414,417,420,423,426],{"class":239,"line":408},8,[323,410,411],{"class":332},"values ",[323,413,355],{"class":328},[323,415,416],{"class":332}," [f[field_name] ",[323,418,419],{"class":328},"for",[323,421,422],{"class":332}," f ",[323,424,425],{"class":328},"in",[323,427,428],{"class":332}," layer.getFeatures()\n",[323,430,432,435,438,441,444,447,450,453,456],{"class":239,"line":431},9,[323,433,434],{"class":328},"          if",[323,436,437],{"class":332}," f[field_name] ",[323,439,440],{"class":328},"not",[323,442,443],{"class":328}," in",[323,445,446],{"class":332}," (",[323,448,449],{"class":368},"None",[323,451,452],{"class":332},", ",[323,454,455],{"class":361},"\"\"",[323,457,458],{"class":332},")]\n",[323,460,462,465,468,471,474,477,480,483,486,489,492,494,497],{"class":239,"line":461},10,[323,463,464],{"class":368},"print",[323,466,467],{"class":332},"(",[323,469,470],{"class":328},"f",[323,472,473],{"class":361},"\"Features: ",[323,475,476],{"class":368},"{",[323,478,479],{"class":332},"layer.featureCount()",[323,481,482],{"class":368},"}",[323,484,485],{"class":361},", valid values: ",[323,487,488],{"class":368},"{len",[323,490,491],{"class":332},"(values)",[323,493,482],{"class":368},[323,495,496],{"class":361},"\"",[323,498,499],{"class":332},")\n",[323,501,503,505,507,509,512,515,517,519,522,525,527,529,531],{"class":239,"line":502},11,[323,504,464],{"class":368},[323,506,467],{"class":332},[323,508,470],{"class":328},[323,510,511],{"class":361},"\"Min: ",[323,513,514],{"class":368},"{min",[323,516,491],{"class":332},[323,518,482],{"class":368},[323,520,521],{"class":361},", Max: ",[323,523,524],{"class":368},"{max",[323,526,491],{"class":332},[323,528,482],{"class":368},[323,530,496],{"class":361},[323,532,499],{"class":332},[14,534,535,538,539,542,543,545,546,549,550,22,553,556,557,560,561,564],{},[285,536,537],{},"Breakdown:"," Iterating once up front catches problems before styling. ",[38,540,541],{},"f[field_name]"," reads the attribute by name; filtering out ",[38,544,449],{}," and empty strings avoids ",[38,547,548],{},"TypeError"," when you compute ",[38,551,552],{},"min",[38,554,555],{},"max",". If ",[38,558,559],{},"len(values)"," is far below ",[38,562,563],{},"featureCount()",", your map will have many unclassified (grey) polygons. The min\u002Fmax also give you a quick reality check: if the maximum is wildly larger than the median, the field is right-skewed and a quantile or Jenks classification will read far better than equal interval, which would crush most polygons into the lightest class.",[274,566,568],{"id":567},"step-2-decide-whether-to-normalize","Step 2: Decide Whether to Normalize",[14,570,571,572,575,576,579,580,583],{},"This is the step that separates a correct choropleth from a misleading one. Mapping a raw ",[17,573,574],{},"count"," — total population, total sales — mostly reproduces the size of each polygon: big counties look \"high\" simply because they are big. Normalize to a ",[17,577,578],{},"rate"," or ",[17,581,582],{},"density"," by dividing by area or by another count.",[14,585,586,587,590],{},"If your layer lacks a ready-made density field, compute one with a virtual field using ",[38,588,589],{},"QgsField"," and an expression, or precompute it with the field calculator. Here we create an in-memory density value via a layer-scoped expression field:",[314,592,594],{"className":316,"code":593,"language":318,"meta":319,"style":319},"from qgis.core import QgsField\nfrom qgis.PyQt.QtCore import QVariant\n\n# Add a virtual field: population per square kilometre.\ndensity_field = QgsField(\"pop_density\", QVariant.Double)\nlayer.addExpressionField(\n    \"population \u002F ($area \u002F 1000000)\", density_field\n)\nfield_name = \"pop_density\"\n",[38,595,596,607,619,623,628,644,649,657,661],{"__ignoreMap":319},[323,597,598,600,602,604],{"class":239,"line":325},[323,599,329],{"class":328},[323,601,333],{"class":332},[323,603,336],{"class":328},[323,605,606],{"class":332}," QgsField\n",[323,608,609,611,614,616],{"class":239,"line":342},[323,610,329],{"class":328},[323,612,613],{"class":332}," qgis.PyQt.QtCore ",[323,615,336],{"class":328},[323,617,618],{"class":332}," QVariant\n",[323,620,621],{"class":239,"line":349},[323,622,346],{"emptyLinePlaceholder":345},[323,624,625],{"class":239,"line":374},[323,626,627],{"class":393},"# Add a virtual field: population per square kilometre.\n",[323,629,630,633,635,638,641],{"class":239,"line":385},[323,631,632],{"class":332},"density_field ",[323,634,355],{"class":328},[323,636,637],{"class":332}," QgsField(",[323,639,640],{"class":361},"\"pop_density\"",[323,642,643],{"class":332},", QVariant.Double)\n",[323,645,646],{"class":239,"line":390},[323,647,648],{"class":332},"layer.addExpressionField(\n",[323,650,651,654],{"class":239,"line":397},[323,652,653],{"class":361},"    \"population \u002F ($area \u002F 1000000)\"",[323,655,656],{"class":332},", density_field\n",[323,658,659],{"class":239,"line":408},[323,660,499],{"class":332},[323,662,663,665,667],{"class":239,"line":431},[323,664,377],{"class":332},[323,666,355],{"class":328},[323,668,669],{"class":361}," \"pop_density\"\n",[14,671,672,674,675,678,679,682,683,686,687,690,691,694,695,699],{},[285,673,537],{}," ",[38,676,677],{},"addExpressionField()"," registers a ",[17,680,681],{},"virtual"," field computed on the fly — it is not written to disk, so it is perfect for a transient mapping field. ",[38,684,685],{},"$area"," returns the geometry area in the layer's CRS units; dividing by 1,000,000 converts square metres to square kilometres (valid only when the layer is in a projected CRS measured in metres). If your layer is in a geographic CRS, reproject it first or use ",[38,688,689],{},"area($geometry)"," with an ",[38,692,693],{},"transform()"," wrapper. To shade by an existing per-feature attribute color instead of a class, see ",[20,696,698],{"href":697},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","Set a Vector Layer Symbol Color in PyQGIS",".",[274,701,703],{"id":702},"step-3-choose-a-classification-method-and-class-count","Step 3: Choose a Classification Method and Class Count",[14,705,706],{},"The classification method decides where the class breaks fall, and the count decides how many shades the eye must distinguish. Four to six classes is the cartographic sweet spot; beyond seven, readers cannot reliably match a polygon's shade to the legend.",[314,708,710],{"className":316,"code":709,"language":318,"meta":319,"style":319},"from qgis.core import (\n    QgsGraduatedSymbolRenderer,\n    QgsClassificationQuantile,\n    QgsSymbol,\n)\n\nclass_count = 5\n\nrenderer = QgsGraduatedSymbolRenderer(field_name)\nrenderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\nrenderer.setClassificationMethod(QgsClassificationQuantile())\nrenderer.updateClasses(layer, class_count)\n\nfor r in renderer.ranges():\n    print(f\"{r.lowerValue():.1f} – {r.upperValue():.1f}: {r.label()}\")\n",[38,711,712,723,728,733,738,742,746,756,760,770,775,780,786,791,804],{"__ignoreMap":319},[323,713,714,716,718,720],{"class":239,"line":325},[323,715,329],{"class":328},[323,717,333],{"class":332},[323,719,336],{"class":328},[323,721,722],{"class":332}," (\n",[323,724,725],{"class":239,"line":342},[323,726,727],{"class":332},"    QgsGraduatedSymbolRenderer,\n",[323,729,730],{"class":239,"line":349},[323,731,732],{"class":332},"    QgsClassificationQuantile,\n",[323,734,735],{"class":239,"line":374},[323,736,737],{"class":332},"    QgsSymbol,\n",[323,739,740],{"class":239,"line":385},[323,741,499],{"class":332},[323,743,744],{"class":239,"line":390},[323,745,346],{"emptyLinePlaceholder":345},[323,747,748,751,753],{"class":239,"line":397},[323,749,750],{"class":332},"class_count ",[323,752,355],{"class":328},[323,754,755],{"class":368}," 5\n",[323,757,758],{"class":239,"line":408},[323,759,346],{"emptyLinePlaceholder":345},[323,761,762,765,767],{"class":239,"line":431},[323,763,764],{"class":332},"renderer ",[323,766,355],{"class":328},[323,768,769],{"class":332}," QgsGraduatedSymbolRenderer(field_name)\n",[323,771,772],{"class":239,"line":461},[323,773,774],{"class":332},"renderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\n",[323,776,777],{"class":239,"line":502},[323,778,779],{"class":332},"renderer.setClassificationMethod(QgsClassificationQuantile())\n",[323,781,783],{"class":239,"line":782},12,[323,784,785],{"class":332},"renderer.updateClasses(layer, class_count)\n",[323,787,789],{"class":239,"line":788},13,[323,790,346],{"emptyLinePlaceholder":345},[323,792,794,796,799,801],{"class":239,"line":793},14,[323,795,419],{"class":328},[323,797,798],{"class":332}," r ",[323,800,425],{"class":328},[323,802,803],{"class":332}," renderer.ranges():\n",[323,805,807,810,812,814,816,818,821,824,826,829,831,834,836,838,841,843,846,848,850],{"class":239,"line":806},15,[323,808,809],{"class":368},"    print",[323,811,467],{"class":332},[323,813,470],{"class":328},[323,815,496],{"class":361},[323,817,476],{"class":368},[323,819,820],{"class":332},"r.lowerValue()",[323,822,823],{"class":328},":.1f",[323,825,482],{"class":368},[323,827,828],{"class":361}," – ",[323,830,476],{"class":368},[323,832,833],{"class":332},"r.upperValue()",[323,835,823],{"class":328},[323,837,482],{"class":368},[323,839,840],{"class":361},": ",[323,842,476],{"class":368},[323,844,845],{"class":332},"r.label()",[323,847,482],{"class":368},[323,849,496],{"class":361},[323,851,499],{"class":332},[14,853,854,674,856,859,860,863,864,867,868,699],{},[285,855,537],{},[38,857,858],{},"QgsClassificationQuantile"," puts an equal number of features in each class, which balances how much map ink each shade gets — a sensible default for choropleths. ",[38,861,862],{},"updateClasses(layer, class_count)"," scans the field and computes the five break ranges. Printing ",[38,865,866],{},"ranges()"," confirms the breaks are sensible before you commit. For clustered data where quantile splits look arbitrary, switch to Jenks — see ",[20,869,871],{"href":870},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002F","Classify a Layer with Natural Breaks (Jenks) in PyQGIS",[274,873,875],{"id":874},"step-4-apply-a-sequential-color-ramp","Step 4: Apply a Sequential Color Ramp",[14,877,878,879,881,882,699],{},"Choropleths represent ordered magnitude, so they need a ",[17,880,191],{}," ramp where lightness increases with value. Pull one from the user's style library via ",[38,883,884],{},"QgsStyle.defaultStyle()",[314,886,888],{"className":316,"code":887,"language":318,"meta":319,"style":319},"from qgis.core import QgsStyle\n\nramp = QgsStyle.defaultStyle().colorRamp(\"Blues\")\nrenderer.updateColorRamp(ramp)\n",[38,889,890,901,905,920],{"__ignoreMap":319},[323,891,892,894,896,898],{"class":239,"line":325},[323,893,329],{"class":328},[323,895,333],{"class":332},[323,897,336],{"class":328},[323,899,900],{"class":332}," QgsStyle\n",[323,902,903],{"class":239,"line":342},[323,904,346],{"emptyLinePlaceholder":345},[323,906,907,910,912,915,918],{"class":239,"line":349},[323,908,909],{"class":332},"ramp ",[323,911,355],{"class":328},[323,913,914],{"class":332}," QgsStyle.defaultStyle().colorRamp(",[323,916,917],{"class":361},"\"Blues\"",[323,919,499],{"class":332},[323,921,922],{"class":239,"line":374},[323,923,924],{"class":332},"renderer.updateColorRamp(ramp)\n",[14,926,927,674,929,932,933,936,937,940,941,944,945,948,949,953],{},[285,928,537],{},[38,930,931],{},"colorRamp(\"Blues\")"," returns a built-in sequential ramp. ",[38,934,935],{},"updateColorRamp()"," recolors all five classes in light-to-dark order, so the darkest blue marks the highest-density regions. Avoid diverging ramps (",[38,938,939],{},"RdYlBu",") unless your data has a meaningful midpoint, and avoid qualitative ramps entirely — they destroy the sense of order a choropleth depends on. To reverse the direction, call ",[38,942,943],{},"ramp.invert()"," before passing it. The same ",[38,946,947],{},"QgsStyle"," ramp objects drive continuous raster symbology too — see ",[20,950,952],{"href":951},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fapply-color-ramp-to-raster-pyqgis\u002F","Apply a Color Ramp to a Raster in PyQGIS"," for the single-band pseudocolor equivalent.",[274,955,957],{"id":956},"step-4b-format-the-legend-labels","Step 4b: Format the Legend Labels",[14,959,960,961,964,965,968],{},"The default range labels read like ",[38,962,963],{},"1234.5 - 6789.0",", which is precise but unfriendly. A choropleth's legend is part of the map's message, so round the numbers and add units. ",[38,966,967],{},"setLabelFormat()"," controls the label template for every class at once.",[314,970,972],{"className":316,"code":971,"language":318,"meta":319,"style":319},"from qgis.core import QgsRendererRangeLabelFormat\n\nlabel_format = QgsRendererRangeLabelFormat(\"%1 – %2 \u002Fkm²\", 0)\nrenderer.setLabelFormat(label_format, updateRanges=True)\n\nfor r in renderer.ranges():\n    print(r.label())\n",[38,973,974,985,989,1008,1024,1028,1038],{"__ignoreMap":319},[323,975,976,978,980,982],{"class":239,"line":325},[323,977,329],{"class":328},[323,979,333],{"class":332},[323,981,336],{"class":328},[323,983,984],{"class":332}," QgsRendererRangeLabelFormat\n",[323,986,987],{"class":239,"line":342},[323,988,346],{"emptyLinePlaceholder":345},[323,990,991,994,996,999,1002,1004,1006],{"class":239,"line":349},[323,992,993],{"class":332},"label_format ",[323,995,355],{"class":328},[323,997,998],{"class":332}," QgsRendererRangeLabelFormat(",[323,1000,1001],{"class":361},"\"%1 – %2 \u002Fkm²\"",[323,1003,452],{"class":332},[323,1005,68],{"class":368},[323,1007,499],{"class":332},[323,1009,1010,1013,1017,1019,1022],{"class":239,"line":374},[323,1011,1012],{"class":332},"renderer.setLabelFormat(label_format, ",[323,1014,1016],{"class":1015},"s9osk","updateRanges",[323,1018,355],{"class":328},[323,1020,1021],{"class":368},"True",[323,1023,499],{"class":332},[323,1025,1026],{"class":239,"line":385},[323,1027,346],{"emptyLinePlaceholder":345},[323,1029,1030,1032,1034,1036],{"class":239,"line":390},[323,1031,419],{"class":328},[323,1033,798],{"class":332},[323,1035,425],{"class":328},[323,1037,803],{"class":332},[323,1039,1040,1042],{"class":239,"line":397},[323,1041,809],{"class":368},[323,1043,1044],{"class":332},"(r.label())\n",[14,1046,1047,674,1049,1052,1053,1056,1057,1060,1061,1063,1064,1067,1068,1071],{},[285,1048,537],{},[38,1050,1051],{},"QgsRendererRangeLabelFormat(template, precision)"," sets a label template where ",[38,1054,1055],{},"%1"," and ",[38,1058,1059],{},"%2"," are substituted with each class's lower and upper bounds, and the second argument fixes the decimal precision — ",[38,1062,68],{}," rounds to whole numbers. Passing ",[38,1065,1066],{},"updateRanges=True"," rewrites the labels on the existing ranges in place. The result is a legend that reads ",[38,1069,1070],{},"0 – 85 \u002Fkm²"," instead of an over-precise machine string, which is what a finished map needs.",[274,1073,1075],{"id":1074},"step-5-set-the-renderer-and-refresh-legend-and-canvas","Step 5: Set the Renderer and Refresh Legend and Canvas",[14,1077,1078],{},"Assign the renderer to the layer, then explicitly refresh both the canvas and the Layers-panel legend so your new symbology is visible and the swatches appear.",[314,1080,1082],{"className":316,"code":1081,"language":318,"meta":319,"style":319},"from qgis.utils import iface\n\nlayer.setRenderer(renderer)\nlayer.triggerRepaint()\n\nif iface is not None:\n    iface.layerTreeView().refreshLayerSymbology(layer.id())\n    iface.mapCanvas().refresh()\n\n# Optional: persist the styling for reuse.\nlayer.saveNamedStyle(\"\u002Ftmp\u002Fcounties_choropleth.qml\")\n",[38,1083,1084,1096,1100,1105,1110,1114,1134,1139,1144,1148,1153],{"__ignoreMap":319},[323,1085,1086,1088,1091,1093],{"class":239,"line":325},[323,1087,329],{"class":328},[323,1089,1090],{"class":332}," qgis.utils ",[323,1092,336],{"class":328},[323,1094,1095],{"class":332}," iface\n",[323,1097,1098],{"class":239,"line":342},[323,1099,346],{"emptyLinePlaceholder":345},[323,1101,1102],{"class":239,"line":349},[323,1103,1104],{"class":332},"layer.setRenderer(renderer)\n",[323,1106,1107],{"class":239,"line":374},[323,1108,1109],{"class":332},"layer.triggerRepaint()\n",[323,1111,1112],{"class":239,"line":385},[323,1113,346],{"emptyLinePlaceholder":345},[323,1115,1116,1119,1122,1125,1128,1131],{"class":239,"line":390},[323,1117,1118],{"class":328},"if",[323,1120,1121],{"class":332}," iface ",[323,1123,1124],{"class":328},"is",[323,1126,1127],{"class":328}," not",[323,1129,1130],{"class":368}," None",[323,1132,1133],{"class":332},":\n",[323,1135,1136],{"class":239,"line":397},[323,1137,1138],{"class":332},"    iface.layerTreeView().refreshLayerSymbology(layer.id())\n",[323,1140,1141],{"class":239,"line":408},[323,1142,1143],{"class":332},"    iface.mapCanvas().refresh()\n",[323,1145,1146],{"class":239,"line":431},[323,1147,346],{"emptyLinePlaceholder":345},[323,1149,1150],{"class":239,"line":461},[323,1151,1152],{"class":393},"# Optional: persist the styling for reuse.\n",[323,1154,1155,1158,1161],{"class":239,"line":502},[323,1156,1157],{"class":332},"layer.saveNamedStyle(",[323,1159,1160],{"class":361},"\"\u002Ftmp\u002Fcounties_choropleth.qml\"",[323,1162,499],{"class":332},[14,1164,1165,674,1167,1170,1171,1173,1174,1177,1178,1181,1182,1185,1186,1189,1190,699],{},[285,1166,537],{},[38,1168,1169],{},"setRenderer()"," swaps in the graduated renderer; ",[38,1172,235],{}," invalidates the cached image. ",[38,1175,1176],{},"refreshLayerSymbology()"," rebuilds the legend entries that show each class's color and range. The ",[38,1179,1180],{},"iface"," guard keeps the snippet runnable in standalone scripts. ",[38,1183,1184],{},"saveNamedStyle()"," writes a ",[38,1187,1188],{},".qml"," you can reload on other projects with ",[38,1191,1192],{},"layer.loadNamedStyle()",[274,1194,1196],{"id":1195},"step-6-wrap-it-as-a-reusable-function","Step 6: Wrap It as a Reusable Function",[14,1198,1199],{},"The payoff of scripting arrives when you apply the same choropleth recipe to several layers with one call. Fold the steps into a function that takes the layer, field, class count, and ramp name, and returns the styled layer.",[314,1201,1203],{"className":316,"code":1202,"language":318,"meta":319,"style":319},"from qgis.core import (\n    QgsGraduatedSymbolRenderer,\n    QgsClassificationQuantile,\n    QgsSymbol,\n    QgsStyle,\n)\nfrom qgis.utils import iface\n\n\ndef apply_choropleth(layer, field, classes=5, ramp_name=\"Blues\"):\n    \"\"\"Style a polygon layer as a quantile choropleth.\"\"\"\n    renderer = QgsGraduatedSymbolRenderer(field)\n    renderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\n    renderer.setClassificationMethod(QgsClassificationQuantile())\n    renderer.updateClasses(layer, classes)\n\n    ramp = QgsStyle.defaultStyle().colorRamp(ramp_name)\n    if ramp is None:\n        raise ValueError(f\"Color ramp '{ramp_name}' is not installed\")\n    renderer.updateColorRamp(ramp)\n\n    layer.setRenderer(renderer)\n    layer.triggerRepaint()\n    if iface is not None:\n        iface.layerTreeView().refreshLayerSymbology(layer.id())\n    return layer\n\n\n# Apply to every selected layer in the panel.\nfor lyr in iface.layerTreeView().selectedLayers():\n    apply_choropleth(lyr, \"pop_density\", classes=5, ramp_name=\"YlGnBu\")\n",[38,1204,1205,1215,1219,1223,1227,1232,1236,1246,1250,1254,1280,1285,1295,1300,1305,1310,1315,1326,1341,1369,1375,1380,1386,1392,1407,1413,1422,1427,1432,1438,1451],{"__ignoreMap":319},[323,1206,1207,1209,1211,1213],{"class":239,"line":325},[323,1208,329],{"class":328},[323,1210,333],{"class":332},[323,1212,336],{"class":328},[323,1214,722],{"class":332},[323,1216,1217],{"class":239,"line":342},[323,1218,727],{"class":332},[323,1220,1221],{"class":239,"line":349},[323,1222,732],{"class":332},[323,1224,1225],{"class":239,"line":374},[323,1226,737],{"class":332},[323,1228,1229],{"class":239,"line":385},[323,1230,1231],{"class":332},"    QgsStyle,\n",[323,1233,1234],{"class":239,"line":390},[323,1235,499],{"class":332},[323,1237,1238,1240,1242,1244],{"class":239,"line":397},[323,1239,329],{"class":328},[323,1241,1090],{"class":332},[323,1243,336],{"class":328},[323,1245,1095],{"class":332},[323,1247,1248],{"class":239,"line":408},[323,1249,346],{"emptyLinePlaceholder":345},[323,1251,1252],{"class":239,"line":431},[323,1253,346],{"emptyLinePlaceholder":345},[323,1255,1256,1259,1263,1266,1268,1270,1273,1275,1277],{"class":239,"line":461},[323,1257,1258],{"class":328},"def",[323,1260,1262],{"class":1261},"svObZ"," apply_choropleth",[323,1264,1265],{"class":332},"(layer, field, classes",[323,1267,355],{"class":328},[323,1269,222],{"class":368},[323,1271,1272],{"class":332},", ramp_name",[323,1274,355],{"class":328},[323,1276,917],{"class":361},[323,1278,1279],{"class":332},"):\n",[323,1281,1282],{"class":239,"line":502},[323,1283,1284],{"class":361},"    \"\"\"Style a polygon layer as a quantile choropleth.\"\"\"\n",[323,1286,1287,1290,1292],{"class":239,"line":782},[323,1288,1289],{"class":332},"    renderer ",[323,1291,355],{"class":328},[323,1293,1294],{"class":332}," QgsGraduatedSymbolRenderer(field)\n",[323,1296,1297],{"class":239,"line":788},[323,1298,1299],{"class":332},"    renderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\n",[323,1301,1302],{"class":239,"line":793},[323,1303,1304],{"class":332},"    renderer.setClassificationMethod(QgsClassificationQuantile())\n",[323,1306,1307],{"class":239,"line":806},[323,1308,1309],{"class":332},"    renderer.updateClasses(layer, classes)\n",[323,1311,1313],{"class":239,"line":1312},16,[323,1314,346],{"emptyLinePlaceholder":345},[323,1316,1318,1321,1323],{"class":239,"line":1317},17,[323,1319,1320],{"class":332},"    ramp ",[323,1322,355],{"class":328},[323,1324,1325],{"class":332}," QgsStyle.defaultStyle().colorRamp(ramp_name)\n",[323,1327,1329,1332,1335,1337,1339],{"class":239,"line":1328},18,[323,1330,1331],{"class":328},"    if",[323,1333,1334],{"class":332}," ramp ",[323,1336,1124],{"class":328},[323,1338,1130],{"class":368},[323,1340,1133],{"class":332},[323,1342,1344,1347,1350,1352,1354,1357,1359,1362,1364,1367],{"class":239,"line":1343},19,[323,1345,1346],{"class":328},"        raise",[323,1348,1349],{"class":368}," ValueError",[323,1351,467],{"class":332},[323,1353,470],{"class":328},[323,1355,1356],{"class":361},"\"Color ramp '",[323,1358,476],{"class":368},[323,1360,1361],{"class":332},"ramp_name",[323,1363,482],{"class":368},[323,1365,1366],{"class":361},"' is not installed\"",[323,1368,499],{"class":332},[323,1370,1372],{"class":239,"line":1371},20,[323,1373,1374],{"class":332},"    renderer.updateColorRamp(ramp)\n",[323,1376,1378],{"class":239,"line":1377},21,[323,1379,346],{"emptyLinePlaceholder":345},[323,1381,1383],{"class":239,"line":1382},22,[323,1384,1385],{"class":332},"    layer.setRenderer(renderer)\n",[323,1387,1389],{"class":239,"line":1388},23,[323,1390,1391],{"class":332},"    layer.triggerRepaint()\n",[323,1393,1395,1397,1399,1401,1403,1405],{"class":239,"line":1394},24,[323,1396,1331],{"class":328},[323,1398,1121],{"class":332},[323,1400,1124],{"class":328},[323,1402,1127],{"class":328},[323,1404,1130],{"class":368},[323,1406,1133],{"class":332},[323,1408,1410],{"class":239,"line":1409},25,[323,1411,1412],{"class":332},"        iface.layerTreeView().refreshLayerSymbology(layer.id())\n",[323,1414,1416,1419],{"class":239,"line":1415},26,[323,1417,1418],{"class":328},"    return",[323,1420,1421],{"class":332}," layer\n",[323,1423,1425],{"class":239,"line":1424},27,[323,1426,346],{"emptyLinePlaceholder":345},[323,1428,1430],{"class":239,"line":1429},28,[323,1431,346],{"emptyLinePlaceholder":345},[323,1433,1435],{"class":239,"line":1434},29,[323,1436,1437],{"class":393},"# Apply to every selected layer in the panel.\n",[323,1439,1441,1443,1446,1448],{"class":239,"line":1440},30,[323,1442,419],{"class":328},[323,1444,1445],{"class":332}," lyr ",[323,1447,425],{"class":328},[323,1449,1450],{"class":332}," iface.layerTreeView().selectedLayers():\n",[323,1452,1454,1457,1459,1461,1464,1466,1468,1470,1472,1474,1477],{"class":239,"line":1453},31,[323,1455,1456],{"class":332},"    apply_choropleth(lyr, ",[323,1458,640],{"class":361},[323,1460,452],{"class":332},[323,1462,1463],{"class":1015},"classes",[323,1465,355],{"class":328},[323,1467,222],{"class":368},[323,1469,452],{"class":332},[323,1471,1361],{"class":1015},[323,1473,355],{"class":328},[323,1475,1476],{"class":361},"\"YlGnBu\"",[323,1478,499],{"class":332},[14,1480,1481,1483,1484,1487,1488,1490,1491,1494,1495,579,1497,1499],{},[285,1482,537],{}," Folding the steps into ",[38,1485,1486],{},"apply_choropleth()"," makes the recipe a single composable unit; the ",[38,1489,449],{}," check on the ramp turns a silent failure into a clear error. Looping over ",[38,1492,1493],{},"selectedLayers()"," styles a whole set of layers identically — the exact reproducibility win that justifies scripting over the dialog. Pass a different ",[38,1496,1361],{},[38,1498,1463],{}," per call when layers need to differ.",[274,1501,1503],{"id":1502},"why-normalising-is-not-optional","Why normalising is not optional",[14,1505,1506],{},"A choropleth shaded by a raw count maps population, not the thing you meant to show. Large areas look important because they are large, and the map answers a question nobody asked.",[14,1508,1509],{},[50,1510,1513,1516,1519,1522,1527,1583],{"viewBox":1511,"role":53,"ariaLabel":1512,"xmlns":55},"0 0 760 244","The same regions shaded by raw count, where the largest polygon dominates, and by count per square kilometre, where a small dense region stands out instead",[58,1514,1515],{},"Raw count versus a rate",[62,1517,1518],{},"Four regions of very different sizes are shaded twice. Shaded by raw count, the largest region is darkest simply because it contains more of everything. Shaded by count per square kilometre, a small dense region becomes the darkest and the large region falls to the middle of the scale.",[66,1520],{"x":68,"y":68,"width":69,"height":1521,"fill":71},"244",[73,1523,1526],{"x":75,"y":1524,"style":1525,"fill":77,"textAnchor":79},"26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","Shade a count and you have mapped area, not the phenomenon",[88,1528,1529,1537,1542,1548,1553,1557,1563,1567,1571,1575,1579],{},[66,1530],{"x":94,"y":1531,"width":1532,"height":1533,"rx":1534,"fill":99,"stroke":1535,"style":1536},"46","356","182","10","#b91c1c","stroke-width:2.5",[73,1538,1541],{"x":1539,"y":95,"style":1540,"fill":1535,"textAnchor":79},"194","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","shaded by raw count",[1543,1544],"path",{"d":1545,"fill":107,"fillOpacity":1546,"stroke":77,"style":1547},"M44 200 L44 90 L200 84 L212 200 Z",0.72,"stroke-width:1.4",[73,1549,1552],{"x":96,"y":1550,"style":1551,"fill":71,"textAnchor":79},"150","text-anchor:middle;font-size:11px;font-family:sans-serif","4 200",[1543,1554],{"d":1555,"fill":107,"fillOpacity":1556,"stroke":77,"style":1547},"M212 200 L212 140 L282 136 L286 200 Z",0.3,[73,1558,1562],{"x":1559,"y":1560,"style":1561,"fill":84,"textAnchor":79},"249","176","text-anchor:middle;font-size:10px;font-family:sans-serif","980",[1543,1564],{"d":1565,"fill":107,"fillOpacity":1566,"stroke":77,"style":1547},"M212 136 L212 84 L286 82 L286 132 Z",0.22,[73,1568,1570],{"x":1559,"y":1569,"style":1561,"fill":84,"textAnchor":79},"112","640",[1543,1572],{"d":1573,"fill":107,"fillOpacity":1574,"stroke":77,"style":1547},"M290 200 L290 82 L344 82 L344 200 Z",0.15,[73,1576,1578],{"x":1577,"y":241,"style":1561,"fill":84,"textAnchor":79},"317","410",[73,1580,1582],{"x":1539,"y":1581,"style":1551,"fill":1535,"textAnchor":79},"220","the biggest polygon wins",[88,1584,1585,1589,1593,1597,1600,1604,1608,1612,1615,1619,1623],{},[66,1586],{"x":1587,"y":1531,"width":1532,"height":1533,"rx":1534,"fill":99,"stroke":1588,"style":1536},"388","#15803d",[73,1590,1592],{"x":1591,"y":95,"style":1540,"fill":1588,"textAnchor":79},"566","shaded by count \u002F km²",[1543,1594],{"d":1595,"fill":107,"fillOpacity":1596,"stroke":77,"style":1547},"M416 200 L416 90 L572 84 L584 200 Z",0.32,[73,1598,1599],{"x":200,"y":1550,"style":1551,"fill":84,"textAnchor":79},"18\u002Fkm²",[1543,1601],{"d":1602,"fill":107,"fillOpacity":1603,"stroke":77,"style":1547},"M584 200 L584 140 L654 136 L658 200 Z",0.55,[73,1605,1607],{"x":1606,"y":1560,"style":1561,"fill":77,"textAnchor":79},"621","44\u002Fkm²",[1543,1609],{"d":1610,"fill":107,"fillOpacity":1611,"stroke":77,"style":1547},"M584 136 L584 84 L658 82 L658 132 Z",0.24,[73,1613,1614],{"x":1606,"y":1569,"style":1561,"fill":84,"textAnchor":79},"12\u002Fkm²",[1543,1616],{"d":1617,"fill":107,"fillOpacity":1618,"stroke":77,"style":1547},"M662 200 L662 82 L716 82 L716 200 Z",0.85,[73,1620,1622],{"x":1621,"y":241,"style":1561,"fill":71,"textAnchor":79},"689","96",[73,1624,1625],{"x":1591,"y":1581,"style":1551,"fill":1588,"textAnchor":79},"the dense region stands out",[274,1627,1629],{"id":1628},"sequential-diverging-or-qualitative","Sequential, diverging or qualitative",[14,1631,1632],{},"The ramp family encodes the structure of the data, and choosing one that does not match misleads the reader before they have read a single number.",[14,1634,1635],{},[50,1636,1639,1642,1645,1648,1651,1684,1711,1735],{"viewBox":1637,"role":53,"ariaLabel":1638,"xmlns":55},"0 0 760 240","Three colour ramp families — sequential, diverging and qualitative — each shown as a swatch strip with the kind of data it suits",[58,1640,1641],{},"Three ramp families and what each encodes",[62,1643,1644],{},"A sequential ramp runs light to dark in one hue and encodes an ordered quantity with a meaningful low end. A diverging ramp runs from one hue through a neutral midpoint to another and encodes deviation from a reference value. A qualitative ramp uses unrelated hues of similar lightness and encodes categories with no order.",[66,1646],{"x":68,"y":68,"width":69,"height":1647,"fill":71},"240",[73,1649,1650],{"x":75,"y":1524,"style":1525,"fill":77,"textAnchor":79},"Match the ramp family to the structure of the data",[88,1652,1653,1658,1679],{},[73,1654,191],{"x":1655,"y":1656,"style":1657,"fill":107},"36","66","font-size:11.5px;font-weight:bold;font-family:sans-serif",[88,1659,1660,1665,1669,1672,1675],{},[66,1661],{"x":243,"y":1662,"width":1663,"height":128,"fill":107,"fillOpacity":1574,"stroke":77,"style":1664},"52","76","stroke-width:0.8",[66,1666],{"x":1667,"y":1662,"width":1663,"height":128,"fill":107,"fillOpacity":1668,"stroke":77,"style":1664},"236",0.35,[66,1670],{"x":1671,"y":1662,"width":1663,"height":128,"fill":107,"fillOpacity":1603,"stroke":77,"style":1664},"312",[66,1673],{"x":1587,"y":1662,"width":1663,"height":128,"fill":107,"fillOpacity":1674,"stroke":77,"style":1664},0.75,[66,1676],{"x":1677,"y":1662,"width":1663,"height":128,"fill":107,"fillOpacity":1678,"stroke":77,"style":1664},"464",0.95,[73,1680,1683],{"x":212,"y":1681,"style":1682,"fill":84},"69","font-size:11px;font-family:sans-serif","an ordered quantity · density, income",[88,1685,1686,1691,1707],{},[73,1687,1690],{"x":1655,"y":1688,"style":1657,"fill":1689},"132","#b45309","diverging",[88,1692,1693,1696,1699,1702,1705],{},[66,1694],{"x":243,"y":1695,"width":1663,"height":128,"fill":1689,"fillOpacity":1618,"stroke":77,"style":1664},"118",[66,1697],{"x":1667,"y":1695,"width":1663,"height":128,"fill":1689,"fillOpacity":1698,"stroke":77,"style":1664},0.4,[66,1700],{"x":1671,"y":1695,"width":1663,"height":128,"fill":1701,"stroke":77,"style":1664},"#e7e2d4",[66,1703],{"x":1587,"y":1695,"width":1663,"height":128,"fill":1704,"fillOpacity":1698,"stroke":77,"style":1664},"#2563eb",[66,1706],{"x":1677,"y":1695,"width":1663,"height":128,"fill":1704,"fillOpacity":1618,"stroke":77,"style":1664},[73,1708,1710],{"x":212,"y":1709,"style":1682,"fill":84},"135","deviation from a reference · change",[88,1712,1713,1717,1731],{},[73,1714,1716],{"x":1655,"y":1715,"style":1657,"fill":1704},"198","qualitative",[88,1718,1719,1723,1725,1727,1729],{},[66,1720],{"x":243,"y":1721,"width":1663,"height":128,"fill":107,"fillOpacity":1722,"stroke":77,"style":1664},"184",0.6,[66,1724],{"x":1667,"y":1721,"width":1663,"height":128,"fill":1689,"fillOpacity":1722,"stroke":77,"style":1664},[66,1726],{"x":1671,"y":1721,"width":1663,"height":128,"fill":1704,"fillOpacity":1722,"stroke":77,"style":1664},[66,1728],{"x":1587,"y":1721,"width":1663,"height":128,"fill":1588,"fillOpacity":1722,"stroke":77,"style":1664},[66,1730],{"x":1677,"y":1721,"width":1663,"height":128,"fill":1535,"fillOpacity":1722,"stroke":77,"style":1664},[73,1732,1734],{"x":212,"y":1733,"style":1682,"fill":84},"201","unordered categories · zoning, land use",[73,1736,1738],{"x":75,"y":142,"style":1551,"fill":1737,"textAnchor":79},"#59645f","a sequential ramp on categories invents an order that is not there",[274,1740,1742],{"id":1741},"qgis-version-compatibility","QGIS Version Compatibility",[14,1744,1745,1746,1749],{},"The recipe targets ",[285,1747,1748],{},"QGIS 3.34 LTR (Python 3.12)"," and uses only stable APIs.",[1751,1752,1753,1766],"table",{},[1754,1755,1756],"thead",{},[1757,1758,1759,1763],"tr",{},[1760,1761,1762],"th",{},"QGIS \u002F Python",[1760,1764,1765],{},"Notes",[1767,1768,1769,1780,1793,1801],"tbody",{},[1757,1770,1771,1777],{},[1772,1773,1774],"td",{},[285,1775,1776],{},"3.34 LTR (Python 3.12)",[1772,1778,1779],{},"Baseline. Every snippet runs as written.",[1757,1781,1782,1785],{},[1772,1783,1784],{},"3.28 LTR (Python 3.9)",[1772,1786,1787,1788,1056,1791,699],{},"Fully supported, including ",[38,1789,1790],{},"addExpressionField",[38,1792,858],{},[1757,1794,1795,1798],{},[1772,1796,1797],{},"3.40 \u002F 3.44",[1772,1799,1800],{},"Identical API; more bundled sequential ramps available by name.",[1757,1802,1803,1806],{},[1772,1804,1805],{},"Pre-3.10",[1772,1807,1808,1811,1812,1815],{},[38,1809,1810],{},"setClassificationMethod"," unavailable; older code used the deprecated ",[38,1813,1814],{},"createRenderer(layer, field, classes, mode, symbol, ramp)"," constructor.",[14,1817,1818,1819,1822,1823,1826,1827,1829],{},"On Python 3.9 builds, ",[38,1820,1821],{},"QVariant.Double"," still works; on newer PyQt6-based builds the same enum is reachable as ",[38,1824,1825],{},"QVariant.Type.Double",", but ",[38,1828,1821],{}," remains aliased for compatibility.",[274,1831,1833],{"id":1832},"troubleshooting","Troubleshooting",[14,1835,1836,1839,1840,1843,1844,1847],{},[285,1837,1838],{},"Map is one solid color."," You likely set the renderer without calling ",[38,1841,1842],{},"updateClasses()",", so ",[38,1845,1846],{},"renderer.ranges()"," is empty. Print it to confirm, then re-run the classification step.",[14,1849,1850,1853,1854,1857,1858,1861],{},[285,1851,1852],{},"Many polygons render grey\u002Funclassified."," Those features hold ",[38,1855,1856],{},"NULL"," or out-of-range values. Either filter them out, or call ",[38,1859,1860],{},"renderer.setClassAttribute()"," on a cleaned field. Confirm the field is numeric — string fields silently produce empty classes.",[14,1863,1864,1867,1868,1870],{},[285,1865,1866],{},"Big regions dominate the map."," You mapped a raw count instead of a density or rate. Return to step 2 and divide by ",[38,1869,685],{}," or by population.",[14,1872,1873,674,1879,1882,1883,1885,1886,1889],{},[285,1874,1875,1878],{},[38,1876,1877],{},"updateColorRamp"," raises AttributeError.",[38,1880,1881],{},"colorRamp(\"Name\")"," returned ",[38,1884,449],{}," because that ramp name is not installed. Run ",[38,1887,1888],{},"QgsStyle.defaultStyle().colorRampNames()"," and pick an existing name.",[14,1891,1892,1895,1896,1898,1899,699],{},[285,1893,1894],{},"Legend swatches do not update."," Calling ",[38,1897,235],{}," alone refreshes the canvas but not the tree. Add ",[38,1900,1901],{},"iface.layerTreeView().refreshLayerSymbology(layer.id())",[274,1903,1905],{"id":1904},"conclusion","Conclusion",[14,1907,1908,1909,1911,1912,1914],{},"A trustworthy choropleth is three decisions made deliberately: normalize raw counts into rates, pick a classification method and a modest class count, and apply a sequential ramp that encodes order. With ",[38,1910,40],{}," you can encode all three in a dozen lines, reproduce them across projects, and save the result as a reusable ",[38,1913,1188],{},". From here, experiment with Jenks breaks for clustered data and compare how each method reshapes the same map.",[274,1916,1918],{"id":1917},"frequently-asked-questions","Frequently Asked Questions",[14,1920,1921,1924],{},[285,1922,1923],{},"How many classes should a choropleth have?","\nFour to six. The human eye struggles to match more than about seven shades of one hue back to a legend, so extra classes add noise rather than information.",[14,1926,1927,1930],{},[285,1928,1929],{},"Should I always normalize the field?","\nNormalize whenever the raw value is a count that scales with polygon size or population (totals, sums). Rates, percentages, medians, and densities are already comparable and can be mapped directly.",[14,1932,1933,1936,1938],{},[285,1934,1935],{},"Can I use this on a point or line layer?",[38,1937,40],{}," works on any geometry, but \"choropleth\" specifically means shaded polygons. On points it produces graduated-color markers; switch the graduated method to size for proportional symbols.",[14,1940,1941,1944],{},[285,1942,1943],{},"Why a sequential ramp and not a rainbow?","\nSequential ramps vary lightness monotonically so higher values look unambiguously \"more.\" Rainbow and qualitative ramps lack a perceptual order, so readers cannot tell which color means more.",[274,1946,1948],{"id":1947},"related","Related",[279,1950,1951,1957,1961,1965],{},[282,1952,1953,1956],{},[20,1954,1955],{"href":31},"Graduated & Categorized Renderers in PyQGIS"," — the parent workflow this recipe belongs to",[282,1958,1959],{},[20,1960,871],{"href":870},[282,1962,1963],{},[20,1964,698],{"href":697},[282,1966,1967],{},[20,1968,952],{"href":951},[1970,1971,1972],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":319,"searchDepth":342,"depth":342,"links":1974},[1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989],{"id":276,"depth":342,"text":277},{"id":308,"depth":342,"text":309},{"id":567,"depth":342,"text":568},{"id":702,"depth":342,"text":703},{"id":874,"depth":342,"text":875},{"id":956,"depth":342,"text":957},{"id":1074,"depth":342,"text":1075},{"id":1195,"depth":342,"text":1196},{"id":1502,"depth":342,"text":1503},{"id":1628,"depth":342,"text":1629},{"id":1741,"depth":342,"text":1742},{"id":1832,"depth":342,"text":1833},{"id":1904,"depth":342,"text":1905},{"id":1917,"depth":342,"text":1918},{"id":1947,"depth":342,"text":1948},"Build a choropleth map in PyQGIS with QgsGraduatedSymbolRenderer, a classification method, a sequential color ramp, area normalization, and a refreshed legend.","md",{"slug":1993,"type":1994,"breadcrumb":1995,"datePublished":1996,"dateModified":1997},"create-choropleth-map-pyqgis","article","Create a Choropleth Map","2025-02-18","2026-07-18","\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis",{"title":5,"description":1990},"pyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002Findex","R_mqP5MlVIJ0YsRkPUyioZgfBSAMkxgQMMey60-Oe2g",1787823360557]