AllanCameron / geomtextpath

Create curved text paths in ggplot2
https://allancameron.github.io/geomtextpath
Other
625 stars 24 forks source link

Text smoothing #45

Closed AllanCameron closed 2 years ago

AllanCameron commented 2 years ago

Before attempting to introduce automatic text smoothing, we need some idea of when and how to smooth a path. There are two situations I can think of where adhering too closely to a path brings ugly results.

One is where the line is overly geometric, producing corners that makes the text appear to separate:

library(geomtextpath)
#> Loading required package: ggplot2

df <- data.frame(x = 1:5, y = c(0, 5, 5, 0, 5), 
                 z = "A long text label for demonstration purposes")

p <- ggplot(df, aes(x, y)) + geom_line() + lims(y = c(0, 6))

p + geom_textline(aes(label = z), size = 7, hjust = 0.25, 
                  vjust = -0.5, text_only = TRUE)

I have written an algorithm that attempts to smooth such corners using quadratic Bezier curves. It looks like this:

df2 <- geomtextpath:::smooth_corners(df$x, df$y, radius = 0.25)

df2 <- setNames(as.data.frame(df2), c("x", "y"))
df2$z <- "A long text label for demonstration purposes"

p + 
  geom_textline(aes(label = z), data = df2, size = 7, 
                hjust = 0.25, vjust = -0.5, linecolor = "red") 

You will notice that it needs a radius parameter. It is possible that setting it to a reasonable value internally could avoid extra parameter passing to the grob.

The other type of problematic path is the "noisy" path, as in the economics example. There are several ways to handle this (including the rolling mean), but I have again come up with another algorithm that smooths by finding the centre of mass of regularly spaced chunks of the path (say 50) then creating splines to join the dots:

library(geomtextpath)
#> Loading required package: ggplot2

plot(economics$unemploy, type = "l")
df <- geomtextpath:::smooth_noisy(seq(nrow(economics)), economics$unemploy)
df <- setNames(df, c("x", "y"))
lines(df, col = "red")

This function also takes a parameter, samples which is just the number of points on which the path is sampled. However, this seems to work pretty well with 50 - 100 samples for noisy paths, and again we might not need to expose this parameter.

The latest commit includes these functions, both of which return a two-column x, y matrix.

There may be problems that you see with these approaches, or other problematic paths I haven't considered, so let me know what you think before I look at applying these in the makeContent code.

Created on 2021-12-22 by the reprex package (v2.0.1)

teunbrand commented 2 years ago

Nice work Alan! These look good to me: the bezier solution seems nice for underdefined paths and the spline solution seems nice for overdefined paths.

But I know from the blogpost I recently mentioned that people dislike it if their text intersects the path. It might therefore be nice if, for example, the radius of the bezier curve doesn't exceed the first true offset distance. I think we also mentioned some convex/concave hull-type of thing to specifically mitigate text/path intersections, but I hadn't the time to explore the options in this sense.

teunbrand commented 2 years ago

Sorry I just recalled something. If this is primarily for the sf variant due to it having no access to smoothing stats, is this something we could do before makeContent?

AllanCameron commented 2 years ago

I really think we need a general smoother for text. The blog post you linked was a good example - I think that most paths from real-world examples will need text smoothing. Line plots are generally either geometric or noisy, with only a relatively small subset being smooth.

I'm pretty sure if I was an end-user coming across this package, I would expect it to be easy to smooth the text label on an arbitrary path without running gam or similar - at most setting some smoothing parameter.

teunbrand commented 2 years ago

Yes, I agree with you. My point wasn't about whether we need it, it seems very useful, my question was about whether we'd need to repeat the smoothing every time the window is resized.

AllanCameron commented 2 years ago

Oh, I see what you mean. I can't think of any reason why it couldn't be done beforehand, which would obviously be preferable from the performance point of view.

AllanCameron commented 2 years ago

The latest commit includes a version of text smoothing that works fairly well. It requires a copy of the smoothed x, y co-ordinates to be passed along with the original path into the makeContent functions. The smoothing algorithms try to keep track of the mapping between the original and the smoothed path to get the line gaps right. At the moment, this works fairly well, but the "noisy smoother" isn't quite perfect in its ability to map when the smoothing is high, However, the mechanism is now in place and the algorithm is located in a single small function that can be tweaked as needed.

Current behaviour is as follows:

library(geomtextpath)
#> Loading required package: ggplot2

df <- data.frame(x = 1:5, y = c(1, 3, 3, 1, 5))

p <- ggplot(df, aes(x, y, label = "A reasonably long text label"))

p + geom_textpath(size = 5, hjust = 0.24)

p + geom_textpath(size = 5, hjust = 0.24, text_smoothing = 90)

p + geom_textpath(size = 5, hjust = 0.24, vjust = -1.5)

p + geom_textpath(size = 5, hjust = 0.24, vjust = -1.5, text_smoothing = 90)

p + geom_labelpath(size = 5, hjust = 0.24, vjust = -1.5)

p + geom_labelpath(size = 5, hjust = 0.24, vjust = -1.5, text_smoothing = 90)

The noisy path smoother also works fairly well, and I have moved the economics example to a brief new section in the readme to demonstrate.

I have also put it to practical use to improve the look of the geom_textsf and richtext examples.

I will keep this issue open until I am happier with the algorithm, and you've had a chance to review the changes @teunbrand

Created on 2021-12-31 by the reprex package (v2.0.1)

teunbrand commented 2 years ago

This looks pretty good already! Nice work Alan!

At the moment, this works fairly well, but the "noisy smoother" isn't quite perfect

Without smoothing, the point where the path is cut isn't very intuitive either (notice the 'e' intersecting the path for example).

library(geomtextpath)
#> Loading required package: ggplot2

ggplot(economics, aes(date, unemploy)) +
  geom_textpath(label = "Decline", vjust = 1)

With some smoothing, it already looks much better.

ggplot(economics, aes(date, unemploy)) +
  geom_textpath(label = "Decline", vjust = 1, text_smoothing = 50)

Created on 2022-01-02 by the reprex package (v2.0.1)

I'm wondering whether padding = unit(0.15, "inch") isn't setting too wide a margin. Do you think we should set the default somewhat smaller? I think using unit(5, "pt") or unit(0.05, "inch") tends to look better in most cases.

AllanCameron commented 2 years ago

Do you think we should set the default somewhat smaller?

I'm happy to have a smaller gap - it does look a bit large to me, especially at default text sizes