Giter Club home page Giter Club logo

Comments (11)

AllanCameron avatar AllanCameron commented on July 24, 2024

Yes, I had a similar thought that I described back in #11, but it was very half - baked. I think the key features would need to be:

  • The path / line itself remains unchanged
  • The text follows the curve's 'centre of mass',
  • The degree of smoothing should be user - controllable

One of the problems is that the anchor points themselves may sit on angles that are not representative of the curve as a whole. This would mean that the angles are wrong at the start and end of the path. It might be more appropriate to perform some kind of regression on the line around the anchor points and in the midpoint to get an accurate angle and x,y placement. This would be complex but not intractable.

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

Having had a think about this, I wonder if we just smooth the path that the text is drawn on with a kernel smoother. Remember we have to handle noisy paths as well as noisy x-ordered lines. So we could have something like this:

library(geomtextpath)
#> Loading required package: ggplot2

set.seed(3)

x <- runif(5)
y <- runif(5)

x <- spline(seq_along(x), x, xout = seq(1, length(x), len = 1000))$y
y <- spline(seq_along(y), y, xout = seq(1, length(y), len = 1000))$y

theta <- geomtextpath:::.angle_from_xy(x, y, norm = TRUE)

offset <- rnorm(1000) * 0.01

df <- data.frame(x = x + offset * cos(c(1, theta)),
                 y = y + offset * sin(c(1, theta)),
                 label = "Path label")

p <- ggplot(df, aes(x, y, label = label)) + 
       geom_textpath(size = 8) 

p

A simple kernel smoother would be something like this (I know this is very inefficient, but it's just for demonstration):

path_smoother <- function(data, width = 0.02) {
  x <- data$x
  y <- data$y
  dist <- geomtextpath:::.arclength_from_xy(x, y)
  sd <- max(dist) * width
  data$x <- sapply(dist, function(i) {
  dn <- dnorm(dist, mean = i, sd = sd)
  sum(x * dn/sum(dn))
  })
  data$y <- sapply(dist, function(i) {
  dn <- dnorm(dist, mean = i, sd = sd)
  sum(y * dn/sum(dn))
  })
  data
}

It does a pretty good job of picking out the signal from the noise:

p + geom_textpath(data = path_smoother(df), 
                  color = "red", size = 8, vjust = 1.5)

And of course it's adjustable with the width parameter.

If you find a more efficient way to do it, that would be great, but this makes a solid (if slow) fallback implementation.

Created on 2021-12-01 by the reprex package (v2.0.0)

from geomtextpath.

teunbrand avatar teunbrand commented on July 24, 2024

Hi Allan,

I'm sorry I've been absent for the past few days, I haven't really had the time to look at / work on new code.
I agree with you on most parts, except for the control over smoothing in the sense of curved smoothing. The reason I don't agree is because I'm of the opinion that if a user wants smoothing, they can just use stat_smooth() or related stats to smoothen out a curve of noisy data. I think we can define some stat_*() functions that do this, but I don't really see the need to have this implemented at the grob level.

Instead, I'm imagining the dumb textpath as something that finds a straight line through a noisy piece of curve and places the text on that straight line. For example, if you imagine the curve is a circle, I'd want to find the tangent line to that circle at some user-defined point. That way, it only needs a single angle calculation per label instead of per letter, which simplifies the problem (and why I've termed it 'dumb', as opposed to the smarter curve calculations we currently have in textpathGrob()). The reason that this would need draw-time / makeContent calculations, is because the text-width would determine the parts of the curve that are considered.

A benefit of the straight-line approach is that we can accommodate plotmath/expressions as you mentioned in the second suggestion in #25

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

Hi Teun

There is no need to apologise at all. This project is just something we can do for enjoyment when we have the time, and when real-life commitments don't get in the way.

I see your point about the straight-line text. This should be fairly easy to do at grob level - I can have a look at this next, since getting plotmath working even in a basic form would be nice. With one eye to the future, it turns out that it isn't too difficult to convert plotmath to svg, so there may be a more sophisticated way to get plotmath to follow curves. Incidentally, it's interesting to see that there is an svg entity called textPath, which works by associating a text string to a path, and looks as though its implemented in a similar way to our textpaths.

In any case, I think it's a good idea to have a flat-but-angled label as a "bail-out" for difficult curves (and difficult labels). This could maybe even be an option for automatically fixing the "offset exceeds curvature" cases.

Does this really need its own geom, or could it be an option in geom_textpath, since the implementation is likely to be inside the textpath Grob?

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

The latest commit includes, as a stepping stone to this idea, a keep_straight parameter. This simply passes the string through measure_exp so that all of the glyphs are treated as a single unit. We will still need to ensure the angle matches the overall trend of the plot, but it should be possible to do that within makeContent,

Current behaviour is:

library(ggplot2)
library(geomtextpath)

p <- ggplot(economics, aes(date, unemploy)) +
  geom_path(colour = "grey")
p + geom_textpath(
    aes(label = "Decline", group = 1),
    hjust = 0.6, size = 5, include_line = FALSE, vjust = 0
  )
#> Warning: The text offset exceeds the curvature in one or more paths. This will result in
#> displaced letters. Consider reducing the vjust or text size, or use the hjust
#> parameter to move the string to a different point on the path.

p + geom_textpath(
    aes(label = "Decline", group = 1),
    hjust = 0.6, size = 5, include_line = FALSE, vjust = 0,
    keep_straight = TRUE
  )

Created on 2021-12-05 by the reprex package (v2.0.0)

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

I have implemented angle smoothing based on a simple lm, so this should work in approximately the same way as subsetting with geom_smooth. It is called automatically if either expressions are used, or if keep_straight = TRUE . It's still a little temperamental, since it only uses one string-length worth of path data for the regression. Still, it seems to work tolerably well for most use cases, and at least the infrastructure is now in place so we can improve the algorithm. The calculations are inside a new function called .project_flat that replaces .project_text when there are expressions or keep_straight = TRUE

To give an idea of how it positions text, I have created a little animation:

library(gganimate)
library(geomtextpath)

anim <- ggplot(within(rbind(economics, economics), 
                   hjust <- rep(c(1, 0), each = 574)),
            aes(date, unemploy, hjust = hjust)) +
  geom_path(colour = "grey") + 
  geom_textpath(
    aes(label = "Decline", group = 1),
    size = 5, include_line = FALSE, vjust = 0, keep_straight = TRUE
  ) +
  transition_states(hjust,
                    transition_length = 30,
                    state_length = 1)

myanim <- animate(anim, fps = 50, duration = 30)

save_animation(myanim, "unemploy.gif")

unemply.gif

unemploy

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

I wasn't happy with the above behaviour. The problem is that the offset path is often inverted (exceeds the curvature). For that reason, I think we should use localized path smoothing. I am happier with the results, since they are much more stable, but they still aren't perfect. I think the approach is correct, though we could perhaps change the specific algorithm:

unemploy

from geomtextpath.

teunbrand avatar teunbrand commented on July 24, 2024

Hi Alan, it looks like you've made some nice progress on this! I agree that lm() might not be the best solution, but from the theoretical consideration that the error is expected to be in one direction (in the dependent variable, i.e. y) and not in the other. I came across this method for polygon offsetting, which is also based on the bisector, but it appears it can handle excess curvature gracefully. I'm not saying this is a neat fit with how we've implemented offsetting now, but it can be food for thought.

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

Thanks Teun - I'll have a look. I was using lm to regress y on t and x on t separately (where t is just seq_along(x)), but I gave up on this approach anyway, since the offset / curvature problem was getting in the way. I was thinking that something like a concave hull might be closer to what we need, but that won't work for self-intersecting paths.

The current method gives a workable (and very user-friendly) solution to your example at the moment, and of course it works a lot better on less jagged lines. If we come up with a better algorithm, at least we know it should slot into the code fairly easily.

By workable, I guess I mean we just need to tweak the hjust to find a reasonable point:

library(ggplot2)
library(geomtextpath)

p <- ggplot(economics, aes(date, unemploy)) +
  geom_path(colour = "grey")

p + geom_textpath(
    aes(label = "Decline", group = 1),
    hjust = 0.53, size = 5, include_line = FALSE, vjust = 0
  )

p + geom_textpath(
    aes(label = "Decline", group = 1),
    hjust = 0.53, size = 5, include_line = FALSE, vjust = 0,
    keep_straight = TRUE
  )

Created on 2021-12-07 by the reprex package (v2.0.0)

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

Is there merit in developing the dumb textpath placement further? If you watch the second animation (current behaviour), it looks as though label placement will be good in most instances, and never looks bad, except maybe at the left edge of the path due to the narrowing. It would be nice if the label rose above the peaks as they went round, but it's not clear whether the behaviour in a narrow valley could be made much better. It seems to me that in either case a decent end result could be achieved by the user tweaking the vjust. Another option is to expose the smoothing bandwidth to the user, by replacing keep_flat with a numeric smooth_text. It would have a default NA that produced curved text, and any numeric value would produce straight text, with its angle determined by the smoothed path obtained by using that number as a bandwidth (currently the bandwidth is fixed)

from geomtextpath.

teunbrand avatar teunbrand commented on July 24, 2024

I haven't gotten around to playing with this yet, but we can close the issue for now as indeed it seems to work to satisfaction. I wouldn't say exposing the bandwidth parameter is necessary because if the user is into that level of customisation, they can simply use stat_smooth() to get whatever they are after.

from geomtextpath.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.