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.
- Measure — every label’s box is sized from its rendered extent via TextMeasure.jl, render-free (no
Sceneis allocated; plain strings, LaTeX, and Makie rich text are all supported). Placement works from real glyph metrics — the same true-extent approachggrepelandadjustTexttake — so overlap removal and marker clearance run against the actual text box. - 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.
- 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


