[{"data":1,"prerenderedAt":2591},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations":3},{"id":4,"title":5,"body":6,"description":2584,"extension":2585,"meta":2586,"navigation":326,"path":2587,"seo":2588,"stem":2589,"__hash__":2590},"docs\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Findex.md","Labeling & Annotations in PyQGIS",{"type":7,"value":8,"toc":2564},"minimark",[9,13,32,44,52,57,86,90,105,248,252,257,458,479,483,503,644,668,672,681,790,815,819,828,994,1032,1036,1081,1220,1244,1248,1262,1410,1428,1433,1447,1548,1566,1570,1573,1667,1682,1686,1700,1866,1884,1888,1902,2048,2068,2072,2083,2259,2280,2284,2374,2384,2388,2438,2442,2460,2466,2479,2501,2512,2525,2538,2542,2560],[10,11,5],"h1",{"id":12},"labeling-annotations-in-pyqgis",[14,15,16,17,21,22,25,26,31],"p",{},"Text is what turns a styled map into a communicative one. A choropleth or a road network may render beautifully, but until you attach names, values, and explanatory notes, readers cannot interpret it. PyQGIS exposes the full labeling engine — the same PAL (Placement of Automated Labels) engine the GUI drives — through ",[18,19,20],"code",{},"QgsPalLayerSettings",", ",[18,23,24],{},"QgsTextFormat",", and the labeling wrappers, plus a separate annotation system for fixed map furniture. This guide sits inside ",[27,28,30],"a",{"href":29},"\u002Fpyqgis-cartography-visualization\u002F","PyQGIS Cartography & Data Visualization"," and shows you how to script labels and annotations that are reproducible, version-controlled, and consistent across every map you produce.",[14,33,34,35,39,40,43],{},"Labeling differs from styling in an important way. Where the styling pipeline answers ",[36,37,38],"em",{},"how a feature looks",", labeling answers ",[36,41,42],{},"what text appears, where it sits, and which features win when space runs out",". That last part — conflict resolution between competing labels — is why the engine has so many knobs. Master the core classes here and you will rarely touch the layer properties dialog again.",[14,45,46,47,21,49,51],{},"Scripting labels pays off most when you produce many maps. A hand-configured label style in the GUI lives in one project file and is tedious to replicate. The same configuration expressed in Python is a function you call against any layer, version with git, and apply identically across an atlas of a hundred sheets. The classes you meet below — ",[18,48,24],{},[18,50,20],{},", the labeling wrappers, and the annotation items — are the complete vocabulary you need to make that happen, and they compose the same way whether you are labeling one point or an entire road network.",[53,54,56],"h2",{"id":55},"prerequisites","Prerequisites",[58,59,60,68,71,78],"ul",{},[61,62,63,67],"li",{},[64,65,66],"strong",{},"QGIS 3.34 LTR"," (bundles Python 3.12). Examples note where the 3.28 LTR (Python 3.9) and 3.40\u002F3.44 lines diverge.",[61,69,70],{},"A loaded vector layer with attributes worth labeling — a string field for names and ideally a numeric field for filtering by importance.",[61,72,73,74,77],{},"Comfort running code in the QGIS Python Console, or a standalone script that has called ",[18,75,76],{},"QgsApplication.initQgis()",".",[61,79,80,81,85],{},"Familiarity with ",[27,82,84],{"href":83},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002F","Programmatic Layer Styling",", since labels share the unit and color conventions used by symbols.",[53,87,89],{"id":88},"how-pyqgis-labeling-fits-together","How PyQGIS Labeling Fits Together",[14,91,92,93,96,97,100,101,104],{},"Before writing code, it helps to see the assembly. Labeling is a small object graph: a ",[36,94,95],{},"text format"," describes appearance, a ",[36,98,99],{},"layer settings"," object wires that format to a field or expression and decides placement, and a ",[36,102,103],{},"labeling wrapper"," attaches the settings to the layer.",[106,107,112,113,112,117,112,121,112,128,112,138,112,145,112,151,112,155,112,158,112,162,112,166,112,171,112,175,112,180,112,183,112,199,112,205,112,210,112,216,112,220,112,227,112,232,112,236,112,243],"svg",{"viewBox":108,"role":109,"ariaLabel":110,"xmlns":111},"0 0 760 380","img","Diagram of the PyQGIS labeling object graph and label anatomy","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[114,115,116],"title",{},"PyQGIS labeling object graph and label anatomy",[118,119,120],"desc",{},"Left side shows QgsTextFormat feeding QgsPalLayerSettings, wrapped by QgsVectorLayerSimpleLabeling and attached to a layer. Right side shows a point feature with a buffered label and a leader offset.",[122,123],"rect",{"x":124,"y":124,"width":125,"height":126,"fill":127},"0","760","380","#f6f3ea",[122,129],{"x":130,"y":131,"width":132,"height":133,"rx":134,"fill":135,"stroke":136,"style":137},"30","40","200","64","8","#fffdf7","#0f766e","stroke-width:2.5",[139,140,24],"text",{"x":141,"y":142,"fill":143,"style":144},"130","68","#17211d","text-anchor:middle;font-family:sans-serif;font-size:15px;font-weight:bold",[139,146,150],{"x":141,"y":147,"fill":148,"style":149},"88","#2f3b35","text-anchor:middle;font-family:sans-serif;font-size:12px","font, size, color, buffer",[122,152],{"x":130,"y":153,"width":132,"height":133,"rx":134,"fill":135,"stroke":154,"style":137},"150","#2563eb",[139,156,20],{"x":141,"y":157,"fill":143,"style":144},"178",[139,159,161],{"x":141,"y":160,"fill":148,"style":149},"198","fieldName, placement",[122,163],{"x":130,"y":164,"width":132,"height":133,"rx":134,"fill":135,"stroke":165,"style":137},"260","#b45309",[139,167,170],{"x":141,"y":168,"fill":143,"style":169},"288","text-anchor:middle;font-family:sans-serif;font-size:14px;font-weight:bold","SimpleLabeling",[139,172,174],{"x":141,"y":173,"fill":148,"style":149},"308","attached to layer",[176,177],"line",{"x1":141,"y1":178,"x2":141,"y2":153,"stroke":143,"style":179},"104","stroke-width:2.5;marker-end:url(#ar)",[176,181],{"x1":141,"y1":182,"x2":141,"y2":164,"stroke":143,"style":179},"214",[184,185,186,187,112],"defs",{},"\n    ",[188,189,195],"marker",{"id":190,"markerWidth":191,"markerHeight":191,"refX":192,"refY":193,"orient":194},"ar","9","6","3","auto",[196,197],"path",{"d":198,"fill":143},"M0,0 L6,3 L0,6 Z",[122,200],{"x":201,"y":131,"width":202,"height":203,"rx":134,"fill":135,"stroke":148,"style":204},"330","400","284","stroke-width:2",[139,206,209],{"x":207,"y":208,"fill":143,"style":169},"530","66","Label anatomy",[211,212],"circle",{"cx":213,"cy":214,"r":192,"fill":215},"430","230","#15803d",[139,217,219],{"x":213,"y":218,"fill":148,"style":149},"262","feature point",[176,221],{"x1":222,"y1":223,"x2":224,"y2":225,"stroke":165,"style":226},"436","226","540","160","stroke-width:2;stroke-dasharray:5 4",[139,228,231],{"x":229,"y":153,"fill":165,"style":230},"500","font-family:sans-serif;font-size:11px","offset \u002F leader",[122,233],{"x":224,"y":141,"width":153,"height":131,"rx":234,"fill":235},"4","#26322d",[139,237,242],{"x":238,"y":239,"fill":240,"style":241},"615","156","#d9f99d","text-anchor:middle;font-family:monospace;font-size:16px","Riverside",[139,244,247],{"x":238,"y":245,"fill":148,"style":246},"196","text-anchor:middle;font-family:sans-serif;font-size:11px","text + buffer halo",[53,249,251],{"id":250},"building-a-text-format","Building a Text Format",[14,253,254,256],{},[18,255,24],{}," is the appearance container shared by labels, annotations, and several other text-bearing components. Configure it once and reuse it. The format owns the font, its size and unit, the color, and nested settings objects for the buffer (halo), background, shadow, and drop-shadow.",[258,259,264],"pre",{"className":260,"code":261,"language":262,"meta":263,"style":263},"language-python shiki shiki-themes github-dark","from qgis.PyQt.QtGui import QColor, QFont\nfrom qgis.core import (\n    QgsTextFormat,\n    QgsTextBufferSettings,\n    QgsUnitTypes,\n)\n\ntext_format = QgsTextFormat()\ntext_format.setFont(QFont(\"Noto Sans\", 10))\ntext_format.setSize(10)\ntext_format.setSizeUnit(QgsUnitTypes.RenderPoints)\ntext_format.setColor(QColor(\"#17211d\"))\n\nbuffer = QgsTextBufferSettings()\nbuffer.setEnabled(True)\nbuffer.setSize(1.0)\nbuffer.setSizeUnit(QgsUnitTypes.RenderMillimeters)\nbuffer.setColor(QColor(\"#ffffff\"))\nbuffer.setOpacity(0.9)\ntext_format.setBuffer(buffer)\n","python","",[18,265,266,284,297,303,309,315,321,328,340,359,369,375,386,391,402,413,424,430,441,452],{"__ignoreMap":263},[267,268,270,274,278,281],"span",{"class":176,"line":269},1,[267,271,273],{"class":272},"snl16","from",[267,275,277],{"class":276},"s95oV"," qgis.PyQt.QtGui ",[267,279,280],{"class":272},"import",[267,282,283],{"class":276}," QColor, QFont\n",[267,285,287,289,292,294],{"class":176,"line":286},2,[267,288,273],{"class":272},[267,290,291],{"class":276}," qgis.core ",[267,293,280],{"class":272},[267,295,296],{"class":276}," (\n",[267,298,300],{"class":176,"line":299},3,[267,301,302],{"class":276},"    QgsTextFormat,\n",[267,304,306],{"class":176,"line":305},4,[267,307,308],{"class":276},"    QgsTextBufferSettings,\n",[267,310,312],{"class":176,"line":311},5,[267,313,314],{"class":276},"    QgsUnitTypes,\n",[267,316,318],{"class":176,"line":317},6,[267,319,320],{"class":276},")\n",[267,322,324],{"class":176,"line":323},7,[267,325,327],{"emptyLinePlaceholder":326},true,"\n",[267,329,331,334,337],{"class":176,"line":330},8,[267,332,333],{"class":276},"text_format ",[267,335,336],{"class":272},"=",[267,338,339],{"class":276}," QgsTextFormat()\n",[267,341,343,346,350,352,356],{"class":176,"line":342},9,[267,344,345],{"class":276},"text_format.setFont(QFont(",[267,347,349],{"class":348},"sU2Wk","\"Noto Sans\"",[267,351,21],{"class":276},[267,353,355],{"class":354},"sDLfK","10",[267,357,358],{"class":276},"))\n",[267,360,362,365,367],{"class":176,"line":361},10,[267,363,364],{"class":276},"text_format.setSize(",[267,366,355],{"class":354},[267,368,320],{"class":276},[267,370,372],{"class":176,"line":371},11,[267,373,374],{"class":276},"text_format.setSizeUnit(QgsUnitTypes.RenderPoints)\n",[267,376,378,381,384],{"class":176,"line":377},12,[267,379,380],{"class":276},"text_format.setColor(QColor(",[267,382,383],{"class":348},"\"#17211d\"",[267,385,358],{"class":276},[267,387,389],{"class":176,"line":388},13,[267,390,327],{"emptyLinePlaceholder":326},[267,392,394,397,399],{"class":176,"line":393},14,[267,395,396],{"class":276},"buffer ",[267,398,336],{"class":272},[267,400,401],{"class":276}," QgsTextBufferSettings()\n",[267,403,405,408,411],{"class":176,"line":404},15,[267,406,407],{"class":276},"buffer.setEnabled(",[267,409,410],{"class":354},"True",[267,412,320],{"class":276},[267,414,416,419,422],{"class":176,"line":415},16,[267,417,418],{"class":276},"buffer.setSize(",[267,420,421],{"class":354},"1.0",[267,423,320],{"class":276},[267,425,427],{"class":176,"line":426},17,[267,428,429],{"class":276},"buffer.setSizeUnit(QgsUnitTypes.RenderMillimeters)\n",[267,431,433,436,439],{"class":176,"line":432},18,[267,434,435],{"class":276},"buffer.setColor(QColor(",[267,437,438],{"class":348},"\"#ffffff\"",[267,440,358],{"class":276},[267,442,444,447,450],{"class":176,"line":443},19,[267,445,446],{"class":276},"buffer.setOpacity(",[267,448,449],{"class":354},"0.9",[267,451,320],{"class":276},[267,453,455],{"class":176,"line":454},20,[267,456,457],{"class":276},"text_format.setBuffer(buffer)\n",[14,459,460,463,464,467,468,471,472,475,476,478],{},[64,461,462],{},"Breakdown:"," The ",[18,465,466],{},"QgsTextBufferSettings"," halo is the single most effective readability fix for labels over busy backgrounds — a one-millimetre white buffer lifts dark text off aerial imagery or dense linework. Setting ",[18,469,470],{},"setSizeUnit(RenderPoints)"," keeps font size print-stable, whereas ",[18,473,474],{},"RenderMapUnits"," would scale text with zoom. On QGIS 3.28 the API is identical; only the bundled font catalogue differs between platforms, so prefer a font you know is installed (e.g. ",[18,477,349],{},") over a system-default name.",[53,480,482],{"id":481},"wiring-settings-to-a-layer","Wiring Settings to a Layer",[14,484,485,487,488,491,492,495,496,498,499,502],{},[18,486,20],{}," joins the text format to the data. Its ",[18,489,490],{},"fieldName"," accepts either a plain attribute name or — when ",[18,493,494],{},"isExpression"," is ",[18,497,410],{}," — a full expression. You then wrap it in ",[18,500,501],{},"QgsVectorLayerSimpleLabeling"," and hand it to the layer.",[258,504,506],{"className":260,"code":505,"language":262,"meta":263,"style":263},"from qgis.core import (\n    QgsPalLayerSettings,\n    QgsVectorLayerSimpleLabeling,\n    QgsProject,\n)\n\nlayer = QgsProject.instance().mapLayersByName(\"cities\")[0]\n\nsettings = QgsPalLayerSettings()\nsettings.setFormat(text_format)\nsettings.fieldName = \"name\"\nsettings.isExpression = False\nsettings.placement = QgsPalLayerSettings.OverPoint\n\nlabeling = QgsVectorLayerSimpleLabeling(settings)\nlayer.setLabeling(labeling)\nlayer.setLabelsEnabled(True)\nlayer.triggerRepaint()\n",[18,507,508,518,523,528,533,537,541,562,566,576,581,591,601,611,615,625,630,639],{"__ignoreMap":263},[267,509,510,512,514,516],{"class":176,"line":269},[267,511,273],{"class":272},[267,513,291],{"class":276},[267,515,280],{"class":272},[267,517,296],{"class":276},[267,519,520],{"class":176,"line":286},[267,521,522],{"class":276},"    QgsPalLayerSettings,\n",[267,524,525],{"class":176,"line":299},[267,526,527],{"class":276},"    QgsVectorLayerSimpleLabeling,\n",[267,529,530],{"class":176,"line":305},[267,531,532],{"class":276},"    QgsProject,\n",[267,534,535],{"class":176,"line":311},[267,536,320],{"class":276},[267,538,539],{"class":176,"line":317},[267,540,327],{"emptyLinePlaceholder":326},[267,542,543,546,548,551,554,557,559],{"class":176,"line":323},[267,544,545],{"class":276},"layer ",[267,547,336],{"class":272},[267,549,550],{"class":276}," QgsProject.instance().mapLayersByName(",[267,552,553],{"class":348},"\"cities\"",[267,555,556],{"class":276},")[",[267,558,124],{"class":354},[267,560,561],{"class":276},"]\n",[267,563,564],{"class":176,"line":330},[267,565,327],{"emptyLinePlaceholder":326},[267,567,568,571,573],{"class":176,"line":342},[267,569,570],{"class":276},"settings ",[267,572,336],{"class":272},[267,574,575],{"class":276}," QgsPalLayerSettings()\n",[267,577,578],{"class":176,"line":361},[267,579,580],{"class":276},"settings.setFormat(text_format)\n",[267,582,583,586,588],{"class":176,"line":371},[267,584,585],{"class":276},"settings.fieldName ",[267,587,336],{"class":272},[267,589,590],{"class":348}," \"name\"\n",[267,592,593,596,598],{"class":176,"line":377},[267,594,595],{"class":276},"settings.isExpression ",[267,597,336],{"class":272},[267,599,600],{"class":354}," False\n",[267,602,603,606,608],{"class":176,"line":388},[267,604,605],{"class":276},"settings.placement ",[267,607,336],{"class":272},[267,609,610],{"class":276}," QgsPalLayerSettings.OverPoint\n",[267,612,613],{"class":176,"line":393},[267,614,327],{"emptyLinePlaceholder":326},[267,616,617,620,622],{"class":176,"line":404},[267,618,619],{"class":276},"labeling ",[267,621,336],{"class":272},[267,623,624],{"class":276}," QgsVectorLayerSimpleLabeling(settings)\n",[267,626,627],{"class":176,"line":415},[267,628,629],{"class":276},"layer.setLabeling(labeling)\n",[267,631,632,635,637],{"class":176,"line":426},[267,633,634],{"class":276},"layer.setLabelsEnabled(",[267,636,410],{"class":354},[267,638,320],{"class":276},[267,640,641],{"class":176,"line":432},[267,642,643],{"class":276},"layer.triggerRepaint()\n",[14,645,646,648,649,652,653,656,657,660,661,664,665,667],{},[64,647,462],{}," Three calls actually switch labels on: ",[18,650,651],{},"setLabeling()"," installs the configuration, ",[18,654,655],{},"setLabelsEnabled(True)"," flips the master toggle (without it nothing draws), and ",[18,658,659],{},"triggerRepaint()"," forces a canvas redraw. Forgetting the second line is the most common reason scripted labels silently fail to appear. In QGIS 3.40+ the placement enums also exist on ",[18,662,663],{},"Qgis.LabelPlacement",", but the ",[18,666,20],{},"-scoped names used here remain valid across 3.28 through 3.44.",[53,669,671],{"id":670},"expression-based-labels","Expression-Based Labels",[14,673,674,675,678,679,77],{},"Static field names rarely survive contact with real data. Expression labels let you concatenate fields, format numbers, and conditionally hide text. Set ",[18,676,677],{},"isExpression = True"," and pass an expression string to ",[18,680,490],{},[258,682,684],{"className":260,"code":683,"language":262,"meta":263,"style":263},"settings = QgsPalLayerSettings()\nsettings.setFormat(text_format)\nsettings.isExpression = True\nsettings.fieldName = (\n    \"\\\"name\\\" || '\\\\n' || format_number(\\\"population\\\", 0) || ' people'\"\n)\nsettings.placement = QgsPalLayerSettings.OverPoint\nsettings.multilineAlign = QgsPalLayerSettings.MultiAlignCenter\n\nlayer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\nlayer.setLabelsEnabled(True)\nlayer.triggerRepaint()\n",[18,685,686,694,698,707,715,747,751,759,769,773,778,786],{"__ignoreMap":263},[267,687,688,690,692],{"class":176,"line":269},[267,689,570],{"class":276},[267,691,336],{"class":272},[267,693,575],{"class":276},[267,695,696],{"class":176,"line":286},[267,697,580],{"class":276},[267,699,700,702,704],{"class":176,"line":299},[267,701,595],{"class":276},[267,703,336],{"class":272},[267,705,706],{"class":354}," True\n",[267,708,709,711,713],{"class":176,"line":305},[267,710,585],{"class":276},[267,712,336],{"class":272},[267,714,296],{"class":276},[267,716,717,720,723,726,728,731,734,737,739,742,744],{"class":176,"line":311},[267,718,719],{"class":348},"    \"",[267,721,722],{"class":354},"\\\"",[267,724,725],{"class":348},"name",[267,727,722],{"class":354},[267,729,730],{"class":348}," || '",[267,732,733],{"class":354},"\\\\",[267,735,736],{"class":348},"n' || format_number(",[267,738,722],{"class":354},[267,740,741],{"class":348},"population",[267,743,722],{"class":354},[267,745,746],{"class":348},", 0) || ' people'\"\n",[267,748,749],{"class":176,"line":317},[267,750,320],{"class":276},[267,752,753,755,757],{"class":176,"line":323},[267,754,605],{"class":276},[267,756,336],{"class":272},[267,758,610],{"class":276},[267,760,761,764,766],{"class":176,"line":330},[267,762,763],{"class":276},"settings.multilineAlign ",[267,765,336],{"class":272},[267,767,768],{"class":276}," QgsPalLayerSettings.MultiAlignCenter\n",[267,770,771],{"class":176,"line":342},[267,772,327],{"emptyLinePlaceholder":326},[267,774,775],{"class":176,"line":361},[267,776,777],{"class":276},"layer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\n",[267,779,780,782,784],{"class":176,"line":371},[267,781,634],{"class":276},[267,783,410],{"class":354},[267,785,320],{"class":276},[267,787,788],{"class":176,"line":377},[267,789,643],{"class":276},[14,791,792,794,795,798,799,802,803,806,807,810,811,77],{},[64,793,462],{}," The expression builds a two-line label — the city name, a literal newline (",[18,796,797],{},"'\\n'","), then a thousands-separated population. ",[18,800,801],{},"format_number"," keeps large values legible. Because the expression spans multiple lines, ",[18,804,805],{},"multilineAlign"," centres the wrapped text. Note the escaping: field names are wrapped in escaped double quotes (",[18,808,809],{},"\\\"name\\\"",") and string literals in single quotes, exactly as the QGIS expression engine expects. The same expression language drives data-defined overrides, which is how you push per-feature font sizes or colors — and it is the foundation for the rule filters covered in ",[27,812,814],{"href":813},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis\u002F","Add Rule-Based Labels in PyQGIS",[53,816,818],{"id":817},"data-defined-overrides","Data-Defined Overrides",[14,820,821,822,825,826,77],{},"A single text format applies one appearance to every label. Often you want the appearance itself to respond to the data — bigger fonts for bigger cities, a different color for a flagged category, or showing labels only for features above a threshold. That is the job of data-defined properties, attached through the settings object's ",[18,823,824],{},"dataDefinedProperties()"," collection. Each property is keyed by an enum on ",[18,827,20],{},[258,829,831],{"className":260,"code":830,"language":262,"meta":263,"style":263},"from qgis.core import QgsPalLayerSettings, QgsProperty\n\nsettings = QgsPalLayerSettings()\nsettings.setFormat(text_format)\nsettings.fieldName = \"name\"\nsettings.placement = QgsPalLayerSettings.OverPoint\n\nprops = settings.dataDefinedProperties()\n# Font size scales with population, clamped between 8 and 18 points\nprops.setProperty(\n    QgsPalLayerSettings.Size,\n    QgsProperty.fromExpression(\"scale_linear(\\\"population\\\", 0, 5000000, 8, 18)\"),\n)\n# Only label features whose population exceeds 50,000\nprops.setProperty(\n    QgsPalLayerSettings.Show,\n    QgsProperty.fromExpression(\"\\\"population\\\" > 50000\"),\n)\nsettings.setDataDefinedProperties(props)\n\nlayer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\nlayer.setLabelsEnabled(True)\nlayer.triggerRepaint()\n",[18,832,833,844,848,856,860,868,876,880,890,896,901,906,926,930,935,939,944,962,966,971,975,980,989],{"__ignoreMap":263},[267,834,835,837,839,841],{"class":176,"line":269},[267,836,273],{"class":272},[267,838,291],{"class":276},[267,840,280],{"class":272},[267,842,843],{"class":276}," QgsPalLayerSettings, QgsProperty\n",[267,845,846],{"class":176,"line":286},[267,847,327],{"emptyLinePlaceholder":326},[267,849,850,852,854],{"class":176,"line":299},[267,851,570],{"class":276},[267,853,336],{"class":272},[267,855,575],{"class":276},[267,857,858],{"class":176,"line":305},[267,859,580],{"class":276},[267,861,862,864,866],{"class":176,"line":311},[267,863,585],{"class":276},[267,865,336],{"class":272},[267,867,590],{"class":348},[267,869,870,872,874],{"class":176,"line":317},[267,871,605],{"class":276},[267,873,336],{"class":272},[267,875,610],{"class":276},[267,877,878],{"class":176,"line":323},[267,879,327],{"emptyLinePlaceholder":326},[267,881,882,885,887],{"class":176,"line":330},[267,883,884],{"class":276},"props ",[267,886,336],{"class":272},[267,888,889],{"class":276}," settings.dataDefinedProperties()\n",[267,891,892],{"class":176,"line":342},[267,893,895],{"class":894},"sAwPA","# Font size scales with population, clamped between 8 and 18 points\n",[267,897,898],{"class":176,"line":361},[267,899,900],{"class":276},"props.setProperty(\n",[267,902,903],{"class":176,"line":371},[267,904,905],{"class":276},"    QgsPalLayerSettings.Size,\n",[267,907,908,911,914,916,918,920,923],{"class":176,"line":377},[267,909,910],{"class":276},"    QgsProperty.fromExpression(",[267,912,913],{"class":348},"\"scale_linear(",[267,915,722],{"class":354},[267,917,741],{"class":348},[267,919,722],{"class":354},[267,921,922],{"class":348},", 0, 5000000, 8, 18)\"",[267,924,925],{"class":276},"),\n",[267,927,928],{"class":176,"line":388},[267,929,320],{"class":276},[267,931,932],{"class":176,"line":393},[267,933,934],{"class":894},"# Only label features whose population exceeds 50,000\n",[267,936,937],{"class":176,"line":404},[267,938,900],{"class":276},[267,940,941],{"class":176,"line":415},[267,942,943],{"class":276},"    QgsPalLayerSettings.Show,\n",[267,945,946,948,951,953,955,957,960],{"class":176,"line":426},[267,947,910],{"class":276},[267,949,950],{"class":348},"\"",[267,952,722],{"class":354},[267,954,741],{"class":348},[267,956,722],{"class":354},[267,958,959],{"class":348}," > 50000\"",[267,961,925],{"class":276},[267,963,964],{"class":176,"line":432},[267,965,320],{"class":276},[267,967,968],{"class":176,"line":443},[267,969,970],{"class":276},"settings.setDataDefinedProperties(props)\n",[267,972,973],{"class":176,"line":454},[267,974,327],{"emptyLinePlaceholder":326},[267,976,978],{"class":176,"line":977},21,[267,979,777],{"class":276},[267,981,983,985,987],{"class":176,"line":982},22,[267,984,634],{"class":276},[267,986,410],{"class":354},[267,988,320],{"class":276},[267,990,992],{"class":176,"line":991},23,[267,993,643],{"class":276},[14,995,996,998,999,1002,1003,1006,1007,1010,1011,1014,1015,21,1018,21,1021,21,1024,1027,1028,1031],{},[64,997,462],{}," ",[18,1000,1001],{},"QgsProperty.fromExpression()"," turns any expression into a data-defined value. The ",[18,1004,1005],{},"Size"," property uses ",[18,1008,1009],{},"scale_linear"," to map population into an 8–18 point range, so prominence reflects magnitude without manual binning. The ",[18,1012,1013],{},"Show"," property acts as a per-feature on\u002Foff switch — features failing the test are simply never considered by the placement engine, which also lightens its workload. Dozens of properties exist (",[18,1016,1017],{},"Color",[18,1019,1020],{},"BufferSize",[18,1022,1023],{},"OffsetXY",[18,1025,1026],{},"LabelRotation",", and more); they all follow this same ",[18,1029,1030],{},"setProperty(enum, QgsProperty)"," pattern. Data-defined overrides are the bridge between flat styling and the per-class behavior you get from rule-based labeling.",[53,1033,1035],{"id":1034},"placement-priority-and-obstacles","Placement, Priority, and Obstacles",[14,1037,1038,1039,1042,1043,21,1046,1049,1050,1053,1054,21,1057,1049,1060,1063,1064,1066,1067,1049,1069,1072,1073,1076,1077,1080],{},"Placement is where the PAL engine earns its name. For points, ",[18,1040,1041],{},"placement"," selects strategies such as ",[18,1044,1045],{},"OverPoint",[18,1047,1048],{},"AroundPoint",", or ",[18,1051,1052],{},"OrderedPositionsAroundPoint","; for lines, ",[18,1055,1056],{},"Line",[18,1058,1059],{},"Curved",[18,1061,1062],{},"Horizontal","; for polygons, ",[18,1065,1045],{}," (centroid), ",[18,1068,1062],{},[18,1070,1071],{},"Free",". When labels compete for the same pixels, two mechanisms decide the winner: per-layer ",[18,1074,1075],{},"priority"," (0–10) and whether features act as ",[18,1078,1079],{},"obstacles"," that repel other layers' labels.",[258,1082,1084],{"className":260,"code":1083,"language":262,"meta":263,"style":263},"from qgis.core import QgsPalLayerSettings\n\nroad_settings = QgsPalLayerSettings()\nroad_settings.setFormat(text_format)\nroad_settings.fieldName = \"road_name\"\nroad_settings.placement = QgsPalLayerSettings.Curved\nroad_settings.priority = 8                    # 0 (low) .. 10 (high)\nroad_settings.lineSettings().setPlacementFlags(\n    QgsPalLayerSettings.OnLine | QgsPalLayerSettings.MapOrientation\n)\n\n# Treat building polygons as obstacles so road labels avoid them\nbuilding_settings = QgsPalLayerSettings()\nbuilding_settings.setFormat(text_format)\nbuilding_settings.fieldName = \"addr\"\nbuilding_settings.obstacle = True\nbuilding_settings.obstacleSettings().setFactor(1.5)\n",[18,1085,1086,1097,1101,1110,1115,1125,1135,1148,1153,1164,1168,1172,1177,1186,1191,1201,1210],{"__ignoreMap":263},[267,1087,1088,1090,1092,1094],{"class":176,"line":269},[267,1089,273],{"class":272},[267,1091,291],{"class":276},[267,1093,280],{"class":272},[267,1095,1096],{"class":276}," QgsPalLayerSettings\n",[267,1098,1099],{"class":176,"line":286},[267,1100,327],{"emptyLinePlaceholder":326},[267,1102,1103,1106,1108],{"class":176,"line":299},[267,1104,1105],{"class":276},"road_settings ",[267,1107,336],{"class":272},[267,1109,575],{"class":276},[267,1111,1112],{"class":176,"line":305},[267,1113,1114],{"class":276},"road_settings.setFormat(text_format)\n",[267,1116,1117,1120,1122],{"class":176,"line":311},[267,1118,1119],{"class":276},"road_settings.fieldName ",[267,1121,336],{"class":272},[267,1123,1124],{"class":348}," \"road_name\"\n",[267,1126,1127,1130,1132],{"class":176,"line":317},[267,1128,1129],{"class":276},"road_settings.placement ",[267,1131,336],{"class":272},[267,1133,1134],{"class":276}," QgsPalLayerSettings.Curved\n",[267,1136,1137,1140,1142,1145],{"class":176,"line":323},[267,1138,1139],{"class":276},"road_settings.priority ",[267,1141,336],{"class":272},[267,1143,1144],{"class":354}," 8",[267,1146,1147],{"class":894},"                    # 0 (low) .. 10 (high)\n",[267,1149,1150],{"class":176,"line":330},[267,1151,1152],{"class":276},"road_settings.lineSettings().setPlacementFlags(\n",[267,1154,1155,1158,1161],{"class":176,"line":342},[267,1156,1157],{"class":276},"    QgsPalLayerSettings.OnLine ",[267,1159,1160],{"class":272},"|",[267,1162,1163],{"class":276}," QgsPalLayerSettings.MapOrientation\n",[267,1165,1166],{"class":176,"line":361},[267,1167,320],{"class":276},[267,1169,1170],{"class":176,"line":371},[267,1171,327],{"emptyLinePlaceholder":326},[267,1173,1174],{"class":176,"line":377},[267,1175,1176],{"class":894},"# Treat building polygons as obstacles so road labels avoid them\n",[267,1178,1179,1182,1184],{"class":176,"line":388},[267,1180,1181],{"class":276},"building_settings ",[267,1183,336],{"class":272},[267,1185,575],{"class":276},[267,1187,1188],{"class":176,"line":393},[267,1189,1190],{"class":276},"building_settings.setFormat(text_format)\n",[267,1192,1193,1196,1198],{"class":176,"line":404},[267,1194,1195],{"class":276},"building_settings.fieldName ",[267,1197,336],{"class":272},[267,1199,1200],{"class":348}," \"addr\"\n",[267,1202,1203,1206,1208],{"class":176,"line":415},[267,1204,1205],{"class":276},"building_settings.obstacle ",[267,1207,336],{"class":272},[267,1209,706],{"class":354},[267,1211,1212,1215,1218],{"class":176,"line":426},[267,1213,1214],{"class":276},"building_settings.obstacleSettings().setFactor(",[267,1216,1217],{"class":354},"1.5",[267,1219,320],{"class":276},[14,1221,1222,998,1224,1227,1228,1230,1231,1234,1235,1238,1239,1243],{},[64,1223,462],{},[18,1225,1226],{},"priority = 8"," tells the engine to keep road labels even when lower-priority labels must be dropped. ",[18,1229,1059],{}," placement bends street names along the line geometry, and ",[18,1232,1233],{},"lineSettings()"," (the typed sub-settings object introduced in the 3.x line) controls whether labels sit on, above, or below the line. Marking buildings as obstacles with an ",[18,1236,1237],{},"obstacleSettings().setFactor(1.5)"," makes the engine treat them as 1.5x their footprint when avoiding overlaps. For polygon thematic maps, pair these placement choices with the renderer work in ",[27,1240,1242],{"href":1241},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated & Categorized Renderers"," so labels reinforce the color classes rather than fight them.",[53,1245,1247],{"id":1246},"annotations-fixed-text-on-the-map","Annotations: Fixed Text on the Map",[14,1249,1250,1251,1254,1255,1258,1259,77],{},"Labels are bound to features. Annotations are not — they are free-floating callouts, titles, or legends pinned either to a map coordinate or to a screen position. The modern path uses an annotation layer holding ",[18,1252,1253],{},"QgsAnnotationItem"," subclasses; the classic path uses ",[18,1256,1257],{},"QgsTextAnnotation"," managed by ",[18,1260,1261],{},"QgsMapCanvas",[258,1263,1265],{"className":260,"code":1264,"language":262,"meta":263,"style":263},"from qgis.core import (\n    QgsProject,\n    QgsAnnotationLayer,\n    QgsAnnotationPointTextItem,\n    QgsPointXY,\n    QgsCoordinateReferenceSystem,\n)\n\nproject = QgsProject.instance()\nanno_layer = QgsAnnotationLayer(\n    \"Map notes\",\n    QgsAnnotationLayer.LayerOptions(project.transformContext()),\n)\nanno_layer.setCrs(QgsCoordinateReferenceSystem(\"EPSG:4326\"))\n\nitem = QgsAnnotationPointTextItem(\"Survey area\", QgsPointXY(12.49, 41.89))\nitem.setFormat(text_format)\nanno_layer.addItem(item)\n\nproject.addMapLayer(anno_layer)\nanno_layer.triggerRepaint()\n",[18,1266,1267,1277,1281,1286,1291,1296,1301,1305,1309,1319,1329,1337,1342,1346,1356,1360,1386,1391,1396,1400,1405],{"__ignoreMap":263},[267,1268,1269,1271,1273,1275],{"class":176,"line":269},[267,1270,273],{"class":272},[267,1272,291],{"class":276},[267,1274,280],{"class":272},[267,1276,296],{"class":276},[267,1278,1279],{"class":176,"line":286},[267,1280,532],{"class":276},[267,1282,1283],{"class":176,"line":299},[267,1284,1285],{"class":276},"    QgsAnnotationLayer,\n",[267,1287,1288],{"class":176,"line":305},[267,1289,1290],{"class":276},"    QgsAnnotationPointTextItem,\n",[267,1292,1293],{"class":176,"line":311},[267,1294,1295],{"class":276},"    QgsPointXY,\n",[267,1297,1298],{"class":176,"line":317},[267,1299,1300],{"class":276},"    QgsCoordinateReferenceSystem,\n",[267,1302,1303],{"class":176,"line":323},[267,1304,320],{"class":276},[267,1306,1307],{"class":176,"line":330},[267,1308,327],{"emptyLinePlaceholder":326},[267,1310,1311,1314,1316],{"class":176,"line":342},[267,1312,1313],{"class":276},"project ",[267,1315,336],{"class":272},[267,1317,1318],{"class":276}," QgsProject.instance()\n",[267,1320,1321,1324,1326],{"class":176,"line":361},[267,1322,1323],{"class":276},"anno_layer ",[267,1325,336],{"class":272},[267,1327,1328],{"class":276}," QgsAnnotationLayer(\n",[267,1330,1331,1334],{"class":176,"line":371},[267,1332,1333],{"class":348},"    \"Map notes\"",[267,1335,1336],{"class":276},",\n",[267,1338,1339],{"class":176,"line":377},[267,1340,1341],{"class":276},"    QgsAnnotationLayer.LayerOptions(project.transformContext()),\n",[267,1343,1344],{"class":176,"line":388},[267,1345,320],{"class":276},[267,1347,1348,1351,1354],{"class":176,"line":393},[267,1349,1350],{"class":276},"anno_layer.setCrs(QgsCoordinateReferenceSystem(",[267,1352,1353],{"class":348},"\"EPSG:4326\"",[267,1355,358],{"class":276},[267,1357,1358],{"class":176,"line":404},[267,1359,327],{"emptyLinePlaceholder":326},[267,1361,1362,1365,1367,1370,1373,1376,1379,1381,1384],{"class":176,"line":415},[267,1363,1364],{"class":276},"item ",[267,1366,336],{"class":272},[267,1368,1369],{"class":276}," QgsAnnotationPointTextItem(",[267,1371,1372],{"class":348},"\"Survey area\"",[267,1374,1375],{"class":276},", QgsPointXY(",[267,1377,1378],{"class":354},"12.49",[267,1380,21],{"class":276},[267,1382,1383],{"class":354},"41.89",[267,1385,358],{"class":276},[267,1387,1388],{"class":176,"line":426},[267,1389,1390],{"class":276},"item.setFormat(text_format)\n",[267,1392,1393],{"class":176,"line":432},[267,1394,1395],{"class":276},"anno_layer.addItem(item)\n",[267,1397,1398],{"class":176,"line":443},[267,1399,327],{"emptyLinePlaceholder":326},[267,1401,1402],{"class":176,"line":454},[267,1403,1404],{"class":276},"project.addMapLayer(anno_layer)\n",[267,1406,1407],{"class":176,"line":977},[267,1408,1409],{"class":276},"anno_layer.triggerRepaint()\n",[14,1411,1412,998,1414,1417,1418,1421,1422,1424,1425,1427],{},[64,1413,462],{},[18,1415,1416],{},"QgsAnnotationLayer"," behaves like any other layer — it appears in the tree, exports with the map, and respects CRS. ",[18,1419,1420],{},"QgsAnnotationPointTextItem"," (available from QGIS 3.22 onward, fully fleshed out by 3.34) anchors text to a real-world coordinate, so it pans and zooms with the map. This is the recommended approach for new code. The legacy ",[18,1423,1257],{}," route still works for canvas-only callouts but does not persist into layouts as cleanly. Annotation layers also share the same ",[18,1426,24],{}," you built earlier, keeping typography consistent between feature labels and map notes.",[1429,1430,1432],"h3",{"id":1431},"the-legacy-qgstextannotation-route","The Legacy QgsTextAnnotation Route",[14,1434,1435,1436,1438,1439,1442,1443,1446],{},"You will still encounter ",[18,1437,1257],{}," in older scripts and plugins, so it is worth recognizing. It stores HTML-formatted text in a ",[18,1440,1441],{},"QTextDocument"," and is managed by the project's ",[18,1444,1445],{},"QgsAnnotationManager"," rather than living in the layer tree.",[258,1448,1450],{"className":260,"code":1449,"language":262,"meta":263,"style":263},"from qgis.PyQt.QtGui import QTextDocument\nfrom qgis.core import QgsTextAnnotation, QgsProject, QgsPointXY\n\ndoc = QTextDocument()\ndoc.setHtml(\"\u003Cb>Study site A\u003C\u002Fb>\u003Cbr\u002F>collected 2024-09\")\n\nannotation = QgsTextAnnotation()\nannotation.setDocument(doc)\nannotation.setMapPosition(QgsPointXY(12.49, 41.89))\nannotation.setHasFixedMapPosition(True)\n\nQgsProject.instance().annotationManager().addAnnotation(annotation)\n",[18,1451,1452,1463,1474,1478,1488,1498,1502,1512,1517,1530,1539,1543],{"__ignoreMap":263},[267,1453,1454,1456,1458,1460],{"class":176,"line":269},[267,1455,273],{"class":272},[267,1457,277],{"class":276},[267,1459,280],{"class":272},[267,1461,1462],{"class":276}," QTextDocument\n",[267,1464,1465,1467,1469,1471],{"class":176,"line":286},[267,1466,273],{"class":272},[267,1468,291],{"class":276},[267,1470,280],{"class":272},[267,1472,1473],{"class":276}," QgsTextAnnotation, QgsProject, QgsPointXY\n",[267,1475,1476],{"class":176,"line":299},[267,1477,327],{"emptyLinePlaceholder":326},[267,1479,1480,1483,1485],{"class":176,"line":305},[267,1481,1482],{"class":276},"doc ",[267,1484,336],{"class":272},[267,1486,1487],{"class":276}," QTextDocument()\n",[267,1489,1490,1493,1496],{"class":176,"line":311},[267,1491,1492],{"class":276},"doc.setHtml(",[267,1494,1495],{"class":348},"\"\u003Cb>Study site A\u003C\u002Fb>\u003Cbr\u002F>collected 2024-09\"",[267,1497,320],{"class":276},[267,1499,1500],{"class":176,"line":317},[267,1501,327],{"emptyLinePlaceholder":326},[267,1503,1504,1507,1509],{"class":176,"line":323},[267,1505,1506],{"class":276},"annotation ",[267,1508,336],{"class":272},[267,1510,1511],{"class":276}," QgsTextAnnotation()\n",[267,1513,1514],{"class":176,"line":330},[267,1515,1516],{"class":276},"annotation.setDocument(doc)\n",[267,1518,1519,1522,1524,1526,1528],{"class":176,"line":342},[267,1520,1521],{"class":276},"annotation.setMapPosition(QgsPointXY(",[267,1523,1378],{"class":354},[267,1525,21],{"class":276},[267,1527,1383],{"class":354},[267,1529,358],{"class":276},[267,1531,1532,1535,1537],{"class":176,"line":361},[267,1533,1534],{"class":276},"annotation.setHasFixedMapPosition(",[267,1536,410],{"class":354},[267,1538,320],{"class":276},[267,1540,1541],{"class":176,"line":371},[267,1542,327],{"emptyLinePlaceholder":326},[267,1544,1545],{"class":176,"line":377},[267,1546,1547],{"class":276},"QgsProject.instance().annotationManager().addAnnotation(annotation)\n",[14,1549,1550,998,1552,1555,1556,1559,1560,1562,1563,1565],{},[64,1551,462],{},[18,1553,1554],{},"setHasFixedMapPosition(True)"," pins the callout to a coordinate; set it ",[18,1557,1558],{},"False"," to pin to a screen position instead. The HTML body allows mixed formatting that a single ",[18,1561,24],{}," cannot express. For new projects prefer the annotation-layer approach above — it is cleaner, layout-aware, and easier to manage programmatically — but reach for ",[18,1564,1257],{}," when you must edit or remove annotations a legacy script created.",[53,1567,1569],{"id":1568},"connecting-labels-to-styling-and-renderers","Connecting Labels to Styling and Renderers",[14,1571,1572],{},"Labels never live in isolation. Their job is to clarify whatever the renderer is communicating, so coordinate the two. When a layer is styled by a categorized renderer, mirror its categories in your label colors or rule filters so a reader's eye links a label to its class instantly. When it is styled by a graduated renderer, consider data-defined font sizing that tracks the same numeric field the color ramp uses, reinforcing magnitude through two visual channels at once.",[258,1574,1576],{"className":260,"code":1575,"language":262,"meta":263,"style":263},"# Tint label text to match the renderer's class color for a category\nfrom qgis.core import QgsProperty, QgsPalLayerSettings\n\nprops = settings.dataDefinedProperties()\nprops.setProperty(\n    QgsPalLayerSettings.Color,\n    QgsProperty.fromExpression(\n        \"CASE WHEN \\\"type\\\" = 'park' THEN '#15803d' \"\n        \"WHEN \\\"type\\\" = 'water' THEN '#2563eb' \"\n        \"ELSE '#17211d' END\"\n    ),\n)\nsettings.setDataDefinedProperties(props)\n",[18,1577,1578,1583,1594,1598,1606,1610,1615,1620,1635,1649,1654,1659,1663],{"__ignoreMap":263},[267,1579,1580],{"class":176,"line":269},[267,1581,1582],{"class":894},"# Tint label text to match the renderer's class color for a category\n",[267,1584,1585,1587,1589,1591],{"class":176,"line":286},[267,1586,273],{"class":272},[267,1588,291],{"class":276},[267,1590,280],{"class":272},[267,1592,1593],{"class":276}," QgsProperty, QgsPalLayerSettings\n",[267,1595,1596],{"class":176,"line":299},[267,1597,327],{"emptyLinePlaceholder":326},[267,1599,1600,1602,1604],{"class":176,"line":305},[267,1601,884],{"class":276},[267,1603,336],{"class":272},[267,1605,889],{"class":276},[267,1607,1608],{"class":176,"line":311},[267,1609,900],{"class":276},[267,1611,1612],{"class":176,"line":317},[267,1613,1614],{"class":276},"    QgsPalLayerSettings.Color,\n",[267,1616,1617],{"class":176,"line":323},[267,1618,1619],{"class":276},"    QgsProperty.fromExpression(\n",[267,1621,1622,1625,1627,1630,1632],{"class":176,"line":330},[267,1623,1624],{"class":348},"        \"CASE WHEN ",[267,1626,722],{"class":354},[267,1628,1629],{"class":348},"type",[267,1631,722],{"class":354},[267,1633,1634],{"class":348}," = 'park' THEN '#15803d' \"\n",[267,1636,1637,1640,1642,1644,1646],{"class":176,"line":342},[267,1638,1639],{"class":348},"        \"WHEN ",[267,1641,722],{"class":354},[267,1643,1629],{"class":348},[267,1645,722],{"class":354},[267,1647,1648],{"class":348}," = 'water' THEN '#2563eb' \"\n",[267,1650,1651],{"class":176,"line":361},[267,1652,1653],{"class":348},"        \"ELSE '#17211d' END\"\n",[267,1655,1656],{"class":176,"line":371},[267,1657,1658],{"class":276},"    ),\n",[267,1660,1661],{"class":176,"line":377},[267,1662,320],{"class":276},[267,1664,1665],{"class":176,"line":388},[267,1666,970],{"class":276},[14,1668,1669,1671,1672,1675,1676,1678,1679,1681],{},[64,1670,462],{}," A ",[18,1673,1674],{},"CASE"," expression maps category values to the same hex colors the renderer uses, so park labels read green and water labels read blue without maintaining three separate rules. This keeps the typography subordinate to, but coherent with, the symbology you set in ",[27,1677,84],{"href":83},". For maps where the classes themselves carry the message — population bands, density quintiles — build the labeling alongside the work in ",[27,1680,1242],{"href":1241}," so both layers classify the data the same way.",[53,1683,1685],{"id":1684},"callouts-connecting-displaced-labels","Callouts: Connecting Displaced Labels",[14,1687,1688,1689,1692,1693,1696,1697,77],{},"When the placement engine pushes a label away from its feature to avoid an overlap, the link between text and point can become ambiguous. Callouts draw a leader line from the label back to the feature, restoring that connection. They are configured on the settings object through a ",[18,1690,1691],{},"QgsSimpleLineCallout"," (or balloon callout) and enabled with ",[18,1694,1695],{},"setCallout()"," plus ",[18,1698,1699],{},"setCalloutEnabled()",[258,1701,1703],{"className":260,"code":1702,"language":262,"meta":263,"style":263},"from qgis.PyQt.QtGui import QColor\nfrom qgis.core import (\n    QgsPalLayerSettings,\n    QgsSimpleLineCallout,\n    QgsUnitTypes,\n)\n\nsettings = QgsPalLayerSettings()\nsettings.setFormat(text_format)\nsettings.fieldName = \"name\"\nsettings.placement = QgsPalLayerSettings.AroundPoint\nsettings.dist = 4                      # push label 4 units off the point\nsettings.distUnits = QgsUnitTypes.RenderMillimeters\n\ncallout = QgsSimpleLineCallout()\ncallout.lineSymbol().setColor(QColor(\"#2f3b35\"))\ncallout.lineSymbol().setWidth(0.3)\ncallout.setMinimumLength(2)\ncallout.setMinimumLengthUnit(QgsUnitTypes.RenderMillimeters)\n\nsettings.setCallout(callout)\nsettings.setCalloutsEnabled(True)\n",[18,1704,1705,1716,1726,1730,1735,1739,1743,1747,1755,1759,1767,1776,1789,1799,1803,1813,1823,1833,1843,1848,1852,1857],{"__ignoreMap":263},[267,1706,1707,1709,1711,1713],{"class":176,"line":269},[267,1708,273],{"class":272},[267,1710,277],{"class":276},[267,1712,280],{"class":272},[267,1714,1715],{"class":276}," QColor\n",[267,1717,1718,1720,1722,1724],{"class":176,"line":286},[267,1719,273],{"class":272},[267,1721,291],{"class":276},[267,1723,280],{"class":272},[267,1725,296],{"class":276},[267,1727,1728],{"class":176,"line":299},[267,1729,522],{"class":276},[267,1731,1732],{"class":176,"line":305},[267,1733,1734],{"class":276},"    QgsSimpleLineCallout,\n",[267,1736,1737],{"class":176,"line":311},[267,1738,314],{"class":276},[267,1740,1741],{"class":176,"line":317},[267,1742,320],{"class":276},[267,1744,1745],{"class":176,"line":323},[267,1746,327],{"emptyLinePlaceholder":326},[267,1748,1749,1751,1753],{"class":176,"line":330},[267,1750,570],{"class":276},[267,1752,336],{"class":272},[267,1754,575],{"class":276},[267,1756,1757],{"class":176,"line":342},[267,1758,580],{"class":276},[267,1760,1761,1763,1765],{"class":176,"line":361},[267,1762,585],{"class":276},[267,1764,336],{"class":272},[267,1766,590],{"class":348},[267,1768,1769,1771,1773],{"class":176,"line":371},[267,1770,605],{"class":276},[267,1772,336],{"class":272},[267,1774,1775],{"class":276}," QgsPalLayerSettings.AroundPoint\n",[267,1777,1778,1781,1783,1786],{"class":176,"line":377},[267,1779,1780],{"class":276},"settings.dist ",[267,1782,336],{"class":272},[267,1784,1785],{"class":354}," 4",[267,1787,1788],{"class":894},"                      # push label 4 units off the point\n",[267,1790,1791,1794,1796],{"class":176,"line":388},[267,1792,1793],{"class":276},"settings.distUnits ",[267,1795,336],{"class":272},[267,1797,1798],{"class":276}," QgsUnitTypes.RenderMillimeters\n",[267,1800,1801],{"class":176,"line":393},[267,1802,327],{"emptyLinePlaceholder":326},[267,1804,1805,1808,1810],{"class":176,"line":404},[267,1806,1807],{"class":276},"callout ",[267,1809,336],{"class":272},[267,1811,1812],{"class":276}," QgsSimpleLineCallout()\n",[267,1814,1815,1818,1821],{"class":176,"line":415},[267,1816,1817],{"class":276},"callout.lineSymbol().setColor(QColor(",[267,1819,1820],{"class":348},"\"#2f3b35\"",[267,1822,358],{"class":276},[267,1824,1825,1828,1831],{"class":176,"line":426},[267,1826,1827],{"class":276},"callout.lineSymbol().setWidth(",[267,1829,1830],{"class":354},"0.3",[267,1832,320],{"class":276},[267,1834,1835,1838,1841],{"class":176,"line":432},[267,1836,1837],{"class":276},"callout.setMinimumLength(",[267,1839,1840],{"class":354},"2",[267,1842,320],{"class":276},[267,1844,1845],{"class":176,"line":443},[267,1846,1847],{"class":276},"callout.setMinimumLengthUnit(QgsUnitTypes.RenderMillimeters)\n",[267,1849,1850],{"class":176,"line":454},[267,1851,327],{"emptyLinePlaceholder":326},[267,1853,1854],{"class":176,"line":977},[267,1855,1856],{"class":276},"settings.setCallout(callout)\n",[267,1858,1859,1862,1864],{"class":176,"line":982},[267,1860,1861],{"class":276},"settings.setCalloutsEnabled(",[267,1863,410],{"class":354},[267,1865,320],{"class":276},[14,1867,1868,998,1870,1872,1873,1876,1877,1879,1880,1883],{},[64,1869,462],{},[18,1871,1048],{}," placement combined with ",[18,1874,1875],{},"dist = 4"," deliberately offsets labels, which is when callouts earn their keep. ",[18,1878,1691],{}," draws a thin leader from label to anchor; ",[18,1881,1882],{},"setMinimumLength"," suppresses the line when the label sits close enough that a connector would be visual clutter. Callouts were introduced in QGIS 3.10 and are fully stable across 3.28–3.44, making them a safe default for dense point maps where every label cannot sit exactly on its feature.",[53,1885,1887],{"id":1886},"inspecting-and-cloning-existing-labeling","Inspecting and Cloning Existing Labeling",[14,1889,1890,1891,1894,1895,1898,1899,1901],{},"Production scripts often need to read a layer's current labeling before changing it — to clone a style across layers, audit a project, or toggle one property without rebuilding everything. The labeling object round-trips cleanly: ",[18,1892,1893],{},"layer.labeling()"," returns the active configuration, and for simple labeling its ",[18,1896,1897],{},"settings()"," method hands back the ",[18,1900,20],{}," you can copy and adapt.",[258,1903,1905],{"className":260,"code":1904,"language":262,"meta":263,"style":263},"from qgis.core import (\n    QgsVectorLayerSimpleLabeling,\n    QgsProject,\n)\n\nsource = QgsProject.instance().mapLayersByName(\"cities\")[0]\ntargets = QgsProject.instance().mapLayersByName(\"towns\")\n\nif source.labelsEnabled() and isinstance(\n    source.labeling(), QgsVectorLayerSimpleLabeling\n):\n    base_settings = source.labeling().settings()\n    for layer in targets:\n        cloned = QgsVectorLayerSimpleLabeling(base_settings.clone())\n        layer.setLabeling(cloned)\n        layer.setLabelsEnabled(True)\n        layer.triggerRepaint()\n",[18,1906,1907,1917,1921,1925,1929,1933,1950,1964,1968,1985,1990,1995,2005,2019,2029,2034,2043],{"__ignoreMap":263},[267,1908,1909,1911,1913,1915],{"class":176,"line":269},[267,1910,273],{"class":272},[267,1912,291],{"class":276},[267,1914,280],{"class":272},[267,1916,296],{"class":276},[267,1918,1919],{"class":176,"line":286},[267,1920,527],{"class":276},[267,1922,1923],{"class":176,"line":299},[267,1924,532],{"class":276},[267,1926,1927],{"class":176,"line":305},[267,1928,320],{"class":276},[267,1930,1931],{"class":176,"line":311},[267,1932,327],{"emptyLinePlaceholder":326},[267,1934,1935,1938,1940,1942,1944,1946,1948],{"class":176,"line":317},[267,1936,1937],{"class":276},"source ",[267,1939,336],{"class":272},[267,1941,550],{"class":276},[267,1943,553],{"class":348},[267,1945,556],{"class":276},[267,1947,124],{"class":354},[267,1949,561],{"class":276},[267,1951,1952,1955,1957,1959,1962],{"class":176,"line":323},[267,1953,1954],{"class":276},"targets ",[267,1956,336],{"class":272},[267,1958,550],{"class":276},[267,1960,1961],{"class":348},"\"towns\"",[267,1963,320],{"class":276},[267,1965,1966],{"class":176,"line":330},[267,1967,327],{"emptyLinePlaceholder":326},[267,1969,1970,1973,1976,1979,1982],{"class":176,"line":342},[267,1971,1972],{"class":272},"if",[267,1974,1975],{"class":276}," source.labelsEnabled() ",[267,1977,1978],{"class":272},"and",[267,1980,1981],{"class":354}," isinstance",[267,1983,1984],{"class":276},"(\n",[267,1986,1987],{"class":176,"line":361},[267,1988,1989],{"class":276},"    source.labeling(), QgsVectorLayerSimpleLabeling\n",[267,1991,1992],{"class":176,"line":371},[267,1993,1994],{"class":276},"):\n",[267,1996,1997,2000,2002],{"class":176,"line":377},[267,1998,1999],{"class":276},"    base_settings ",[267,2001,336],{"class":272},[267,2003,2004],{"class":276}," source.labeling().settings()\n",[267,2006,2007,2010,2013,2016],{"class":176,"line":388},[267,2008,2009],{"class":272},"    for",[267,2011,2012],{"class":276}," layer ",[267,2014,2015],{"class":272},"in",[267,2017,2018],{"class":276}," targets:\n",[267,2020,2021,2024,2026],{"class":176,"line":393},[267,2022,2023],{"class":276},"        cloned ",[267,2025,336],{"class":272},[267,2027,2028],{"class":276}," QgsVectorLayerSimpleLabeling(base_settings.clone())\n",[267,2030,2031],{"class":176,"line":404},[267,2032,2033],{"class":276},"        layer.setLabeling(cloned)\n",[267,2035,2036,2039,2041],{"class":176,"line":415},[267,2037,2038],{"class":276},"        layer.setLabelsEnabled(",[267,2040,410],{"class":354},[267,2042,320],{"class":276},[267,2044,2045],{"class":176,"line":426},[267,2046,2047],{"class":276},"        layer.triggerRepaint()\n",[14,2049,2050,2052,2053,2056,2057,2060,2061,2063,2064,2067],{},[64,2051,462],{}," Guard with ",[18,2054,2055],{},"labelsEnabled()"," and an ",[18,2058,2059],{},"isinstance"," check before reading settings, because a layer may have rule-based or no labeling at all, and ",[18,2062,1897],{}," only exists on the simple wrapper. Call ",[18,2065,2066],{},".clone()"," on the settings — never reuse the same object across layers, since they would then share mutable state and one edit would silently change them all. This clone-and-attach pattern is how you propagate a hand-tuned style across a whole project from a single source layer.",[53,2069,2071],{"id":2070},"rendering-labeled-maps-in-standalone-scripts","Rendering Labeled Maps in Standalone Scripts",[14,2073,2074,2075,2078,2079,2082],{},"Inside the QGIS desktop the canvas repaints automatically. In a headless script — a scheduled export, a server task — you drive a ",[18,2076,2077],{},"QgsMapSettings"," and ",[18,2080,2081],{},"QgsMapRendererParallelJob"," yourself, and labels render only if they are enabled on each layer and label rendering is left on in the map settings.",[258,2084,2086],{"className":260,"code":2085,"language":262,"meta":263,"style":263},"from qgis.core import (\n    QgsMapSettings,\n    QgsMapRendererParallelJob,\n    QgsProject,\n)\nfrom qgis.PyQt.QtCore import QSize\nfrom qgis.PyQt.QtGui import QColor\n\nlayer = QgsProject.instance().mapLayersByName(\"cities\")[0]\n\nsettings = QgsMapSettings()\nsettings.setLayers([layer])\nsettings.setBackgroundColor(QColor(\"#f6f3ea\"))\nsettings.setOutputSize(QSize(1600, 1000))\nsettings.setExtent(layer.extent())\n# Label rendering is on by default; this makes the intent explicit\nsettings.setFlag(QgsMapSettings.DrawLabeling, True)\n\njob = QgsMapRendererParallelJob(settings)\njob.start()\njob.waitForFinished()\njob.renderedImage().save(\"\u002Ftmp\u002Flabeled_map.png\", \"PNG\")\n",[18,2087,2088,2098,2103,2108,2112,2116,2128,2138,2142,2158,2162,2171,2176,2186,2201,2206,2211,2220,2224,2234,2239,2244],{"__ignoreMap":263},[267,2089,2090,2092,2094,2096],{"class":176,"line":269},[267,2091,273],{"class":272},[267,2093,291],{"class":276},[267,2095,280],{"class":272},[267,2097,296],{"class":276},[267,2099,2100],{"class":176,"line":286},[267,2101,2102],{"class":276},"    QgsMapSettings,\n",[267,2104,2105],{"class":176,"line":299},[267,2106,2107],{"class":276},"    QgsMapRendererParallelJob,\n",[267,2109,2110],{"class":176,"line":305},[267,2111,532],{"class":276},[267,2113,2114],{"class":176,"line":311},[267,2115,320],{"class":276},[267,2117,2118,2120,2123,2125],{"class":176,"line":317},[267,2119,273],{"class":272},[267,2121,2122],{"class":276}," qgis.PyQt.QtCore ",[267,2124,280],{"class":272},[267,2126,2127],{"class":276}," QSize\n",[267,2129,2130,2132,2134,2136],{"class":176,"line":323},[267,2131,273],{"class":272},[267,2133,277],{"class":276},[267,2135,280],{"class":272},[267,2137,1715],{"class":276},[267,2139,2140],{"class":176,"line":330},[267,2141,327],{"emptyLinePlaceholder":326},[267,2143,2144,2146,2148,2150,2152,2154,2156],{"class":176,"line":342},[267,2145,545],{"class":276},[267,2147,336],{"class":272},[267,2149,550],{"class":276},[267,2151,553],{"class":348},[267,2153,556],{"class":276},[267,2155,124],{"class":354},[267,2157,561],{"class":276},[267,2159,2160],{"class":176,"line":361},[267,2161,327],{"emptyLinePlaceholder":326},[267,2163,2164,2166,2168],{"class":176,"line":371},[267,2165,570],{"class":276},[267,2167,336],{"class":272},[267,2169,2170],{"class":276}," QgsMapSettings()\n",[267,2172,2173],{"class":176,"line":377},[267,2174,2175],{"class":276},"settings.setLayers([layer])\n",[267,2177,2178,2181,2184],{"class":176,"line":388},[267,2179,2180],{"class":276},"settings.setBackgroundColor(QColor(",[267,2182,2183],{"class":348},"\"#f6f3ea\"",[267,2185,358],{"class":276},[267,2187,2188,2191,2194,2196,2199],{"class":176,"line":393},[267,2189,2190],{"class":276},"settings.setOutputSize(QSize(",[267,2192,2193],{"class":354},"1600",[267,2195,21],{"class":276},[267,2197,2198],{"class":354},"1000",[267,2200,358],{"class":276},[267,2202,2203],{"class":176,"line":404},[267,2204,2205],{"class":276},"settings.setExtent(layer.extent())\n",[267,2207,2208],{"class":176,"line":415},[267,2209,2210],{"class":894},"# Label rendering is on by default; this makes the intent explicit\n",[267,2212,2213,2216,2218],{"class":176,"line":426},[267,2214,2215],{"class":276},"settings.setFlag(QgsMapSettings.DrawLabeling, ",[267,2217,410],{"class":354},[267,2219,320],{"class":276},[267,2221,2222],{"class":176,"line":432},[267,2223,327],{"emptyLinePlaceholder":326},[267,2225,2226,2229,2231],{"class":176,"line":443},[267,2227,2228],{"class":276},"job ",[267,2230,336],{"class":272},[267,2232,2233],{"class":276}," QgsMapRendererParallelJob(settings)\n",[267,2235,2236],{"class":176,"line":454},[267,2237,2238],{"class":276},"job.start()\n",[267,2240,2241],{"class":176,"line":977},[267,2242,2243],{"class":276},"job.waitForFinished()\n",[267,2245,2246,2249,2252,2254,2257],{"class":176,"line":982},[267,2247,2248],{"class":276},"job.renderedImage().save(",[267,2250,2251],{"class":348},"\"\u002Ftmp\u002Flabeled_map.png\"",[267,2253,21],{"class":276},[267,2255,2256],{"class":348},"\"PNG\"",[267,2258,320],{"class":276},[14,2260,2261,998,2263,2266,2267,2269,2270,2273,2274,2078,2276,2279],{},[64,2262,462],{},[18,2264,2265],{},"setFlag(QgsMapSettings.DrawLabeling, True)"," is the headless equivalent of the canvas label toggle; clearing it produces an unlabeled render even when layers have labeling configured. ",[18,2268,2081],{}," runs the PAL engine off the main thread, and ",[18,2271,2272],{},"waitForFinished()"," blocks until placement and drawing complete. The label ",[36,2275,1075],{},[36,2277,2278],{},"obstacle"," settings from earlier matter most here — in a fixed-size export there is no zooming to relieve crowding, so conflict resolution decides exactly which labels survive.",[53,2281,2283],{"id":2282},"compatibility-notes","Compatibility Notes",[2285,2286,2287,2306],"table",{},[2288,2289,2290],"thead",{},[2291,2292,2293,2297,2300,2303],"tr",{},[2294,2295,2296],"th",{},"Feature",[2294,2298,2299],{},"3.28 LTR (Py 3.9)",[2294,2301,2302],{},"3.34 LTR (Py 3.12)",[2294,2304,2305],{},"3.40 \u002F 3.44",[2307,2308,2309,2328,2346,2360],"tbody",{},[2291,2310,2311,2317,2320,2323],{},[2312,2313,2314,2316],"td",{},[18,2315,20],{}," placement enums",[2312,2318,2319],{},"scoped names",[2312,2321,2322],{},"scoped names (baseline)",[2312,2324,2325,2326],{},"scoped + ",[18,2327,663],{},[2291,2329,2330,2339,2342,2344],{},[2312,2331,2332,2334,2335,2338],{},[18,2333,1233],{}," \u002F ",[18,2336,2337],{},"obstacleSettings()"," typed sub-objects",[2312,2340,2341],{},"available",[2312,2343,2341],{},[2312,2345,2341],{},[2291,2347,2348,2352,2355,2358],{},[2312,2349,2350],{},[18,2351,1420],{},[2312,2353,2354],{},"available (3.22+)",[2312,2356,2357],{},"recommended",[2312,2359,2357],{},[2291,2361,2362,2367,2369,2372],{},[2312,2363,2364,2366],{},[18,2365,1257],{}," (legacy canvas)",[2312,2368,2341],{},[2312,2370,2371],{},"deprecated for new code",[2312,2373,2341],{},[14,2375,2376,2377,2380,2381,2383],{},"Pin scripts to the scoped ",[18,2378,2379],{},"QgsPalLayerSettings.OverPoint","-style enums for the widest compatibility; they are stable from 3.28 through 3.44. Only adopt ",[18,2382,663],{}," if you have dropped support for the older LTR.",[53,2385,2387],{"id":2386},"key-takeaways","Key Takeaways",[58,2389,2390,2399,2410,2416,2419,2429],{},[61,2391,2392,2393,2395,2396,2398],{},"Labeling is a three-object graph: ",[18,2394,24],{}," (look) → ",[18,2397,20],{}," (what + where) → a labeling wrapper attached to the layer.",[61,2400,2401,2402,2078,2404,2406,2407,2409],{},"Always call ",[18,2403,655],{},[18,2405,659],{}," after ",[18,2408,651],{},", or nothing renders.",[61,2411,2412,2413,2415],{},"A ",[18,2414,466],{}," halo is the cheapest readability win available.",[61,2417,2418],{},"Use expressions for anything beyond a single field; they unlock multiline, formatting, and conditional text.",[61,2420,2421,2078,2423,2425,2426,2428],{},[18,2422,1075],{},[18,2424,2278],{},"\u002F",[18,2427,2337],{}," resolve label conflicts; tune them rather than disabling collision detection.",[61,2430,2431,2432,2434,2435,2437],{},"Prefer ",[18,2433,1416],{}," + annotation items over legacy ",[18,2436,1257],{}," for new, layout-friendly map notes.",[53,2439,2441],{"id":2440},"frequently-asked-questions","Frequently Asked Questions",[14,2443,2444,2450,2451,2454,2455,2457,2458,77],{},[64,2445,2446,2447,2449],{},"Why don't my labels appear even though ",[18,2448,651],{}," ran without error?","\nThe most likely cause is a missing ",[18,2452,2453],{},"layer.setLabelsEnabled(True)",". ",[18,2456,651],{}," installs configuration but does not switch the master label flag on. Also confirm the layer is visible, the field name is spelled exactly as in the attribute table, and you called ",[18,2459,659],{},[14,2461,2462,2465],{},[64,2463,2464],{},"What's the difference between a label and an annotation?","\nA label is generated from feature attributes and is positioned automatically by the PAL engine, one per feature. An annotation is a single piece of free text you place yourself, anchored to a coordinate or screen position, independent of any feature. Use labels for data; use annotations for titles, callouts, and notes.",[14,2467,2468,2471,2472,2474,2475,2478],{},[64,2469,2470],{},"How do I make some labels win when they overlap?","\nRaise the ",[18,2473,1075],{}," (0–10) of the important layer and mark interfering features as obstacles with ",[18,2476,2477],{},"obstacle = True",". The PAL engine drops lower-priority labels first when space is tight, so you rarely need to disable collision handling entirely.",[14,2480,2481,2484,2485,2488,2489,2492,2493,2496,2497,2500],{},[64,2482,2483],{},"Can I scale label size with the map instead of keeping it print-fixed?","\nYes. Set ",[18,2486,2487],{},"text_format.setSizeUnit(QgsUnitTypes.RenderMapUnits)"," (or ",[18,2490,2491],{},"RenderMetersInMapUnits",") so text grows and shrinks with zoom. Keep ",[18,2494,2495],{},"RenderPoints"," or ",[18,2498,2499],{},"RenderMillimeters"," when you need stable output for printed layouts.",[14,2502,2503,2506,2507,2509,2510,77],{},[64,2504,2505],{},"Do annotation layers export into print layouts?","\nYes. ",[18,2508,1416],{}," participates in the layer tree and renders into layout map items like any vector or raster layer, which is its main advantage over the legacy canvas-only ",[18,2511,1257],{},[14,2513,2514,2517,2518,2520,2521,2524],{},[64,2515,2516],{},"How do I label only some features without a separate filter layer?","\nUse a data-defined ",[18,2519,1013],{}," property: ",[18,2522,2523],{},"props.setProperty(QgsPalLayerSettings.Show, QgsProperty.fromExpression(\"\\\"population\\\" > 50000\"))",". Features failing the expression are excluded from placement entirely, which is more efficient than rendering then hiding them. For multi-class logic across scales, graduate to rule-based labeling.",[14,2526,2527,2530,2531,2533,2534,2537],{},[64,2528,2529],{},"Can a single layer carry more than one labeling style at once?","\nNot through simple labeling, which holds exactly one ",[18,2532,20],{},". When you need several distinct label treatments on the same layer — different fields, fonts, or scale bands — switch to ",[18,2535,2536],{},"QgsRuleBasedLabeling",", where each rule contributes its own settings and the engine renders all matching rules together.",[53,2539,2541],{"id":2540},"related","Related",[58,2543,2544,2548,2552,2556],{},[61,2545,2546],{},[27,2547,30],{"href":29},[61,2549,2550],{},[27,2551,84],{"href":83},[61,2553,2554],{},[27,2555,1242],{"href":1241},[61,2557,2558],{},[27,2559,814],{"href":813},[2561,2562,2563],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":263,"searchDepth":286,"depth":286,"links":2565},[2566,2567,2568,2569,2570,2571,2572,2573,2576,2577,2578,2579,2580,2581,2582,2583],{"id":55,"depth":286,"text":56},{"id":88,"depth":286,"text":89},{"id":250,"depth":286,"text":251},{"id":481,"depth":286,"text":482},{"id":670,"depth":286,"text":671},{"id":817,"depth":286,"text":818},{"id":1034,"depth":286,"text":1035},{"id":1246,"depth":286,"text":1247,"children":2574},[2575],{"id":1431,"depth":299,"text":1432},{"id":1568,"depth":286,"text":1569},{"id":1684,"depth":286,"text":1685},{"id":1886,"depth":286,"text":1887},{"id":2070,"depth":286,"text":2071},{"id":2282,"depth":286,"text":2283},{"id":2386,"depth":286,"text":2387},{"id":2440,"depth":286,"text":2441},{"id":2540,"depth":286,"text":2541},"Configure layer labeling and map annotations in PyQGIS using QgsPalLayerSettings, QgsTextFormat, buffers, placement rules, and QgsTextAnnotation.","md",{},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations",{"title":5,"description":2584},"pyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Findex","MHdJsKgN2s4v4RHAbNfMQmtnZKi4GViM2dUcNv1SOEM",1781792483472]