QGIS Labeling – How to Create Dynamic Leader Lines

coordinatesdynamic-layerlabelingpostgisqgis

I'm trying to create dynamic leader lines by using a PostGIS view in addition to the QGIS „Move Label“ tool.

CREATE VIEW leader_line AS
SELECT
gid,
ST_MakeLine(geom, ST_SetSRID(ST_MakePoint(xcord_label, ycord_label), SRID))::geometry(linestring, SRID) AS geom
FROM point
WHERE xcord_label IS NOT NULL;

This works fine for all labels WHERE ST_X(geom) < xcord_label but creates wrong looking leader lines for labels WHERE ST_X(geom) > xcord_label.

enter image description here
enter image description here

Does anybody know how to get properly placed leader lines for labels WHERE ST_X(geom) > xcord_label? Is there any way to refer to the xmax coordinate of the labels?

enter image description here

Best Answer

You can use QGIS' quadrant placement specifier determined from the line's azimuth to place a better label. The quadrant specifies 8 positions around a point:

[ 0=Above Left | 1=Above | 2=Above Right |
  3=Left       | 4=Over  | 5=Right       |
  6=Below Left | 7=Below | 8=Below Right ]

Here's an example around Null Island, creating a table and two views.

CREATE TABLE points (
  gid serial PRIMARY KEY,
  geom geometry(Point, 4326),
  label_geom geometry(Point, 4326),
  label text
);

INSERT INTO points(geom, label_geom, label)
SELECT origin, pt, round(degrees(ST_Azimuth(origin, pt))) || ' degrees'
FROM (
  SELECT
    ST_SetSRID(ST_MakePoint(0, 0), 4326) AS origin,
    ST_SetSRID(ST_MakePoint(cos(radians(x)), sin(radians(x))), 4326) AS pt
  FROM generate_series(0, 350, 15) AS x
) AS f;

CREATE OR REPLACE VIEW point_labels AS
  SELECT gid, label_geom AS geom,
  CASE
    WHEN ST_Azimuth(geom, label_geom) ISNULL THEN 2 -- default if azimuth cannot be determined
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 22.5 THEN 1 -- Above
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 67.5 THEN 2 -- Above Right
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 112.5 THEN 5 -- Right
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 157.5 THEN 8 -- Below Right
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 202.5 THEN 7 -- Below
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 247.5 THEN 6 -- Below Left
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 292.5 THEN 3 -- Left
    WHEN degrees(ST_Azimuth(geom, label_geom)) < 337.5 THEN 0 -- Above Left
    ELSE 1 -- >= 337.5 Above
  END AS quadrant, label
  FROM points;

CREATE OR REPLACE VIEW leader_line AS
  SELECT gid, ST_MakeLine(geom, label_geom)::geometry(LineString, 4326) AS geom, label
  FROM points;

Then in QGIS, add:

  • pointsgeom
  • leader_linegeom – primary key needs to be gid
  • point_labelsgeom – primary key needs to be gid

QGIS

Now configure the layer properties for point_labels:

  • Change style so the point is not drawn, e.g., change size to 0.0
  • Label this layer with label, and change placement to "Offset from point", modifying the "Quadrant" to use the attribute field quadrant

quadrant

Bingo!

Bingo

Note that a slightly different approach is required for geography types, since ST_Azimuth behaves differently.


Update: When adding new points to the points layer, the geom field is updated as usual, but the label_geom is not. To populate a default value of label_geom with new points, a trigger needs to be created. But if a trigger function is used, the quadrant specifier can be stored in the points table and the point_labels view can be ignored:

For example, let's start again with a slightly different example with one table and one view:

-- DROP TABLE points CASCADE;
CREATE TABLE points (
  gid serial PRIMARY KEY,
  geom geometry(Point, 4326),
  label_geom geometry(Point, 4326),
  quadrant integer,
  label text
);

CREATE FUNCTION label_geom_tg_fn() RETURNS trigger AS
$BODY$
DECLARE
  azimuth float8;
BEGIN
  -- Set a default label_geom
  IF NEW.label_geom ISNULL THEN
    NEW.label_geom := NEW.geom;
  END IF;
  -- Determine quadrant
  azimuth := degrees(ST_Azimuth(NEW.geom, NEW.label_geom));
  NEW.quadrant := CASE
    WHEN azimuth ISNULL THEN 2 -- azimuth cannot be determined, so put Above Right
    WHEN azimuth < 22.5 THEN 1 -- Above
    WHEN azimuth < 67.5 THEN 2 -- Above Right
    WHEN azimuth < 112.5 THEN 5 -- Right
    WHEN azimuth < 157.5 THEN 8 -- Below Right
    WHEN azimuth < 202.5 THEN 7 -- Below
    WHEN azimuth < 247.5 THEN 6 -- Below Left
    WHEN azimuth < 292.5 THEN 3 -- Left
    WHEN azimuth < 337.5 THEN 0 -- Above Left
    ELSE 1 END;-- >= 337.5 Above
  RETURN NEW;
END;$BODY$ LANGUAGE plpgsql;

CREATE TRIGGER label_geom_tg BEFORE INSERT OR UPDATE
   ON points FOR EACH ROW
   EXECUTE PROCEDURE label_geom_tg_fn();

The from the first example, re-do the INSERT INTO points and CREATE OR REPLACE VIEW leader_line statements, as these do not require modification. But ignore the leader_line view.

Then in QGIS, add:

  • pointsgeom
  • pointslabel_geom
  • leader_linegeom – primary key needs to be gid

Now configure the layer properties for points with label_geom as the first example did for point_labels. The quadrant specifier will be modified automatically for new and moved points, but you will only notice these changes each time you save your edits.