[{"data":1,"prerenderedAt":1506},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis":3},{"id":4,"title":5,"body":6,"description":1499,"extension":1500,"meta":1501,"navigation":104,"path":1502,"seo":1503,"stem":1504,"__hash__":1505},"docs\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002Findex.md","Create a Choropleth Map in PyQGIS",{"type":7,"value":8,"toc":1484},"minimark",[9,13,28,31,36,64,68,71,293,326,330,346,353,432,462,466,469,614,634,638,648,688,708,712,723,799,826,830,833,917,948,952,955,1235,1256,1260,1267,1333,1347,1351,1365,1379,1388,1407,1419,1423,1432,1436,1442,1448,1456,1462,1466,1480],[10,11,5],"h1",{"id":12},"create-a-choropleth-map-in-pyqgis",[14,15,16,17,21,22,27],"p",{},"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 ",[18,19,20],"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 — as a focused task within the ",[23,24,26],"a",{"href":25},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated & Categorized Renderers in PyQGIS"," cluster.",[14,29,30],{},"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.",[32,33,35],"h2",{"id":34},"prerequisites","Prerequisites",[37,38,39,47,50,61],"ul",{},[40,41,42,46],"li",{},[43,44,45],"strong",{},"QGIS 3.34 LTR"," (Python 3.12) with PyQGIS available, or another 3.x release.",[40,48,49],{},"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.",[40,51,52,53,56,57,60],{},"The ",[43,54,55],{},"QGIS Python Console"," open (",[18,58,59],{},"Ctrl+Alt+P",").",[40,62,63],{},"A grasp of why raw counts mislead on choropleths — covered in step 2 below.",[32,65,67],{"id":66},"step-1-load-the-layer-and-inspect-the-field","Step 1: Load the Layer and Inspect the Field",[14,69,70],{},"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.",[72,73,78],"pre",{"className":74,"code":75,"language":76,"meta":77,"style":77},"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","",[18,79,80,99,106,132,143,148,155,166,189,219,260],{"__ignoreMap":77},[81,82,85,89,93,96],"span",{"class":83,"line":84},"line",1,[81,86,88],{"class":87},"snl16","from",[81,90,92],{"class":91},"s95oV"," qgis.core ",[81,94,95],{"class":87},"import",[81,97,98],{"class":91}," QgsProject\n",[81,100,102],{"class":83,"line":101},2,[81,103,105],{"emptyLinePlaceholder":104},true,"\n",[81,107,109,112,115,118,122,125,129],{"class":83,"line":108},3,[81,110,111],{"class":91},"layer ",[81,113,114],{"class":87},"=",[81,116,117],{"class":91}," QgsProject.instance().mapLayersByName(",[81,119,121],{"class":120},"sU2Wk","\"counties\"",[81,123,124],{"class":91},")[",[81,126,128],{"class":127},"sDLfK","0",[81,130,131],{"class":91},"]\n",[81,133,135,138,140],{"class":83,"line":134},4,[81,136,137],{"class":91},"field_name ",[81,139,114],{"class":87},[81,141,142],{"class":120}," \"population\"\n",[81,144,146],{"class":83,"line":145},5,[81,147,105],{"emptyLinePlaceholder":104},[81,149,151],{"class":83,"line":150},6,[81,152,154],{"class":153},"sAwPA","# Sanity-check the field: type, range, and null count.\n",[81,156,158,161,163],{"class":83,"line":157},7,[81,159,160],{"class":91},"idx ",[81,162,114],{"class":87},[81,164,165],{"class":91}," layer.fields().indexFromName(field_name)\n",[81,167,169,172,174,177,180,183,186],{"class":83,"line":168},8,[81,170,171],{"class":91},"values ",[81,173,114],{"class":87},[81,175,176],{"class":91}," [f[field_name] ",[81,178,179],{"class":87},"for",[81,181,182],{"class":91}," f ",[81,184,185],{"class":87},"in",[81,187,188],{"class":91}," layer.getFeatures()\n",[81,190,192,195,198,201,204,207,210,213,216],{"class":83,"line":191},9,[81,193,194],{"class":87},"          if",[81,196,197],{"class":91}," f[field_name] ",[81,199,200],{"class":87},"not",[81,202,203],{"class":87}," in",[81,205,206],{"class":91}," (",[81,208,209],{"class":127},"None",[81,211,212],{"class":91},", ",[81,214,215],{"class":120},"\"\"",[81,217,218],{"class":91},")]\n",[81,220,222,225,228,231,234,237,240,243,246,249,252,254,257],{"class":83,"line":221},10,[81,223,224],{"class":127},"print",[81,226,227],{"class":91},"(",[81,229,230],{"class":87},"f",[81,232,233],{"class":120},"\"Features: ",[81,235,236],{"class":127},"{",[81,238,239],{"class":91},"layer.featureCount()",[81,241,242],{"class":127},"}",[81,244,245],{"class":120},", valid values: ",[81,247,248],{"class":127},"{len",[81,250,251],{"class":91},"(values)",[81,253,242],{"class":127},[81,255,256],{"class":120},"\"",[81,258,259],{"class":91},")\n",[81,261,263,265,267,269,272,275,277,279,282,285,287,289,291],{"class":83,"line":262},11,[81,264,224],{"class":127},[81,266,227],{"class":91},[81,268,230],{"class":87},[81,270,271],{"class":120},"\"Min: ",[81,273,274],{"class":127},"{min",[81,276,251],{"class":91},[81,278,242],{"class":127},[81,280,281],{"class":120},", Max: ",[81,283,284],{"class":127},"{max",[81,286,251],{"class":91},[81,288,242],{"class":127},[81,290,256],{"class":120},[81,292,259],{"class":91},[14,294,295,298,299,302,303,305,306,309,310,313,314,317,318,321,322,325],{},[43,296,297],{},"Breakdown:"," Iterating once up front catches problems before styling. ",[18,300,301],{},"f[field_name]"," reads the attribute by name; filtering out ",[18,304,209],{}," and empty strings avoids ",[18,307,308],{},"TypeError"," when you compute ",[18,311,312],{},"min","\u002F",[18,315,316],{},"max",". If ",[18,319,320],{},"len(values)"," is far below ",[18,323,324],{},"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.",[32,327,329],{"id":328},"step-2-decide-whether-to-normalize","Step 2: Decide Whether to Normalize",[14,331,332,333,337,338,341,342,345],{},"This is the step that separates a correct choropleth from a misleading one. Mapping a raw ",[334,335,336],"em",{},"count"," — total population, total sales — mostly reproduces the size of each polygon: big counties look \"high\" simply because they are big. Normalize to a ",[334,339,340],{},"rate"," or ",[334,343,344],{},"density"," by dividing by area or by another count.",[14,347,348,349,352],{},"If your layer lacks a ready-made density field, compute one with a virtual field using ",[18,350,351],{},"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:",[72,354,356],{"className":74,"code":355,"language":76,"meta":77,"style":77},"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",[18,357,358,369,381,385,390,406,411,419,423],{"__ignoreMap":77},[81,359,360,362,364,366],{"class":83,"line":84},[81,361,88],{"class":87},[81,363,92],{"class":91},[81,365,95],{"class":87},[81,367,368],{"class":91}," QgsField\n",[81,370,371,373,376,378],{"class":83,"line":101},[81,372,88],{"class":87},[81,374,375],{"class":91}," qgis.PyQt.QtCore ",[81,377,95],{"class":87},[81,379,380],{"class":91}," QVariant\n",[81,382,383],{"class":83,"line":108},[81,384,105],{"emptyLinePlaceholder":104},[81,386,387],{"class":83,"line":134},[81,388,389],{"class":153},"# Add a virtual field: population per square kilometre.\n",[81,391,392,395,397,400,403],{"class":83,"line":145},[81,393,394],{"class":91},"density_field ",[81,396,114],{"class":87},[81,398,399],{"class":91}," QgsField(",[81,401,402],{"class":120},"\"pop_density\"",[81,404,405],{"class":91},", QVariant.Double)\n",[81,407,408],{"class":83,"line":150},[81,409,410],{"class":91},"layer.addExpressionField(\n",[81,412,413,416],{"class":83,"line":157},[81,414,415],{"class":120},"    \"population \u002F ($area \u002F 1000000)\"",[81,417,418],{"class":91},", density_field\n",[81,420,421],{"class":83,"line":168},[81,422,259],{"class":91},[81,424,425,427,429],{"class":83,"line":191},[81,426,137],{"class":91},[81,428,114],{"class":87},[81,430,431],{"class":120}," \"pop_density\"\n",[14,433,434,436,437,440,441,444,445,448,449,452,453,456,457,461],{},[43,435,297],{}," ",[18,438,439],{},"addExpressionField()"," registers a ",[334,442,443],{},"virtual"," field computed on the fly — it is not written to disk, so it is perfect for a transient mapping field. ",[18,446,447],{},"$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 ",[18,450,451],{},"area($geometry)"," with an ",[18,454,455],{},"transform()"," wrapper. To shade by an existing per-feature attribute color instead of a class, see ",[23,458,460],{"href":459},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","Set a Vector Layer Symbol Color in PyQGIS",".",[32,463,465],{"id":464},"step-3-choose-a-classification-method-and-class-count","Step 3: Choose a Classification Method and Class Count",[14,467,468],{},"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.",[72,470,472],{"className":74,"code":471,"language":76,"meta":77,"style":77},"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",[18,473,474,485,490,495,500,504,508,518,522,532,537,542,548,553,566],{"__ignoreMap":77},[81,475,476,478,480,482],{"class":83,"line":84},[81,477,88],{"class":87},[81,479,92],{"class":91},[81,481,95],{"class":87},[81,483,484],{"class":91}," (\n",[81,486,487],{"class":83,"line":101},[81,488,489],{"class":91},"    QgsGraduatedSymbolRenderer,\n",[81,491,492],{"class":83,"line":108},[81,493,494],{"class":91},"    QgsClassificationQuantile,\n",[81,496,497],{"class":83,"line":134},[81,498,499],{"class":91},"    QgsSymbol,\n",[81,501,502],{"class":83,"line":145},[81,503,259],{"class":91},[81,505,506],{"class":83,"line":150},[81,507,105],{"emptyLinePlaceholder":104},[81,509,510,513,515],{"class":83,"line":157},[81,511,512],{"class":91},"class_count ",[81,514,114],{"class":87},[81,516,517],{"class":127}," 5\n",[81,519,520],{"class":83,"line":168},[81,521,105],{"emptyLinePlaceholder":104},[81,523,524,527,529],{"class":83,"line":191},[81,525,526],{"class":91},"renderer ",[81,528,114],{"class":87},[81,530,531],{"class":91}," QgsGraduatedSymbolRenderer(field_name)\n",[81,533,534],{"class":83,"line":221},[81,535,536],{"class":91},"renderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\n",[81,538,539],{"class":83,"line":262},[81,540,541],{"class":91},"renderer.setClassificationMethod(QgsClassificationQuantile())\n",[81,543,545],{"class":83,"line":544},12,[81,546,547],{"class":91},"renderer.updateClasses(layer, class_count)\n",[81,549,551],{"class":83,"line":550},13,[81,552,105],{"emptyLinePlaceholder":104},[81,554,556,558,561,563],{"class":83,"line":555},14,[81,557,179],{"class":87},[81,559,560],{"class":91}," r ",[81,562,185],{"class":87},[81,564,565],{"class":91}," renderer.ranges():\n",[81,567,569,572,574,576,578,580,583,586,588,591,593,596,598,600,603,605,608,610,612],{"class":83,"line":568},15,[81,570,571],{"class":127},"    print",[81,573,227],{"class":91},[81,575,230],{"class":87},[81,577,256],{"class":120},[81,579,236],{"class":127},[81,581,582],{"class":91},"r.lowerValue()",[81,584,585],{"class":87},":.1f",[81,587,242],{"class":127},[81,589,590],{"class":120}," – ",[81,592,236],{"class":127},[81,594,595],{"class":91},"r.upperValue()",[81,597,585],{"class":87},[81,599,242],{"class":127},[81,601,602],{"class":120},": ",[81,604,236],{"class":127},[81,606,607],{"class":91},"r.label()",[81,609,242],{"class":127},[81,611,256],{"class":120},[81,613,259],{"class":91},[14,615,616,436,618,621,622,625,626,629,630,461],{},[43,617,297],{},[18,619,620],{},"QgsClassificationQuantile"," puts an equal number of features in each class, which balances how much map ink each shade gets — a sensible default for choropleths. ",[18,623,624],{},"updateClasses(layer, class_count)"," scans the field and computes the five break ranges. Printing ",[18,627,628],{},"ranges()"," confirms the breaks are sensible before you commit. For clustered data where quantile splits look arbitrary, switch to Jenks — see ",[23,631,633],{"href":632},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002F","Classify a Layer with Natural Breaks (Jenks) in PyQGIS",[32,635,637],{"id":636},"step-4-apply-a-sequential-color-ramp","Step 4: Apply a Sequential Color Ramp",[14,639,640,641,644,645,461],{},"Choropleths represent ordered magnitude, so they need a ",[334,642,643],{},"sequential"," ramp where lightness increases with value. Pull one from the user's style library via ",[18,646,647],{},"QgsStyle.defaultStyle()",[72,649,651],{"className":74,"code":650,"language":76,"meta":77,"style":77},"from qgis.core import QgsStyle\n\nramp = QgsStyle.defaultStyle().colorRamp(\"Blues\")\nrenderer.updateColorRamp(ramp)\n",[18,652,653,664,668,683],{"__ignoreMap":77},[81,654,655,657,659,661],{"class":83,"line":84},[81,656,88],{"class":87},[81,658,92],{"class":91},[81,660,95],{"class":87},[81,662,663],{"class":91}," QgsStyle\n",[81,665,666],{"class":83,"line":101},[81,667,105],{"emptyLinePlaceholder":104},[81,669,670,673,675,678,681],{"class":83,"line":108},[81,671,672],{"class":91},"ramp ",[81,674,114],{"class":87},[81,676,677],{"class":91}," QgsStyle.defaultStyle().colorRamp(",[81,679,680],{"class":120},"\"Blues\"",[81,682,259],{"class":91},[81,684,685],{"class":83,"line":134},[81,686,687],{"class":91},"renderer.updateColorRamp(ramp)\n",[14,689,690,436,692,695,696,699,700,703,704,707],{},[43,691,297],{},[18,693,694],{},"colorRamp(\"Blues\")"," returns a built-in sequential ramp. ",[18,697,698],{},"updateColorRamp()"," recolors all five classes in light-to-dark order, so the darkest blue marks the highest-density regions. Avoid diverging ramps (",[18,701,702],{},"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 ",[18,705,706],{},"ramp.invert()"," before passing it.",[32,709,711],{"id":710},"step-4b-format-the-legend-labels","Step 4b: Format the Legend Labels",[14,713,714,715,718,719,722],{},"The default range labels read like ",[18,716,717],{},"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. ",[18,720,721],{},"setLabelFormat()"," controls the label template for every class at once.",[72,724,726],{"className":74,"code":725,"language":76,"meta":77,"style":77},"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",[18,727,728,739,743,762,778,782,792],{"__ignoreMap":77},[81,729,730,732,734,736],{"class":83,"line":84},[81,731,88],{"class":87},[81,733,92],{"class":91},[81,735,95],{"class":87},[81,737,738],{"class":91}," QgsRendererRangeLabelFormat\n",[81,740,741],{"class":83,"line":101},[81,742,105],{"emptyLinePlaceholder":104},[81,744,745,748,750,753,756,758,760],{"class":83,"line":108},[81,746,747],{"class":91},"label_format ",[81,749,114],{"class":87},[81,751,752],{"class":91}," QgsRendererRangeLabelFormat(",[81,754,755],{"class":120},"\"%1 – %2 \u002Fkm²\"",[81,757,212],{"class":91},[81,759,128],{"class":127},[81,761,259],{"class":91},[81,763,764,767,771,773,776],{"class":83,"line":134},[81,765,766],{"class":91},"renderer.setLabelFormat(label_format, ",[81,768,770],{"class":769},"s9osk","updateRanges",[81,772,114],{"class":87},[81,774,775],{"class":127},"True",[81,777,259],{"class":91},[81,779,780],{"class":83,"line":145},[81,781,105],{"emptyLinePlaceholder":104},[81,783,784,786,788,790],{"class":83,"line":150},[81,785,179],{"class":87},[81,787,560],{"class":91},[81,789,185],{"class":87},[81,791,565],{"class":91},[81,793,794,796],{"class":83,"line":157},[81,795,571],{"class":127},[81,797,798],{"class":91},"(r.label())\n",[14,800,801,436,803,806,807,810,811,814,815,817,818,821,822,825],{},[43,802,297],{},[18,804,805],{},"QgsRendererRangeLabelFormat(template, precision)"," sets a label template where ",[18,808,809],{},"%1"," and ",[18,812,813],{},"%2"," are substituted with each class's lower and upper bounds, and the second argument fixes the decimal precision — ",[18,816,128],{}," rounds to whole numbers. Passing ",[18,819,820],{},"updateRanges=True"," rewrites the labels on the existing ranges in place. The result is a legend that reads ",[18,823,824],{},"0 – 85 \u002Fkm²"," instead of an over-precise machine string, which is what a finished map needs.",[32,827,829],{"id":828},"step-5-set-the-renderer-and-refresh-legend-and-canvas","Step 5: Set the Renderer and Refresh Legend and Canvas",[14,831,832],{},"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.",[72,834,836],{"className":74,"code":835,"language":76,"meta":77,"style":77},"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",[18,837,838,850,854,859,864,868,888,893,898,902,907],{"__ignoreMap":77},[81,839,840,842,845,847],{"class":83,"line":84},[81,841,88],{"class":87},[81,843,844],{"class":91}," qgis.utils ",[81,846,95],{"class":87},[81,848,849],{"class":91}," iface\n",[81,851,852],{"class":83,"line":101},[81,853,105],{"emptyLinePlaceholder":104},[81,855,856],{"class":83,"line":108},[81,857,858],{"class":91},"layer.setRenderer(renderer)\n",[81,860,861],{"class":83,"line":134},[81,862,863],{"class":91},"layer.triggerRepaint()\n",[81,865,866],{"class":83,"line":145},[81,867,105],{"emptyLinePlaceholder":104},[81,869,870,873,876,879,882,885],{"class":83,"line":150},[81,871,872],{"class":87},"if",[81,874,875],{"class":91}," iface ",[81,877,878],{"class":87},"is",[81,880,881],{"class":87}," not",[81,883,884],{"class":127}," None",[81,886,887],{"class":91},":\n",[81,889,890],{"class":83,"line":157},[81,891,892],{"class":91},"    iface.layerTreeView().refreshLayerSymbology(layer.id())\n",[81,894,895],{"class":83,"line":168},[81,896,897],{"class":91},"    iface.mapCanvas().refresh()\n",[81,899,900],{"class":83,"line":191},[81,901,105],{"emptyLinePlaceholder":104},[81,903,904],{"class":83,"line":221},[81,905,906],{"class":153},"# Optional: persist the styling for reuse.\n",[81,908,909,912,915],{"class":83,"line":262},[81,910,911],{"class":91},"layer.saveNamedStyle(",[81,913,914],{"class":120},"\"\u002Ftmp\u002Fcounties_choropleth.qml\"",[81,916,259],{"class":91},[14,918,919,436,921,924,925,928,929,932,933,936,937,940,941,944,945,461],{},[43,920,297],{},[18,922,923],{},"setRenderer()"," swaps in the graduated renderer; ",[18,926,927],{},"triggerRepaint()"," invalidates the cached image. ",[18,930,931],{},"refreshLayerSymbology()"," rebuilds the legend entries that show each class's color and range. The ",[18,934,935],{},"iface"," guard keeps the snippet runnable in standalone scripts. ",[18,938,939],{},"saveNamedStyle()"," writes a ",[18,942,943],{},".qml"," you can reload on other projects with ",[18,946,947],{},"layer.loadNamedStyle()",[32,949,951],{"id":950},"step-6-wrap-it-as-a-reusable-function","Step 6: Wrap It as a Reusable Function",[14,953,954],{},"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.",[72,956,958],{"className":74,"code":957,"language":76,"meta":77,"style":77},"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",[18,959,960,970,974,978,982,987,991,1001,1005,1009,1036,1041,1051,1056,1061,1066,1071,1082,1097,1125,1131,1136,1142,1148,1163,1169,1178,1183,1188,1194,1207],{"__ignoreMap":77},[81,961,962,964,966,968],{"class":83,"line":84},[81,963,88],{"class":87},[81,965,92],{"class":91},[81,967,95],{"class":87},[81,969,484],{"class":91},[81,971,972],{"class":83,"line":101},[81,973,489],{"class":91},[81,975,976],{"class":83,"line":108},[81,977,494],{"class":91},[81,979,980],{"class":83,"line":134},[81,981,499],{"class":91},[81,983,984],{"class":83,"line":145},[81,985,986],{"class":91},"    QgsStyle,\n",[81,988,989],{"class":83,"line":150},[81,990,259],{"class":91},[81,992,993,995,997,999],{"class":83,"line":157},[81,994,88],{"class":87},[81,996,844],{"class":91},[81,998,95],{"class":87},[81,1000,849],{"class":91},[81,1002,1003],{"class":83,"line":168},[81,1004,105],{"emptyLinePlaceholder":104},[81,1006,1007],{"class":83,"line":191},[81,1008,105],{"emptyLinePlaceholder":104},[81,1010,1011,1014,1018,1021,1023,1026,1029,1031,1033],{"class":83,"line":221},[81,1012,1013],{"class":87},"def",[81,1015,1017],{"class":1016},"svObZ"," apply_choropleth",[81,1019,1020],{"class":91},"(layer, field, classes",[81,1022,114],{"class":87},[81,1024,1025],{"class":127},"5",[81,1027,1028],{"class":91},", ramp_name",[81,1030,114],{"class":87},[81,1032,680],{"class":120},[81,1034,1035],{"class":91},"):\n",[81,1037,1038],{"class":83,"line":262},[81,1039,1040],{"class":120},"    \"\"\"Style a polygon layer as a quantile choropleth.\"\"\"\n",[81,1042,1043,1046,1048],{"class":83,"line":544},[81,1044,1045],{"class":91},"    renderer ",[81,1047,114],{"class":87},[81,1049,1050],{"class":91}," QgsGraduatedSymbolRenderer(field)\n",[81,1052,1053],{"class":83,"line":550},[81,1054,1055],{"class":91},"    renderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\n",[81,1057,1058],{"class":83,"line":555},[81,1059,1060],{"class":91},"    renderer.setClassificationMethod(QgsClassificationQuantile())\n",[81,1062,1063],{"class":83,"line":568},[81,1064,1065],{"class":91},"    renderer.updateClasses(layer, classes)\n",[81,1067,1069],{"class":83,"line":1068},16,[81,1070,105],{"emptyLinePlaceholder":104},[81,1072,1074,1077,1079],{"class":83,"line":1073},17,[81,1075,1076],{"class":91},"    ramp ",[81,1078,114],{"class":87},[81,1080,1081],{"class":91}," QgsStyle.defaultStyle().colorRamp(ramp_name)\n",[81,1083,1085,1088,1091,1093,1095],{"class":83,"line":1084},18,[81,1086,1087],{"class":87},"    if",[81,1089,1090],{"class":91}," ramp ",[81,1092,878],{"class":87},[81,1094,884],{"class":127},[81,1096,887],{"class":91},[81,1098,1100,1103,1106,1108,1110,1113,1115,1118,1120,1123],{"class":83,"line":1099},19,[81,1101,1102],{"class":87},"        raise",[81,1104,1105],{"class":127}," ValueError",[81,1107,227],{"class":91},[81,1109,230],{"class":87},[81,1111,1112],{"class":120},"\"Color ramp '",[81,1114,236],{"class":127},[81,1116,1117],{"class":91},"ramp_name",[81,1119,242],{"class":127},[81,1121,1122],{"class":120},"' is not installed\"",[81,1124,259],{"class":91},[81,1126,1128],{"class":83,"line":1127},20,[81,1129,1130],{"class":91},"    renderer.updateColorRamp(ramp)\n",[81,1132,1134],{"class":83,"line":1133},21,[81,1135,105],{"emptyLinePlaceholder":104},[81,1137,1139],{"class":83,"line":1138},22,[81,1140,1141],{"class":91},"    layer.setRenderer(renderer)\n",[81,1143,1145],{"class":83,"line":1144},23,[81,1146,1147],{"class":91},"    layer.triggerRepaint()\n",[81,1149,1151,1153,1155,1157,1159,1161],{"class":83,"line":1150},24,[81,1152,1087],{"class":87},[81,1154,875],{"class":91},[81,1156,878],{"class":87},[81,1158,881],{"class":87},[81,1160,884],{"class":127},[81,1162,887],{"class":91},[81,1164,1166],{"class":83,"line":1165},25,[81,1167,1168],{"class":91},"        iface.layerTreeView().refreshLayerSymbology(layer.id())\n",[81,1170,1172,1175],{"class":83,"line":1171},26,[81,1173,1174],{"class":87},"    return",[81,1176,1177],{"class":91}," layer\n",[81,1179,1181],{"class":83,"line":1180},27,[81,1182,105],{"emptyLinePlaceholder":104},[81,1184,1186],{"class":83,"line":1185},28,[81,1187,105],{"emptyLinePlaceholder":104},[81,1189,1191],{"class":83,"line":1190},29,[81,1192,1193],{"class":153},"# Apply to every selected layer in the panel.\n",[81,1195,1197,1199,1202,1204],{"class":83,"line":1196},30,[81,1198,179],{"class":87},[81,1200,1201],{"class":91}," lyr ",[81,1203,185],{"class":87},[81,1205,1206],{"class":91}," iface.layerTreeView().selectedLayers():\n",[81,1208,1210,1213,1215,1217,1220,1222,1224,1226,1228,1230,1233],{"class":83,"line":1209},31,[81,1211,1212],{"class":91},"    apply_choropleth(lyr, ",[81,1214,402],{"class":120},[81,1216,212],{"class":91},[81,1218,1219],{"class":769},"classes",[81,1221,114],{"class":87},[81,1223,1025],{"class":127},[81,1225,212],{"class":91},[81,1227,1117],{"class":769},[81,1229,114],{"class":87},[81,1231,1232],{"class":120},"\"YlGnBu\"",[81,1234,259],{"class":91},[14,1236,1237,1239,1240,1243,1244,1246,1247,1250,1251,341,1253,1255],{},[43,1238,297],{}," Folding the steps into ",[18,1241,1242],{},"apply_choropleth()"," makes the recipe a single composable unit; the ",[18,1245,209],{}," check on the ramp turns a silent failure into a clear error. Looping over ",[18,1248,1249],{},"selectedLayers()"," styles a whole set of layers identically — the exact reproducibility win that justifies scripting over the dialog. Pass a different ",[18,1252,1117],{},[18,1254,1219],{}," per call when layers need to differ.",[32,1257,1259],{"id":1258},"qgis-version-compatibility","QGIS Version Compatibility",[14,1261,1262,1263,1266],{},"The recipe targets ",[43,1264,1265],{},"QGIS 3.34 LTR (Python 3.12)"," and uses only stable APIs.",[1268,1269,1270,1283],"table",{},[1271,1272,1273],"thead",{},[1274,1275,1276,1280],"tr",{},[1277,1278,1279],"th",{},"QGIS \u002F Python",[1277,1281,1282],{},"Notes",[1284,1285,1286,1297,1310,1318],"tbody",{},[1274,1287,1288,1294],{},[1289,1290,1291],"td",{},[43,1292,1293],{},"3.34 LTR (Python 3.12)",[1289,1295,1296],{},"Baseline. Every snippet runs as written.",[1274,1298,1299,1302],{},[1289,1300,1301],{},"3.28 LTR (Python 3.9)",[1289,1303,1304,1305,810,1308,461],{},"Fully supported, including ",[18,1306,1307],{},"addExpressionField",[18,1309,620],{},[1274,1311,1312,1315],{},[1289,1313,1314],{},"3.40 \u002F 3.44",[1289,1316,1317],{},"Identical API; more bundled sequential ramps available by name.",[1274,1319,1320,1323],{},[1289,1321,1322],{},"Pre-3.10",[1289,1324,1325,1328,1329,1332],{},[18,1326,1327],{},"setClassificationMethod"," unavailable; older code used the deprecated ",[18,1330,1331],{},"createRenderer(layer, field, classes, mode, symbol, ramp)"," constructor.",[14,1334,1335,1336,1339,1340,1343,1344,1346],{},"On Python 3.9 builds, ",[18,1337,1338],{},"QVariant.Double"," still works; on newer PyQt6-based builds the same enum is reachable as ",[18,1341,1342],{},"QVariant.Type.Double",", but ",[18,1345,1338],{}," remains aliased for compatibility.",[32,1348,1350],{"id":1349},"troubleshooting","Troubleshooting",[14,1352,1353,1356,1357,1360,1361,1364],{},[43,1354,1355],{},"Map is one solid color."," You likely set the renderer without calling ",[18,1358,1359],{},"updateClasses()",", so ",[18,1362,1363],{},"renderer.ranges()"," is empty. Print it to confirm, then re-run the classification step.",[14,1366,1367,1370,1371,1374,1375,1378],{},[43,1368,1369],{},"Many polygons render grey\u002Funclassified."," Those features hold ",[18,1372,1373],{},"NULL"," or out-of-range values. Either filter them out, or call ",[18,1376,1377],{},"renderer.setClassAttribute()"," on a cleaned field. Confirm the field is numeric — string fields silently produce empty classes.",[14,1380,1381,1384,1385,1387],{},[43,1382,1383],{},"Big regions dominate the map."," You mapped a raw count instead of a density or rate. Return to step 2 and divide by ",[18,1386,447],{}," or by population.",[14,1389,1390,436,1396,1399,1400,1402,1403,1406],{},[43,1391,1392,1395],{},[18,1393,1394],{},"updateColorRamp"," raises AttributeError.",[18,1397,1398],{},"colorRamp(\"Name\")"," returned ",[18,1401,209],{}," because that ramp name is not installed. Run ",[18,1404,1405],{},"QgsStyle.defaultStyle().colorRampNames()"," and pick an existing name.",[14,1408,1409,1412,1413,1415,1416,461],{},[43,1410,1411],{},"Legend swatches do not update."," Calling ",[18,1414,927],{}," alone refreshes the canvas but not the tree. Add ",[18,1417,1418],{},"iface.layerTreeView().refreshLayerSymbology(layer.id())",[32,1420,1422],{"id":1421},"conclusion","Conclusion",[14,1424,1425,1426,1428,1429,1431],{},"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 ",[18,1427,20],{}," you can encode all three in a dozen lines, reproduce them across projects, and save the result as a reusable ",[18,1430,943],{},". From here, experiment with Jenks breaks for clustered data and compare how each method reshapes the same map.",[32,1433,1435],{"id":1434},"frequently-asked-questions","Frequently Asked Questions",[14,1437,1438,1441],{},[43,1439,1440],{},"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,1443,1444,1447],{},[43,1445,1446],{},"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,1449,1450,1453,1455],{},[43,1451,1452],{},"Can I use this on a point or line layer?",[18,1454,20],{}," 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,1457,1458,1461],{},[43,1459,1460],{},"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.",[32,1463,1465],{"id":1464},"related","Related",[37,1467,1468,1472,1476],{},[40,1469,1470],{},[23,1471,26],{"href":25},[40,1473,1474],{},[23,1475,633],{"href":632},[40,1477,1478],{},[23,1479,460],{"href":459},[1481,1482,1483],"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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":77,"searchDepth":101,"depth":101,"links":1485},[1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498],{"id":34,"depth":101,"text":35},{"id":66,"depth":101,"text":67},{"id":328,"depth":101,"text":329},{"id":464,"depth":101,"text":465},{"id":636,"depth":101,"text":637},{"id":710,"depth":101,"text":711},{"id":828,"depth":101,"text":829},{"id":950,"depth":101,"text":951},{"id":1258,"depth":101,"text":1259},{"id":1349,"depth":101,"text":1350},{"id":1421,"depth":101,"text":1422},{"id":1434,"depth":101,"text":1435},{"id":1464,"depth":101,"text":1465},"Build a choropleth map in PyQGIS with QgsGraduatedSymbolRenderer, a classification method, a sequential color ramp, area normalization, and a refreshed legend.","md",{},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis",{"title":5,"description":1499},"pyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002Findex","oMwCi9sHqFe3hcmRl9CtFvwnn00t16shO5oxI1R-0Pk",1781792483472]