3

I'd like calculate (not plot) 2d spline paths in R. There's an old question on that topic that suggests xspline(): Calculate a 2D spline curve in R

xspline() somewhat works for my purpose, but has important limitations:

  • I cannot customize the number of interpolation points
  • I need to call plot.new(), even if I don't want it to draw anything
  • I only have a single parameter (shape) to customize the spline; I'd like to be able to try a few more different types, if possible

Reproducible example:

library(ggplot2)

# control points
x <- c(.1, .5, .7, .8)
y <- c(.9, .6, .5, .1)

plot.new() # necessary for xspline(); would be great if it could be avoided

# how do I set the number of interpolation points?
# how do I modify the exact path (beyond shape parameter)?
path <- xspline(x, y, shape = 1, draw = FALSE)

# plot path (black) and control points (blue) with ggplot
ggplot(data = NULL, aes(x, y)) +
  geom_point(data = as.data.frame(path), size = 0.5) +
  geom_point(data = data.frame(x, y), size = 2, color = "blue")

Created on 2021-08-14 by the reprex package (v2.0.0)

Are there any easily available alternatives to xspline()?

Claus Wilke
  • 16,992
  • 7
  • 53
  • 104

2 Answers2

4

The xsplinePoints()from the {grid} package allows you to convert a xsplineGrob object to xy coordinates. One solution might be to wrap these functions to return points along an x-spline.

library(grid)
splines <- function(x, y, shape = 1, ..., density = 1) {
  # Density controls number of points, though not the exact number
  xs <- xsplineGrob(x * density, y * density, shape = shape, ...,
                    default.units = "inches")
  # xsplinePoints seem to always return inches
  xy <- xsplinePoints(xs)
  # Drop units
  xy <- lapply(xy, convertUnit, unitTo = "inches", valueOnly = TRUE)
  data.frame(x = xy$x / density,  y = xy$y / density)
}

I presume xsplinePoints() makes some behind the scenes calculation based on the size of the graphics device where smaller devices need less points. The idea behind the density parameter is to let you (indirectly) control how many points are returned by artifically inflating the dimensions before handing the data to grid, and then deflating before returning to the user.

To compare with your example:

library(ggplot2)

# control points
x <- c(.1, .5, .7, .8)
y <- c(.9, .6, .5, .1)

plot.new() 

path <- xspline(x, y, shape = 1, draw = FALSE)

# plot path (black) and control points (blue) with ggplot
ggplot(data = NULL, aes(x, y)) +
  geom_point(data = as.data.frame(path), size = 0.5) +
  geom_point(data = data.frame(x, y), size = 2, color = "blue") +
  # density = 1 (red) and density = 3 (green)
  geom_point(data = splines(x, y), colour = "red") +
  geom_point(data = splines(x, y, density = 3), colour = "green")

Created on 2021-08-14 by the reprex package (v1.0.0)

Claus Wilke
  • 16,992
  • 7
  • 53
  • 104
teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • Thanks! I find it frustrating that these commonly needed computations are hidden in graphics functions instead of being available standalone. – Claus Wilke Aug 14 '21 at 21:52
4

It's not clear from your example, but base R's spline function might meet your needs. We can wrap it in a function to make it easier to use the output:

f <- function(x, y, n, method = "natural") {
  new_x <- seq(min(x), max(x), length.out = n)
  data.frame(x = new_x, y = spline(x, y, n = n, method = method)$y)
}

So the co-ordinates for 10 evenly spaced points along the curve can be obtained like this:

f(x, y, 10)
#>            x         y
#> 1  0.1000000 0.9000000
#> 2  0.1777778 0.8042481
#> 3  0.2555556 0.7173182
#> 4  0.3333333 0.6480324
#> 5  0.4111111 0.6052126
#> 6  0.4888889 0.5976809
#> 7  0.5666667 0.6222222
#> 8  0.6444444 0.6013374
#> 9  0.7222222 0.4303155
#> 10 0.8000000 0.1000000

And we can show the shape of the curve like this:

ggplot(data = NULL, aes(x, y)) +
  geom_point(data = f(x, y, 100), size = 0.5) +
  geom_point(data = data.frame(x, y), size = 2, color = "blue")

You can change the method argument to get different shapes - the options are listed in ?spline

EDIT

To use spline on paths, simply create splines on x and y separately. These can be as a function of another variable t, or this can be left out if you want to assume equal time spacing on the path:

f2 <- function(x, y, t = seq_along(x), n, method = "natural") {
  new_t <- seq(min(t), max(t), length.out = n)
  new_x <- spline(t, x, xout = new_t, method = method)$y
  new_y <- spline(t, y, xout = new_t, method = method)$y
  data.frame(t = new_t, x = new_x, y = new_y)
}

x <- rnorm(10)
y <- rnorm(10)

ggplot(data = NULL, aes(x, y)) +
  geom_point(data = f2(x, y, n = 1000), size = 0.5) +
  geom_point(data = data.frame(x, y), size = 2, color = "blue")

Created on 2021-08-14 by the reprex package (v2.0.0)

Allan Cameron
  • 147,086
  • 7
  • 49
  • 87
  • Thanks, this is useful. I think it's not exactly what I want though, because it treats y as a function of x, correct? I'd want a path (x, y) as a function of some other parameter t, so I can make loops. – Claus Wilke Aug 14 '21 at 22:08
  • @ClausWilke I see. You can still use `spline` - see my update – Allan Cameron Aug 14 '21 at 22:43