# \[ANN\] MakieTextRepel.jl and TextMeasure.jl

**URL:** <https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810>\
**Category:** Package Announcements\
**Tags:** plotting, makie, animations\
**Created:** [June 26, 2026, 2:18am UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810 "2026-06-26T02:18:47Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![jowch](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jowch/32/222240_2.png) [@jowch](https://discourse.julialang.org/u/jowch)\
**Post date:** [June 26, 2026, 2:18am UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/1 "2026-06-26T02:18:47Z")

</div>

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.

 ![Left: plain text! labels collide. Middle: textrepel! separates them and draws connectors back to each point. Right: the same solver driving Makie.annotation! through TextRepelAlgorithm.](https://global.discourse-cdn.com/julialang/original/3X/3/f/3ff28a8affb8f398dd451e576125fdefa298ffe1.png)

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:

```julia-auto
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.

```julia-auto
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

```

---

<div class="post-metadata">

**Author:** ![jowch](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jowch/32/222240_2.png) [@jowch](https://discourse.julialang.org/u/jowch)\
**Post date:** [June 26, 2026, 2:22am UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/2 "2026-06-26T02:22:22Z")

</div>

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.](https://global.discourse-cdn.com/julialang/original/3X/9/1/9197e963265dc00a7fe88996c350cd6968651141.gif)

---

<div class="post-metadata">

**Author:** ![jowch](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jowch/32/222240_2.png) [@jowch](https://discourse.julialang.org/u/jowch)\
**Post date:** [June 26, 2026, 2:23am UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/3 "2026-06-26T02:23:31Z")

</div>

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.](https://global.discourse-cdn.com/julialang/original/3X/7/1/71837c28575b0d5068405e78c221a9f863c8d3d8.gif)

For more information about MakieTextLayout.jl, I invite you to take a look at the [README.md](https://github.com/jowch/MakieTextRepel.jl/tree/main#makietextrepeljl) and the [algorithm.md](https://github.com/jowch/MakieTextRepel.jl/blob/main/docs/algorithm.md)

For more information about TextMeasure.jl and higher resolution examples, see the [README.md](https://github.com/jowch/TextMeasure.jl#textmeasurejl). For details on how to make the examples, see [the examples gallery](https://github.com/jowch/TextMeasure.jl/tree/main/examples).

Have a nice day!

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [June 26, 2026, 2:27am UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/4 "2026-06-26T02:27:13Z")

</div>

> [@jowch](#):
>
> **TextMeasure.jl**  
> A backend-agnostic text **layout engine**

Interesting to see this, on the face of it this overlaps a bunch with [https://code.tecosaur.net/tec/Reflow.jl](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!

---

<div class="post-metadata">

**Author:** ![jowch](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jowch/32/222240_2.png) [@jowch](https://discourse.julialang.org/u/jowch)\
**Post date:** [June 26, 2026, 2:32am UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/5 "2026-06-26T02:32:54Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![juliohm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliohm/32/215266_2.png) [@juliohm](https://discourse.julialang.org/u/juliohm)\
**Post date:** [June 26, 2026, 1:41pm UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/6 "2026-06-26T13:41:20Z")

</div>

That is wonderful! :juliaheartpulsing:

> [@jowch](#):
>
> **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.

Isn’t that what `Makie.annotation` does these days? Is it a different algorithm?

Very nice examples! 👏

---

<div class="post-metadata">

**Author:** ![jowch](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jowch/32/222240_2.png) [@jowch](https://discourse.julialang.org/u/jowch)\
**Post date:** [June 26, 2026, 8:29pm UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/7 "2026-06-26T20:29:47Z")

</div>

Yes, `annotation!` and `textrepel!` are both solving the same problem. The algorithms are different and from my research, `annotation!` was designed to make it easy to use custom solvers. You can use the `TextRepelAlgorithm()` with `annotation!` if you’d like. From my understanding of both algorithms, the algorithm here is more uncompromising about overlaps. I use it when the underlying data has a lot of data points packed closely together and when I don’t want to deal with overlaps at all (e.g., during data exploration and prototyping).

---

<div class="post-metadata">

**Author:** ![jules](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@jules](https://discourse.julialang.org/u/jules)\
**Post date:** [June 27, 2026, 4:32pm UTC](https://discourse.julialang.org/t/ann-makietextrepel-jl-and-textmeasure-jl/137810/8 "2026-06-27T16:32:49Z")

</div>

That’s cool, if you think your algorithm is better we could also think about replacing Makie’s default with it. Changing the exact positions of text does not count as breaking as it’s kind of unpredictable anyway.
