# Callout Labels with more lines

I have an issue with the Labeling in QGIS. I want to label lines, that are connected to each other, but have different attributes and so also different labels. The lines QGIS draws from the label to the line only marks one place of the line. In my case, this not very helpful because I can't see, where the start and the end of the line is.

I would like to have like two separate lines from the Label, which go to the start- and endpoint of the Line, so the visibility of the line segment, which is labeled gets improved.

The Labels should look like in the Picture above. This from AutoCAD.

• Easiest probably to create start- and end point of the lines. Commented May 30, 2023 at 21:42
• Including a screenshot of the current situation and a mock-up of what you want to achieve would clarify your question immensely.
– Matt
Commented May 31, 2023 at 6:07
• I Added a Picture to clarify.
– Toby
Commented May 31, 2023 at 14:52

## UPDATE:

Please look at my newer improved solution, which works in any Layout map window, regardless of map canvas visibility.

As @Tom already mentioned, we can solve the task with the Geometry Generator. Only in this case we need an additional Python expression function to solve the main part of the task: `determining the label boundary`.

I will explain the whole solution, but first let me show you the final result:

And here are the necessary steps:

1.) Add the Python code with the Function Editor of the Expression Dialog (tested with QGIS 3.30.1):

``````from qgis.core import *
from qgis.gui import *
from qgis.utils import iface
import math

def midpt(pt1, pt2):
x = (pt1.x() + pt2.x())/2
y = (pt1.y() + pt2.y())/2
return QgsPointXY(x,y)

def createRelPoint(point, dist, azim):
point_x, point_y = point.x(), point.y()
x = point_x + dist * math.cos(azim_rad)
y = point_y + dist * math.sin(azim_rad)
return QgsPointXY(x, y)

@qgsfunction(args='auto', group='Custom', usesGeometry=True, referenced_columns=[])
def getLabelLeaders(x, y, handle_width, feature, parent):
center=QgsPointXY(x,y)
geom = feature.geometry().asPolyline()
startPt = geom[0]
endPt = geom[-1]
buffer=1
rec=QgsRectangle.fromCenterAndSize(center,buffer,buffer)
labels=iface.mapCanvas().labelingResults ().labelsWithinRect(rec)
#labels=iface.mapCanvas().labelingResults().labelsAtPosition(center)
if len(labels)>0:
polygon=labels[0].labelGeometry.asPolygon()[0]
p1 = midpt(polygon[0],polygon[3])
p2 = midpt(polygon[1],polygon[2])
if p1.distance(startPt) > p2.distance(startPt):
p1,p2 = p2,p1

azim = p1.azimuth(p2)
p1 = createRelPoint(p1,-handle_width,90-azim)
p2 = createRelPoint(p2,handle_width,90-azim)

l1 = QgsGeometry.fromPolylineXY([startPt,p1,center])
l2 = QgsGeometry.fromPolylineXY([endPt,p2,center])
mline = QgsGeometry.collectGeometry([l1,l2])
return mline

``````

2.) Create and configure your labels:

My example uses a `Single Label` value: ` "strasse" ||'\n'||"str_name"||'\nLänge: '|| round("measurelength",0)||'km'` with a rectangular background (all dimension values are in `Map Units` to prevent label resizing).

Move one label to activate the `Auxiliary Storage` mechanism. You can rotate your labels as well. After that open the label Placement tab and configure the label Alignment: `horizontal='Center', vertical='Half'`. Set Priority to High. On the Rendering tab I'm setting Overlapping Mode to `Allow Overlaps without Penalty`.

`getLabelLeaders("auxiliary_storage_labeling_positionx","auxiliary_storage_labeling_positiony",300)`

The third parameter of the `getLabelLeaders` function defines the length of the short leader segments (I call it label handles).

Only labels that are moved will show the custom leaders.

The solution is not perfect, because sometimes label boundaries are not exact, but in most cases it should work as expected. If you don't see the leaders in your Print Layout, you have to zoom to the labels in Map Canvas and refresh the Layout afterwards (the expression needs Map Canvas to fetch the label boundaries).

BTW: it took me some hours to solve this task, but it was a lot of fun!

Enjoy, Christoph

• I even tried to solve the task with Callout Lines, but had an issue with the Geometry Generator (see github.com/qgis/QGIS/issues/53377). Commented Jun 6, 2023 at 12:52
• Hi Christoph, sorry for my late response, but i was on holidays. I tried your solution today, and followed all the steps, but the lines dont show up in my project. Can it be an issue with the map canvas you mention at the end? I'm pretty new to qgis, and dont know anything about it.
– Toby
Commented Jun 12, 2023 at 9:17
• Hi Toby, never mind. You need a projected coordinate system like Gauss Krüger. Commented Jun 12, 2023 at 12:57
• Hi Christoph. I Just found the mistake. I Didn't delete the standart part in the Geometry generator. Now it works perfectly fine. Thanks for that nice work. If can appreciate your work in some way let me know.
– Toby
Commented Jun 12, 2023 at 13:04
• @Toby maybe it's easier to create an AutoLisp Routine for labeling in AutoCAD/BricsCAD and export the generated labels to QGIS. Commented Jun 20, 2023 at 15:35

