[{"data":1,"prerenderedAt":1377},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcurved-labels-along-lines-pyqgis":3},{"id":4,"title":5,"body":6,"description":1366,"extension":1367,"meta":1368,"navigation":258,"path":1373,"seo":1374,"stem":1375,"__hash__":1376},"docs\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcurved-labels-along-lines-pyqgis\u002Findex.md","Place Curved Labels Along Lines in PyQGIS",{"type":7,"value":8,"toc":1352},"minimark",[9,13,17,26,173,178,195,199,427,448,452,455,532,548,552,638,648,742,746,749,810,838,842,893,908,912,915,1022,1035,1038,1120,1124,1130,1205,1209,1261,1265,1272,1276,1282,1288,1297,1307,1317,1321,1348],[10,11,5],"h1",{"id":12},"place-curved-labels-along-lines-in-pyqgis",[14,15,16],"p",{},"Road names that follow the road, river names that bend with the river — curved labels are the difference between a map that looks drawn and one that looks generated. QGIS's labelling engine does the hard part; the work in PyQGIS is choosing the right dozen settings out of a very large class, and understanding why a label you asked for is not on the map.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002F","Labelling and Annotations in PyQGIS",". It covers curved placement along lines, repeating labels on long features, halos that keep text readable over busy backgrounds, and the priority and obstacle rules that decide which labels are drawn when they cannot all fit.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,82,88,93,95,99,102,105,108,115,119,122,170],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 264","img","A winding line labelled three ways: horizontal text ignoring the line, parallel text rotated to the segment, and curved text following the bend character by character","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Three placements on the same winding line",[40,41,42],"desc",{},"The same curved road is labelled three ways. Horizontal placement puts the text flat regardless of direction and reads poorly against the line. Parallel placement rotates the whole label to one segment's angle, so it drifts away from a curving line. Curved placement distributes characters along the line so the label follows every bend.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","264","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Curved placement is the only one that survives a bend",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"16","48","236","176","10","#fffdf7","#59645f","stroke-width:2",[51,70,74],{"x":71,"y":72,"style":73,"fill":56,"textAnchor":57},"134","72","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","Horizontal",[76,77],"path",{"d":78,"fill":79,"stroke":80,"style":81},"M40 190 C90 190 96 120 140 118 C186 116 196 96 228 92","none","#2563eb","stroke-width:3",[51,83,87],{"x":71,"y":84,"style":85,"fill":86,"textAnchor":57},"150","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","Mill Lane",[51,89,92],{"x":71,"y":90,"style":85,"fill":91,"textAnchor":57},"212","#b45309","no relationship to the line",[44,94],{"x":48,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},[51,96,98],{"x":97,"y":72,"style":73,"fill":56,"textAnchor":57},"382","Parallel",[76,100],{"d":101,"fill":79,"stroke":80,"style":81},"M288 190 C338 190 344 120 388 118 C434 116 444 96 476 92",[51,103,87],{"x":97,"y":84,"style":85,"fill":86,"textAnchor":57,"transform":104},"rotate(-14 382 150)",[51,106,107],{"x":97,"y":90,"style":85,"fill":91,"textAnchor":57},"one angle for the whole word",[44,109],{"x":110,"y":62,"width":111,"height":64,"rx":65,"fill":112,"stroke":113,"style":114},"512","232","#edf8e9","#15803d","stroke-width:2.5",[51,116,118],{"x":117,"y":72,"style":73,"fill":113,"textAnchor":57},"628","Curved",[76,120],{"d":121,"fill":79,"stroke":80,"style":81},"M536 190 C586 190 592 120 636 118 C682 116 692 96 724 92",[123,124,126,131,137,143,148,154,159,165],"g",{"style":125,"fill":86},"font-size:11px;font-family:sans-serif",[51,127,130],{"x":128,"y":64,"transform":129},"548","rotate(-4 548 176)","M",[51,132,136],{"x":133,"y":134,"transform":135},"562","168","rotate(-38 562 168)","i",[51,138,142],{"x":139,"y":140,"transform":141},"570","156","rotate(-62 570 156)","l",[51,144,142],{"x":145,"y":146,"transform":147},"576","142","rotate(-70 576 142)",[51,149,153],{"x":150,"y":151,"transform":152},"592","112","rotate(-8 592 112)","L",[51,155,21],{"x":156,"y":157,"transform":158},"604","108","rotate(-6 604 108)",[51,160,164],{"x":161,"y":162,"transform":163},"616","106","rotate(-6 616 106)","n",[51,166,169],{"x":117,"y":167,"transform":168},"104","rotate(-6 628 104)","e",[51,171,172],{"x":117,"y":90,"style":85,"fill":113,"textAnchor":57},"each character follows the line",[174,175,177],"h2",{"id":176},"prerequisites","Prerequisites",[179,180,181,189,192],"ul",{},[182,183,184,188],"li",{},[185,186,187],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[182,190,191],{},"A line layer with a name field worth labelling.",[182,193,194],{},"A projected CRS if you use map-unit offsets; millimetre units are safer and are used here.",[174,196,198],{"id":197},"enable-curved-labels","Enable curved labels",[200,201,206],"pre",{"className":202,"code":203,"language":204,"meta":205,"style":205},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsPalLayerSettings, QgsTextFormat, QgsVectorLayerSimpleLabeling, QgsProject,\n)\nfrom qgis.PyQt.QtGui import QFont, QColor\n\nlayer = QgsProject.instance().mapLayersByName(\"roads\")[0]\n\nsettings = QgsPalLayerSettings()\nsettings.fieldName = \"name\"\nsettings.placement = QgsPalLayerSettings.Curved\nsettings.maxCurvedCharAngleIn = 25\nsettings.maxCurvedCharAngleOut = -25\n\ntext_format = QgsTextFormat()\ntext_format.setFont(QFont(\"Noto Sans\", 9))\ntext_format.setColor(QColor(\"#17211d\"))\nsettings.setFormat(text_format)\n\nlayer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\nlayer.setLabelsEnabled(True)\nlayer.triggerRepaint()\n","python","",[207,208,209,228,234,240,253,260,285,290,301,312,323,334,348,353,364,382,393,399,404,410,421],"code",{"__ignoreMap":205},[210,211,214,218,222,225],"span",{"class":212,"line":213},"line",1,[210,215,217],{"class":216},"snl16","from",[210,219,221],{"class":220},"s95oV"," qgis.core ",[210,223,224],{"class":216},"import",[210,226,227],{"class":220}," (\n",[210,229,231],{"class":212,"line":230},2,[210,232,233],{"class":220},"    QgsPalLayerSettings, QgsTextFormat, QgsVectorLayerSimpleLabeling, QgsProject,\n",[210,235,237],{"class":212,"line":236},3,[210,238,239],{"class":220},")\n",[210,241,243,245,248,250],{"class":212,"line":242},4,[210,244,217],{"class":216},[210,246,247],{"class":220}," qgis.PyQt.QtGui ",[210,249,224],{"class":216},[210,251,252],{"class":220}," QFont, QColor\n",[210,254,256],{"class":212,"line":255},5,[210,257,259],{"emptyLinePlaceholder":258},true,"\n",[210,261,263,266,269,272,276,279,282],{"class":212,"line":262},6,[210,264,265],{"class":220},"layer ",[210,267,268],{"class":216},"=",[210,270,271],{"class":220}," QgsProject.instance().mapLayersByName(",[210,273,275],{"class":274},"sU2Wk","\"roads\"",[210,277,278],{"class":220},")[",[210,280,46],{"class":281},"sDLfK",[210,283,284],{"class":220},"]\n",[210,286,288],{"class":212,"line":287},7,[210,289,259],{"emptyLinePlaceholder":258},[210,291,293,296,298],{"class":212,"line":292},8,[210,294,295],{"class":220},"settings ",[210,297,268],{"class":216},[210,299,300],{"class":220}," QgsPalLayerSettings()\n",[210,302,304,307,309],{"class":212,"line":303},9,[210,305,306],{"class":220},"settings.fieldName ",[210,308,268],{"class":216},[210,310,311],{"class":274}," \"name\"\n",[210,313,315,318,320],{"class":212,"line":314},10,[210,316,317],{"class":220},"settings.placement ",[210,319,268],{"class":216},[210,321,322],{"class":220}," QgsPalLayerSettings.Curved\n",[210,324,326,329,331],{"class":212,"line":325},11,[210,327,328],{"class":220},"settings.maxCurvedCharAngleIn ",[210,330,268],{"class":216},[210,332,333],{"class":281}," 25\n",[210,335,337,340,342,345],{"class":212,"line":336},12,[210,338,339],{"class":220},"settings.maxCurvedCharAngleOut ",[210,341,268],{"class":216},[210,343,344],{"class":216}," -",[210,346,347],{"class":281},"25\n",[210,349,351],{"class":212,"line":350},13,[210,352,259],{"emptyLinePlaceholder":258},[210,354,356,359,361],{"class":212,"line":355},14,[210,357,358],{"class":220},"text_format ",[210,360,268],{"class":216},[210,362,363],{"class":220}," QgsTextFormat()\n",[210,365,367,370,373,376,379],{"class":212,"line":366},15,[210,368,369],{"class":220},"text_format.setFont(QFont(",[210,371,372],{"class":274},"\"Noto Sans\"",[210,374,375],{"class":220},", ",[210,377,378],{"class":281},"9",[210,380,381],{"class":220},"))\n",[210,383,385,388,391],{"class":212,"line":384},16,[210,386,387],{"class":220},"text_format.setColor(QColor(",[210,389,390],{"class":274},"\"#17211d\"",[210,392,381],{"class":220},[210,394,396],{"class":212,"line":395},17,[210,397,398],{"class":220},"settings.setFormat(text_format)\n",[210,400,402],{"class":212,"line":401},18,[210,403,259],{"emptyLinePlaceholder":258},[210,405,407],{"class":212,"line":406},19,[210,408,409],{"class":220},"layer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\n",[210,411,413,416,419],{"class":212,"line":412},20,[210,414,415],{"class":220},"layer.setLabelsEnabled(",[210,417,418],{"class":281},"True",[210,420,239],{"class":220},[210,422,424],{"class":212,"line":423},21,[210,425,426],{"class":220},"layer.triggerRepaint()\n",[14,428,429,432,433,436,437,440,441,444,445,447],{},[185,430,431],{},"Breakdown:"," ",[207,434,435],{},"fieldName"," can be a plain field name or, with ",[207,438,439],{},"settings.isExpression = True",", any expression — ",[207,442,443],{},"concat(\"name\", ' (', \"class\", ')')"," for instance. ",[207,446,118],{}," placement is only valid for line geometry; on polygons or points it falls back silently, which is why a curved setting that \"does nothing\" is usually applied to the wrong layer. The two character-angle limits control how sharp a bend the engine will follow before giving up on a label position: the inside angle is positive, the outside negative, and loosening them past about 40 degrees produces text that is technically on the line and painful to read.",[174,449,451],{"id":450},"repeat-labels-on-long-features","Repeat labels on long features",[14,453,454],{},"A single label on a twelve-kilometre road is invisible unless the reader happens to be looking at that part of it.",[200,456,458],{"className":202,"code":457,"language":204,"meta":205,"style":205},"settings.repeatDistance = 120\nsettings.repeatDistanceUnit = QgsPalLayerSettings.MM      # millimetres on the page\nsettings.dist = 1.5                                        # offset from the line\nsettings.distUnits = QgsPalLayerSettings.MM\nsettings.lineSettings().setPlacementFlags(\n    QgsLabeling.OnLine | QgsLabeling.MapOrientation\n)\n",[207,459,460,470,487,500,512,517,528],{"__ignoreMap":205},[210,461,462,465,467],{"class":212,"line":213},[210,463,464],{"class":220},"settings.repeatDistance ",[210,466,268],{"class":216},[210,468,469],{"class":281}," 120\n",[210,471,472,475,477,480,483],{"class":212,"line":230},[210,473,474],{"class":220},"settings.repeatDistanceUnit ",[210,476,268],{"class":216},[210,478,479],{"class":220}," QgsPalLayerSettings.",[210,481,482],{"class":281},"MM",[210,484,486],{"class":485},"sjoCn","      # millimetres on the page\n",[210,488,489,492,494,497],{"class":212,"line":236},[210,490,491],{"class":220},"settings.dist ",[210,493,268],{"class":216},[210,495,496],{"class":281}," 1.5",[210,498,499],{"class":485},"                                        # offset from the line\n",[210,501,502,505,507,509],{"class":212,"line":242},[210,503,504],{"class":220},"settings.distUnits ",[210,506,268],{"class":216},[210,508,479],{"class":220},[210,510,511],{"class":281},"MM\n",[210,513,514],{"class":212,"line":255},[210,515,516],{"class":220},"settings.lineSettings().setPlacementFlags(\n",[210,518,519,522,525],{"class":212,"line":262},[210,520,521],{"class":220},"    QgsLabeling.OnLine ",[210,523,524],{"class":216},"|",[210,526,527],{"class":220}," QgsLabeling.MapOrientation\n",[210,529,530],{"class":212,"line":287},[210,531,239],{"class":220},[14,533,534,432,536,539,540,543,544,547],{},[185,535,431],{},[207,537,538],{},"repeatDistance"," in page millimetres means the spacing is consistent on the printed map regardless of scale — the property you want for a printed atlas, as opposed to map units which change apparent spacing with every zoom. ",[207,541,542],{},"dist"," lifts the text off the line so the stroke does not cut through the descenders; 1 to 2 mm is usually enough. ",[207,545,546],{},"MapOrientation"," keeps labels upright relative to the map rather than flipping upside down on lines drawn right-to-left, which is the single setting that stops a road network looking like it was labelled by someone standing on their head.",[174,549,551],{"id":550},"keep-text-readable-over-anything","Keep text readable over anything",[200,553,555],{"className":202,"code":554,"language":204,"meta":205,"style":205},"from qgis.core import QgsTextBufferSettings\n\nbuffer_settings = QgsTextBufferSettings()\nbuffer_settings.setEnabled(True)\nbuffer_settings.setSize(0.9)\nbuffer_settings.setSizeUnit(QgsUnitTypes.RenderMillimeters)\nbuffer_settings.setColor(QColor(\"#fffdf7\"))\nbuffer_settings.setOpacity(0.9)\n\ntext_format.setBuffer(buffer_settings)\nsettings.setFormat(text_format)\n",[207,556,557,568,572,582,591,601,606,616,625,629,634],{"__ignoreMap":205},[210,558,559,561,563,565],{"class":212,"line":213},[210,560,217],{"class":216},[210,562,221],{"class":220},[210,564,224],{"class":216},[210,566,567],{"class":220}," QgsTextBufferSettings\n",[210,569,570],{"class":212,"line":230},[210,571,259],{"emptyLinePlaceholder":258},[210,573,574,577,579],{"class":212,"line":236},[210,575,576],{"class":220},"buffer_settings ",[210,578,268],{"class":216},[210,580,581],{"class":220}," QgsTextBufferSettings()\n",[210,583,584,587,589],{"class":212,"line":242},[210,585,586],{"class":220},"buffer_settings.setEnabled(",[210,588,418],{"class":281},[210,590,239],{"class":220},[210,592,593,596,599],{"class":212,"line":255},[210,594,595],{"class":220},"buffer_settings.setSize(",[210,597,598],{"class":281},"0.9",[210,600,239],{"class":220},[210,602,603],{"class":212,"line":262},[210,604,605],{"class":220},"buffer_settings.setSizeUnit(QgsUnitTypes.RenderMillimeters)\n",[210,607,608,611,614],{"class":212,"line":287},[210,609,610],{"class":220},"buffer_settings.setColor(QColor(",[210,612,613],{"class":274},"\"#fffdf7\"",[210,615,381],{"class":220},[210,617,618,621,623],{"class":212,"line":292},[210,619,620],{"class":220},"buffer_settings.setOpacity(",[210,622,598],{"class":281},[210,624,239],{"class":220},[210,626,627],{"class":212,"line":303},[210,628,259],{"emptyLinePlaceholder":258},[210,630,631],{"class":212,"line":314},[210,632,633],{"class":220},"text_format.setBuffer(buffer_settings)\n",[210,635,636],{"class":212,"line":325},[210,637,398],{"class":220},[14,639,640,642,643,647],{},[185,641,431],{}," A buffer — a halo drawn behind the glyphs — is what makes dark text legible over aerial imagery, a hillshade, or a dense line network. Just under a millimetre is a good default: enough to separate the text from its background, small enough that adjacent letters do not merge. Slight transparency stops the halo reading as a solid white blob when labels crowd together. Set the buffer on the format ",[644,645,646],"em",{},"before"," assigning the format to the settings, since both are value objects and later edits to a copy do not propagate.",[14,649,650],{},[29,651,654,657,660,663,676,679,682,689,694,698,702,704,707,710,716,718,721,723,726,729,736,739],{"viewBox":652,"role":32,"ariaLabel":653,"xmlns":34},"0 0 760 254","The same label drawn over a busy background with no buffer, with an over-large buffer, and with a correctly sized buffer",[36,655,656],{},"Buffer size decides whether the label reads",[40,658,659],{},"Over a busy hatched background, a label with no buffer is hard to separate from the lines behind it. A label with an over-large buffer becomes a solid blob that hides the map underneath. A label with a buffer just under a millimetre is legible while the map remains visible around it.",[44,661],{"x":46,"y":46,"width":47,"height":662,"fill":49},"254",[664,665,666],"defs",{},[667,668,672],"pattern",{"id":669,"width":670,"height":670,"patternUnits":671},"labelBusy","12","userSpaceOnUse",[76,673],{"d":674,"stroke":67,"style":675,"fill":79},"M0 12 L12 0","stroke-width:1.2",[51,677,678],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Enough halo to separate, not enough to hide the map",[44,680],{"x":61,"y":62,"width":63,"height":681,"rx":65,"fill":66,"stroke":67,"style":68},"164",[44,683],{"x":684,"y":685,"width":686,"height":687,"fill":688},"32","80","204","100","url(#labelBusy)",[51,690,693],{"x":71,"y":691,"style":73,"fill":692,"textAnchor":57},"70","#b91c1c","no buffer",[51,695,87],{"x":71,"y":696,"style":697,"fill":56,"textAnchor":57},"136","text-anchor:middle;font-size:13px;font-family:sans-serif",[51,699,701],{"x":71,"y":700,"style":85,"fill":692,"textAnchor":57},"200","competes with the background",[44,703],{"x":48,"y":62,"width":63,"height":681,"rx":65,"fill":66,"stroke":67,"style":68},[44,705],{"x":706,"y":685,"width":686,"height":687,"fill":688},"280",[51,708,709],{"x":97,"y":691,"style":73,"fill":91,"textAnchor":57},"buffer 3 mm",[44,711],{"x":712,"y":713,"width":681,"height":714,"rx":715,"fill":66},"300","116","30","4",[51,717,87],{"x":97,"y":696,"style":697,"fill":56,"textAnchor":57},[51,719,720],{"x":97,"y":700,"style":85,"fill":91,"textAnchor":57},"a white block over the map",[44,722],{"x":110,"y":62,"width":111,"height":681,"rx":65,"fill":66,"stroke":113,"style":114},[44,724],{"x":725,"y":685,"width":700,"height":687,"fill":688},"528",[51,727,728],{"x":117,"y":691,"style":73,"fill":113,"textAnchor":57},"buffer 0.9 mm",[44,730],{"x":731,"y":732,"width":732,"height":733,"rx":734,"fill":66,"fillOpacity":735},"566","124","20","3",0.9,[51,737,87],{"x":117,"y":738,"style":697,"fill":56,"textAnchor":57},"139",[51,740,741],{"x":117,"y":700,"style":85,"fill":113,"textAnchor":57},"legible, map still visible",[174,743,745],{"id":744},"decide-which-labels-survive","Decide which labels survive",[14,747,748],{},"The engine will not overlap labels. When they cannot all fit, priority and obstacle settings decide who wins.",[200,750,752],{"className":202,"code":751,"language":204,"meta":205,"style":205},"settings.priority = 8                    # 0 (lowest) to 10 (highest)\nsettings.obstacle = True                 # the road itself blocks other labels\nsettings.obstacleFactor = 1.5\nsettings.displayAll = False\nsettings.zIndex = 2\n",[207,753,754,767,780,790,800],{"__ignoreMap":205},[210,755,756,759,761,764],{"class":212,"line":213},[210,757,758],{"class":220},"settings.priority ",[210,760,268],{"class":216},[210,762,763],{"class":281}," 8",[210,765,766],{"class":485},"                    # 0 (lowest) to 10 (highest)\n",[210,768,769,772,774,777],{"class":212,"line":230},[210,770,771],{"class":220},"settings.obstacle ",[210,773,268],{"class":216},[210,775,776],{"class":281}," True",[210,778,779],{"class":485},"                 # the road itself blocks other labels\n",[210,781,782,785,787],{"class":212,"line":236},[210,783,784],{"class":220},"settings.obstacleFactor ",[210,786,268],{"class":216},[210,788,789],{"class":281}," 1.5\n",[210,791,792,795,797],{"class":212,"line":242},[210,793,794],{"class":220},"settings.displayAll ",[210,796,268],{"class":216},[210,798,799],{"class":281}," False\n",[210,801,802,805,807],{"class":212,"line":255},[210,803,804],{"class":220},"settings.zIndex ",[210,806,268],{"class":216},[210,808,809],{"class":281}," 2\n",[14,811,812,432,814,817,818,821,822,825,826,829,830,833,834,837],{},[185,813,431],{},[207,815,816],{},"priority"," ranks this layer's labels against every other layer's — motorway names at 8 and footpath names at 3 produce a map that drops the right things first. ",[207,819,820],{},"obstacle"," makes the layer's ",[644,823,824],{},"features"," push labels away, so a road name from another layer will not sit across this road; ",[207,827,828],{},"obstacleFactor"," above 1 makes it push harder. ",[207,831,832],{},"displayAll = True"," forces every label to be drawn regardless of collisions, which is occasionally useful for diagnosis and almost never right for a finished map. ",[207,835,836],{},"zIndex"," controls draw order among labels that do end up overlapping.",[174,839,841],{"id":840},"label-only-some-features","Label only some features",[200,843,845],{"className":202,"code":844,"language":204,"meta":205,"style":205},"settings.fieldName = \"name\"\nsettings.isExpression = False\n\nlayer.setLabeling(QgsVectorLayerSimpleLabeling(settings))\nlayer.setSubsetString(\"\\\"class\\\" IN ('motorway', 'primary')\")\n",[207,846,847,855,864,868,872],{"__ignoreMap":205},[210,848,849,851,853],{"class":212,"line":213},[210,850,306],{"class":220},[210,852,268],{"class":216},[210,854,311],{"class":274},[210,856,857,860,862],{"class":212,"line":230},[210,858,859],{"class":220},"settings.isExpression ",[210,861,268],{"class":216},[210,863,799],{"class":281},[210,865,866],{"class":212,"line":236},[210,867,259],{"emptyLinePlaceholder":258},[210,869,870],{"class":212,"line":242},[210,871,409],{"class":220},[210,873,874,877,880,883,886,888,891],{"class":212,"line":255},[210,875,876],{"class":220},"layer.setSubsetString(",[210,878,879],{"class":274},"\"",[210,881,882],{"class":281},"\\\"",[210,884,885],{"class":274},"class",[210,887,882],{"class":281},[210,889,890],{"class":274}," IN ('motorway', 'primary')\"",[210,892,239],{"class":220},[14,894,895,897,898,902,903,907],{},[185,896,431],{}," A subset string filters the layer itself, so both rendering and labelling see only those features. To label a subset while still drawing everything, use rule-based labelling instead — one rule per condition, each with its own settings — which is covered in ",[21,899,901],{"href":900},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis\u002F","Add Rule-Based Labels in PyQGIS"," and mirrors the structure of a ",[21,904,906],{"href":905},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002F","rule-based renderer",".",[174,909,911],{"id":910},"callouts-for-the-labels-that-cannot-fit","Callouts for the labels that cannot fit",[14,913,914],{},"Some features have nowhere to put their name — a short cul-de-sac, a river reduced to a stub at the map edge, a cluster of paths in a park. Rather than dropping those labels or letting them collide, move them away and connect them with a leader line.",[200,916,918],{"className":202,"code":917,"language":204,"meta":205,"style":205},"from qgis.core import QgsSimpleLineCallout, QgsUnitTypes\nfrom qgis.PyQt.QtGui import QColor\n\ncallout = QgsSimpleLineCallout()\ncallout.setEnabled(True)\ncallout.lineSymbol().setColor(QColor(\"#59645f\"))\ncallout.lineSymbol().setWidth(0.2)\ncallout.setMinimumLength(2)\ncallout.setMinimumLengthUnit(QgsUnitTypes.RenderMillimeters)\n\nsettings.setCallout(callout)\nsettings.placement = QgsPalLayerSettings.Curved\nsettings.setObstacleSettings(settings.obstacleSettings())\n",[207,919,920,931,942,946,956,965,975,985,995,1000,1004,1009,1017],{"__ignoreMap":205},[210,921,922,924,926,928],{"class":212,"line":213},[210,923,217],{"class":216},[210,925,221],{"class":220},[210,927,224],{"class":216},[210,929,930],{"class":220}," QgsSimpleLineCallout, QgsUnitTypes\n",[210,932,933,935,937,939],{"class":212,"line":230},[210,934,217],{"class":216},[210,936,247],{"class":220},[210,938,224],{"class":216},[210,940,941],{"class":220}," QColor\n",[210,943,944],{"class":212,"line":236},[210,945,259],{"emptyLinePlaceholder":258},[210,947,948,951,953],{"class":212,"line":242},[210,949,950],{"class":220},"callout ",[210,952,268],{"class":216},[210,954,955],{"class":220}," QgsSimpleLineCallout()\n",[210,957,958,961,963],{"class":212,"line":255},[210,959,960],{"class":220},"callout.setEnabled(",[210,962,418],{"class":281},[210,964,239],{"class":220},[210,966,967,970,973],{"class":212,"line":262},[210,968,969],{"class":220},"callout.lineSymbol().setColor(QColor(",[210,971,972],{"class":274},"\"#59645f\"",[210,974,381],{"class":220},[210,976,977,980,983],{"class":212,"line":287},[210,978,979],{"class":220},"callout.lineSymbol().setWidth(",[210,981,982],{"class":281},"0.2",[210,984,239],{"class":220},[210,986,987,990,993],{"class":212,"line":292},[210,988,989],{"class":220},"callout.setMinimumLength(",[210,991,992],{"class":281},"2",[210,994,239],{"class":220},[210,996,997],{"class":212,"line":303},[210,998,999],{"class":220},"callout.setMinimumLengthUnit(QgsUnitTypes.RenderMillimeters)\n",[210,1001,1002],{"class":212,"line":314},[210,1003,259],{"emptyLinePlaceholder":258},[210,1005,1006],{"class":212,"line":325},[210,1007,1008],{"class":220},"settings.setCallout(callout)\n",[210,1010,1011,1013,1015],{"class":212,"line":336},[210,1012,317],{"class":220},[210,1014,268],{"class":216},[210,1016,322],{"class":220},[210,1018,1019],{"class":212,"line":350},[210,1020,1021],{"class":220},"settings.setObstacleSettings(settings.obstacleSettings())\n",[14,1023,1024,1026,1027,1030,1031,1034],{},[185,1025,431],{}," A callout draws only when the engine has actually moved the label away from its feature, so enabling it costs nothing on the labels that fit — there is no visual change until one is displaced. ",[207,1028,1029],{},"setMinimumLength()"," suppresses the tiny stubs that appear when a label shifts by half a millimetre and would otherwise read as specks of dirt on the map. The line symbol is an ordinary ",[207,1032,1033],{},"QgsLineSymbol",", so a dashed or curved leader is available if the cartography calls for it.",[14,1036,1037],{},"Callouts pair naturally with allowing labels to move: a label that may not be displaced never needs one. The combination that works well on dense networks is curved placement for the labels that fit along their line, permission to move for the ones that do not, and a thin grey callout so the reader can always tell which feature a displaced label belongs to. Where even that is too crowded, the honest answer is to raise the scale at which the layer's labels appear at all.",[14,1039,1040],{},[29,1041,1044,1047,1050,1053,1056,1058,1061,1064,1067,1070,1074,1076,1079,1082,1085,1087,1091,1094,1096,1099,1102,1105,1108,1114,1117],{"viewBox":1042,"role":32,"ariaLabel":1043,"xmlns":34},"0 0 760 244","A short feature whose label cannot fit along it, shown dropped entirely, overlapping its neighbour, and displaced with a thin leader line connecting it back",[36,1045,1046],{},"Three ways to handle a label that will not fit",[40,1048,1049],{},"A short cul-de-sac has no room for its name. Dropping the label leaves the feature unlabelled. Forcing the label on causes it to overlap the neighbouring street name. Displacing it into free space with a thin leader line keeps both labels readable and the association clear.",[44,1051],{"x":46,"y":46,"width":47,"height":1052,"fill":49},"244",[51,1054,1055],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"A leader line beats both dropping and overlapping",[44,1057],{"x":61,"y":62,"width":63,"height":681,"rx":65,"fill":66,"stroke":67,"style":68},[51,1059,1060],{"x":71,"y":72,"style":73,"fill":91,"textAnchor":57},"dropped",[76,1062],{"d":1063,"fill":79,"stroke":80,"style":81},"M40 170 H228",[76,1065],{"d":1066,"fill":79,"stroke":80,"style":81},"M120 170 V120",[51,1068,1069],{"x":71,"y":162,"style":85,"fill":86,"textAnchor":57},"High Street",[51,1071,1073],{"x":71,"y":1072,"style":85,"fill":91,"textAnchor":57},"196","the stub has no name",[44,1075],{"x":48,"y":62,"width":63,"height":681,"rx":65,"fill":66,"stroke":67,"style":68},[51,1077,1078],{"x":97,"y":72,"style":73,"fill":692,"textAnchor":57},"forced",[76,1080],{"d":1081,"fill":79,"stroke":80,"style":81},"M288 170 H476",[76,1083],{"d":1084,"fill":79,"stroke":80,"style":81},"M368 170 V120",[51,1086,1069],{"x":97,"y":162,"style":85,"fill":86,"textAnchor":57},[51,1088,1090],{"x":97,"y":1089,"style":85,"fill":692,"textAnchor":57},"120","Mill Close",[51,1092,1093],{"x":97,"y":1072,"style":85,"fill":692,"textAnchor":57},"two labels, one space",[44,1095],{"x":110,"y":62,"width":111,"height":681,"rx":65,"fill":112,"stroke":113,"style":114},[51,1097,1098],{"x":117,"y":72,"style":73,"fill":113,"textAnchor":57},"callout",[76,1100],{"d":1101,"fill":79,"stroke":80,"style":81},"M536 170 H720",[76,1103],{"d":1104,"fill":79,"stroke":80,"style":81},"M614 170 V120",[51,1106,1069],{"x":1107,"y":162,"style":85,"fill":86,"textAnchor":57},"600",[212,1109],{"x1":1110,"y1":1111,"x2":1112,"y2":1113,"stroke":67,"style":675},"620","140","676","152",[51,1115,1090],{"x":1116,"y":140,"style":85,"fill":86,"textAnchor":57},"700",[51,1118,1119],{"x":117,"y":1072,"style":85,"fill":113,"textAnchor":57},"both readable, link explicit",[174,1121,1123],{"id":1122},"qgis-version-compatibility","QGIS version compatibility",[14,1125,1126,1127,1129],{},"The examples target ",[185,1128,187],{}," (Python 3.12).",[1131,1132,1133,1149],"table",{},[1134,1135,1136],"thead",{},[1137,1138,1139,1143,1146],"tr",{},[1140,1141,1142],"th",{},"QGIS version",[1140,1144,1145],{},"Python",[1140,1147,1148],{},"Notes",[1150,1151,1152,1171,1181,1192],"tbody",{},[1137,1153,1154,1158,1161],{},[1155,1156,1157],"td",{},"3.22 LTR",[1155,1159,1160],{},"3.9",[1155,1162,1163,1166,1167,1170],{},[207,1164,1165],{},"lineSettings()"," available; placement flags moved here from ",[207,1168,1169],{},"QgsPalLayerSettings"," in 3.16.",[1137,1172,1173,1176,1178],{},[1155,1174,1175],{},"3.28 LTR",[1155,1177,1160],{},[1155,1179,1180],{},"Behaviour matches this page.",[1137,1182,1183,1186,1189],{},[1155,1184,1185],{},"3.34 LTR",[1155,1187,1188],{},"3.12",[1155,1190,1191],{},"Baseline for this page.",[1137,1193,1194,1197,1199],{},[1155,1195,1196],{},"3.40 \u002F 3.44",[1155,1198,1188],{},[1155,1200,1201,1204],{},[207,1202,1203],{},"QgsPalLayerSettings.Placement"," members are scoped; adds allow-degraded-placement options for dense networks.",[174,1206,1208],{"id":1207},"troubleshooting","Troubleshooting",[179,1210,1211,1220,1226,1234,1242,1251],{},[182,1212,1213,432,1216,1219],{},[185,1214,1215],{},"No labels appear at all.",[207,1217,1218],{},"setLabelsEnabled(True)"," was not called, or the field contains nulls. Both are silent.",[182,1221,1222,1225],{},[185,1223,1224],{},"Labels are straight rather than curved."," The layer is not a line layer, or the placement was set on a copy of the settings object that was never assigned back.",[182,1227,1228,1231,1232,907],{},[185,1229,1230],{},"Only a few features are labelled."," Collisions. Raise the priority, allow repeats, reduce the font size, or diagnose with ",[207,1233,832],{},[182,1235,1236,432,1239,1241],{},[185,1237,1238],{},"Text follows the line but is upside down.",[207,1240,546],{}," is not set, so labels follow the direction the line was digitised in.",[182,1243,1244,1247,1248,1250],{},[185,1245,1246],{},"Labels sit on top of the line."," Increase ",[207,1249,542],{},", and confirm the unit is millimetres rather than map units.",[182,1252,1253,1256,1257,907],{},[185,1254,1255],{},"The halo looks blocky at print resolution."," The buffer size is in map units and scaled up with the export DPI. Use render millimetres for anything printed — see ",[21,1258,1260],{"href":1259},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002F","Export the Map Canvas to an Image",[174,1262,1264],{"id":1263},"conclusion","Conclusion",[14,1266,1267,1268,1271],{},"Curved line labels are ",[207,1269,1270],{},"placement = Curved"," with sensible character-angle limits, a small offset off the line, repeats measured in page millimetres, and a sub-millimetre buffer for legibility. When labels go missing, the cause is the collision engine rather than the settings — adjust priority and obstacles rather than forcing every label to display.",[174,1273,1275],{"id":1274},"frequently-asked-questions","Frequently Asked Questions",[14,1277,1278,1281],{},[185,1279,1280],{},"Why do some labels still not appear at high zoom?","\nThe engine needs a run of line long enough to fit the text within the angle limits. A short or very sinuous segment may have no valid position at all.",[14,1283,1284,1287],{},[185,1285,1286],{},"Can I label both sides of a line?","\nNot with one label. Use two labelling rules with different offsets and filters, or a single label with an expression combining both values.",[14,1289,1290,1293,1294,1296],{},[185,1291,1292],{},"How do I stop labels repeating too often?","\nRaise ",[207,1295,538],{},". Setting it to zero disables repetition entirely and gives one label per feature.",[14,1298,1299,1302,1303,1306],{},[185,1300,1301],{},"Does curved placement work on polygon outlines?","\nNot directly. Convert the polygons to lines with ",[207,1304,1305],{},"native:polygonstolines"," and label that layer, keeping the polygons for fill.",[14,1308,1309,1312,1313,907],{},[185,1310,1311],{},"How do I match the label font to a print layout?","\nSet the font size in points and the buffer in millimetres, then export through a layout rather than the canvas, so the page units are honoured — see ",[21,1314,1316],{"href":1315},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","Automated Map Layout Generation",[174,1318,1320],{"id":1319},"related","Related",[179,1322,1323,1328,1332,1337,1343],{},[182,1324,1325,1327],{},[21,1326,24],{"href":23}," — the guide this recipe belongs to",[182,1329,1330],{},[21,1331,901],{"href":900},[182,1333,1334],{},[21,1335,1336],{"href":905},"Create a Rule-Based Renderer in PyQGIS",[182,1338,1339],{},[21,1340,1342],{"href":1341},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis\u002F","Data-Defined Symbol Size Expression in PyQGIS",[182,1344,1345],{},[21,1346,1347],{"href":1259},"Export the Map Canvas to an Image in PyQGIS",[1349,1350,1351],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":205,"searchDepth":230,"depth":230,"links":1353},[1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365],{"id":176,"depth":230,"text":177},{"id":197,"depth":230,"text":198},{"id":450,"depth":230,"text":451},{"id":550,"depth":230,"text":551},{"id":744,"depth":230,"text":745},{"id":840,"depth":230,"text":841},{"id":910,"depth":230,"text":911},{"id":1122,"depth":230,"text":1123},{"id":1207,"depth":230,"text":1208},{"id":1263,"depth":230,"text":1264},{"id":1274,"depth":230,"text":1275},{"id":1319,"depth":230,"text":1320},"Configure QgsPalLayerSettings for line labels — curved placement, offsets and repeats, buffers and halos, and the priority and obstacle settings that decide which labels survive.","md",{"slug":1369,"type":1370,"breadcrumb":1371,"datePublished":1372,"dateModified":1372},"curved-labels-along-lines-pyqgis","article","Curved Line Labels","2026-08-10","\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcurved-labels-along-lines-pyqgis",{"title":5,"description":1366},"pyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcurved-labels-along-lines-pyqgis\u002Findex","OOWDviPSYz08fEWNKiTKfLUlwYGOxwoB73kt7PF8Mxw",1786401337548]