[ANN] MakieTextRepel.jl and TextMeasure.jl

Hi everyone! I made a pair of packages that provide some helpful utilities when it comes to adding text labels to your Makie plots and for calculating the sizes of and laying out text.

MakieTextRepel.jl
A ggrepel/adjustText-style label-repel recipe for Makie. Automatically displaces overlapping text labels and draws connector lines back to their data points.

The objective is to pack as many labels as requested into the axis frame as possible without label overlaps and crossed leader lines and, otherwise, drop the ones that can’t fit. The way the algorithm works is a bit different than the force simulation based solvers from ggrepel and adjustText.

  1. Measure — every label’s box is sized from its rendered extent via TextMeasure.jl, render-free (no Scene is allocated; plain strings, LaTeX, and Makie rich text are all supported). Placement works from real glyph metrics — the same true-extent approach ggrepel and adjustText take — so overlap removal and marker clearance run against the actual text box.
  2. Solve — a deterministic, zero-overlap projection solver in pixel space: discrete side-selection (which side of each point its label takes) → crossing repair → Dykstra constraint-projection legalization, with every data anchor treated as a keep-out so labels never sit under their markers.
  3. Render — text + optional boxes + connectors.

Here’s an example of what the code to use it looks like:

using CairoMakie, MakieTextRepel

fig = Figure()
ax = Axis(fig[1, 1])
points = Point2f[(1, 1), (1.1, 1.05), (1.05, 0.9)]

# Draw the labels first, then scatter on top, so the connector lines tuck under the
# markers. Pass `markersize` so the labels keep clear of the markers.
textrepel!(ax, points; text = ["alpha", "beta", "gamma"], markersize = 9)

# or via Makie.annotation!
# annotation!(ax, points; text = ["alpha", "beta", "gamma"],
#     algorithm = TextRepelAlgorithm())

scatter!(ax, points; markersize = 9)

fig

I expect that in the majority of cases where the data points are reasonably spaced, the both textrepel! and annotations! will give similarly satisfying results. If your points are very dense and have a lot of overlap, try textrepel!.

TextMeasure.jl
A backend-agnostic text layout engine: measure once, lay out many times. Inspired by pretext.js, using FreeType/Makie rather than canvas.

using TextMeasure
using FreeTypeAbstraction                # enables FreeTypeBackend

b   = FreeTypeBackend(; font="DejaVu Sans", fontsize=14)
prp = prepare(b, "The quick brown fox")  # measures once (touches the font engine)
lay = layout(prp; max_width=120, align=:left)   # pure arithmetic — call freely

lay.size                                  # (width, height) in px
for ln in lay.lines
    @show ln.str, ln.x, line_top(lay, ln) # top-left placement, block-top = 0
end

I wasn’t able to add links or more than one image in my previous post so here are some more.

You can use TextMeasure.jl measured text sizes to calculate how to pack text into arbitrary shapes with some additional effort.

A wavy coral tide-line kneads a justified prose block — each frame the engine re-flows the prose into whatever region the wave leaves behind.

Here is an example using TextMeasure.jl with MakieTextRepel.jl.

A seamless zoom-dive over the California Central Coast; every place-label is measured here and placed collision-free by MakieTextRepel, live, on every frame as the camera descends.

For more information about MakieTextLayout.jl, I invite you to take a look at the README.md and the algorithm.md

For more information about TextMeasure.jl and higher resolution examples, see the README.md. For details on how to make the examples, see the examples gallery.

Have a nice day!

Interesting to see this, on the face of it this overlaps a bunch with https://code.tecosaur.net/tec/Reflow.jl.

My library is just missing custom text measuring to support variable pitch fonts/arbitrary input, but I didn’t want to go down the glue rabbit hole.

—–

p.s. Cool examples!

Very cool! I’ll take a look. Thanks for checking this out.