You could use Rule-based Labeling, with Callouts and Geometry Generator to create the lines to the start and end points. I'm not convinced that the visibility is improved, but maybe it will for your data set. You may need to play around with the position of the labels.

1. Turn on Rule-based Labeling and create two rules

1. Enable Callouts

1. Use Geometry Generator to create the Start Point (`start_point(\$geometry)`). Similar for the other rule (End Point -> `end_point(\$geometry)`). Set the distance to some way away from the point eg 20 mm

Result on a small sample data set - a callout line to each end of the actual lines:

• Thanks for that quick answer, but this isn't what I was looking for. I need Something where the label is in the middle an the lines go from one label to start an endpoint. But Maybe I could play alittle with what you gave me. I'll try.
– Toby
Commented May 31, 2023 at 14:44

A year has passed and I've learned a lot, which has helped to massively improve the code for solving this issue.

The new Python expression function `getLabelRect` now also works in every layout map item, regardless of the map canvas. It uses the `context` parameter to retrieve the bounding boxes of all visible labels.

``````from qgis.core import *
from qgis.gui import *
from qgis.utils import iface

@qgsfunction(args='auto', group='Custom', usesGeometry=False, referenced_columns=[])
def getLabelRect(feature, parent, context):
mapItemId = context.variable('map_id')
if mapItemId == 'canvas':
labelResults=iface.mapCanvas().labelingResults()
else:
layoutName = context.variable('layout_name')
map = QgsProject.instance().layoutManager().layoutByName(layoutName).itemById(mapItemId)
labelResults = map.previewLabelingResults()

for label in labelResults.allLabels():
if label.featureId == feature.id():
return label.labelGeometry

return
``````

For showing the labels with leaders and a box around the labels, I had to style the layer `labels` with 3 geometry generators.

The topmost geometry generator - with type "Polygon" - draws a polygon around the labels:

``````buffer(getLabelRect(),100,join:='miter')
``````

The geometry generator below - with type "LineString" - draws the left leader. The layer column `label_leader_pos` is used to define the anchor position of the left and right leader ('ll','lr','rr','tt','tb','bb','lb',...). If the column is NULL the default is 'lr'.

``````with_variable('box', buffer(getLabelRect(),100,join:='miter'),
with_variable('cent', centroid(@box),
with_variable('midpt',
CASE
WHEN @anchor = 'l' THEN centroid( make_line(start_point(@box),point_n(@box,2)))
WHEN @anchor = 'r' THEN centroid( make_line(point_n(@box,3),point_n(@box,4)))
WHEN @anchor = 't' THEN centroid( make_line(point_n(@box,2),point_n(@box,3)))
WHEN @anchor = 'b' THEN centroid( make_line(point_n(@box,1),point_n(@box,4)))
END,
make_line(start_point(@geometry),end_point(extend(make_line(@cent,@midpt),0,100)),@midpt)))))
``````

And the lowest geometry generator draws the right leader:

``````with_variable('box', buffer(getLabelRect(),100,join:='miter'),
with_variable('cent',centroid(@box),
with_variable('midpt',
CASE
WHEN @anchor = 'l' THEN centroid( make_line(start_point(@box),point_n(@box,2)))
WHEN @anchor = 'r' THEN centroid( make_line(point_n(@box,3),point_n(@box,4)))
WHEN @anchor = 't' THEN centroid( make_line(point_n(@box,2),point_n(@box,3)))
WHEN @anchor = 'b' THEN centroid( make_line(point_n(@box,1),point_n(@box,4)))
END,
make_line(@midpt,end_point(extend(make_line(centroid(@box),@midpt),0,100)),end_point(@geometry))))))
``````

To manually move and rotate the labels, we need to store the label coordinates and the rotation angle in additional columns (i.e. `label_x`,`label_y` and `label_rot`). If we want to limit the leaders to labels that are correctly placed, we have to add expressions to enable/disable all geometry generators:

To correctly align the labels, we must set the horizontal Alignment to ' Center' and the vertical Alignment to 'Half'.

To get best results for DXF exports, I set text size to `Map Units` and varied letter spacing and line heights accordingly.

To show the Linestrings in a separate layer `linestrings`, I duplicated the `labels` layer and styled the Linestrings without labels. Doing this with layers coming from the same GeoPackage data source crashed QGIS 3.36 Windows. So, in my working example the `linestring` layer comes from a different data source (i.e. PostGIS).

And here is the result shown in BricsCAD: