Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text smoothing #45

Closed
AllanCameron opened this issue Dec 22, 2021 · 8 comments
Closed

Text smoothing #45

AllanCameron opened this issue Dec 22, 2021 · 8 comments

Comments

@AllanCameron
Copy link
Owner

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
Copy link
Collaborator

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
Copy link
Collaborator

teunbrand commented Dec 22, 2021

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
Copy link
Owner Author

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
Copy link
Collaborator

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
Copy link
Owner Author

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
Copy link
Owner Author

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
Copy link
Collaborator

teunbrand commented Jan 2, 2022

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
Copy link
Owner Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants