Add geom_density_2d on sf object to ggplot: can pass geometry to aes(x,y)?

Hi folks,
I'm trying to add contours for the points in the following plot:

Pertinent code (removing theme & other bumf) is:

natlanticextents <- data.frame(lon = c(-70, -55), lat = c(45, 51)) # GSL
extents <- sf::st_as_sf(natlanticextents, coords = c("lon","lat")) %>% sf::st_set_crs(4326)
library(marmap)
b = getNOAA.bathy(lon1 = natlanticextents[1,1], lon2 = natlanticextents[2,1], lat1 = natlanticextents[1,2], lat2 = natlanticextents[2,2], resolution = 4, keep = TRUE, path = paste0(machine, "Blocklab/MapData/getNOAAbathy/")) # get bathymetry data
bf = fortify.bathy(b) # convert bathymetry to data frame

ggplot() +
  annotation_spatial(natlantic, fill = "grey", lwd = 0) + # wide background
  layer_spatial(extents, size = 0, col = "white") + # trains scales
  annotation_spatial(sf_points, # sf 
                     aes(color = Stock)) + #colour on map & legend
  geom_contour(data = bf, # add 200m contour
               aes(x = x, y = y, z = z), breaks = c(-200), size = c(0.15), colour = "black") +
  scale_colour_manual(values = c("blue", "red"))

I'm keen to add contours for the sf_points using geom_density_2d however the guides for this all reference simple x/y lat/lon column dataframes, rather than sf objects. Subsequently I'm struggling to work out how to create these contours for sf_points, which is an sf object created by:

sf_points <- sf::st_as_sf(AnyDataFrame, coords = c("lon","lat")) %>% sf::st_set_crs(4326)

Various attempts:

geom_density_2d(data = sf_points, mapping = aes(geometry))

Error in is.finite(x) : default method not implemented for type 'list'

geom_density_2d(data = AnyDataFrame, mapping = aes(x = lon, y = lat))

Error in layer(data = data, mapping = mapping, stat = stat, geom = GeomDensity2d,: object 'lon' not found

geom_density_2d(data = AnyDataFrame, mapping = aes(Stock))

Error: Discrete value supplied to continuous scale

Am I trying to force a square peg into a round hole here? ggplot elements typically understand sf objects' projections natively, but geom_density_2d requires x & y aesthetics which appear to need to be the x & y positions, which aren't available once converted to geometry with sf::st_as_sf.

Any insights very much appreciated - cheers in advance!

I'm not sure if this will help, but it might. The latest dev version of ggplot makes it easier to mix sf and xy coordinates.

https://twitter.com/ClausWilke/status/1275938314055561216

Thanks Simon, very interesting link. I suspect this won't help since my problem points are already sf, and it seems that the under-the-hood changes here mean that non-sf data are invisibly reprojected to the plot crs if possible. That's my understanding anyway!

Some useful intel from Claus

"Extract the points, calculate the contours manually, and then convert back into sf. First manually infer the density also, using e.g. kde2d(). Then run isoband over the output, then convert to sf."

My attempt:

library(MASS)
gom.kde <- kde2d(x = AnyDataFrame %>% filter(Stock == "GOM") %>% pull(lon),
                 y = AnyDataFrame %>% filter(Stock == "GOM") %>% pull(lat))
library(isoband)
gom.iso <- isobands(x = gom.kde$x,
                    y = gom.kde$y,
                    z = gom.kde$z,
                    levels_low = 1, # levels guessed to try this out
                    levels_high = 2)
iso_to_sfg(gom.iso)

$1:2 MULTIPOLYGON EMPTY

This seems vastly more complicated than it should be, given geom_density_2d has already been coded to do the job. Oddly, per the help:

ggplot(AnyDataFrame, aes(x = lon, y = lat)) +
  geom_point() +
  geom_density_2d()

Works, including if I use that formulation in my code block, i.e. put the non-sf data & aes mappings in the ggplot call, and include a blank geom_density_2d() later, i.e.:
geom_density_2d() works if inheriting data & aes from the ggplot call;

But NOT if the SAME data & aes parameters are given to geom_density_2d() directly:

ggplot() +
geom_density_2d(AnyDataFrame, aes(x = lon, y = lat))

Error: mapping must be created by aes()

This feels like a bug given that seemingly every other ggplot subelement can take data and crs-matching aes(x&y) either explicitly (e.g. geom_contour), or implicitly for sf objects (e.g. layer_spatial, annotation_spatial).

Or maybe a feature request for geom_density_2d to accept sf objects?

Min reprex:

ggplot(faithful, aes(x = eruptions, y = waiting)) +
  geom_density_2d() # works
ggplot() +
  geom_density_2d(faithful, aes(x = eruptions, y = waiting))
# Error: `mapping` must be created by `aes()`

I haven't read the rest of the thread in detail, but for the min reprex, in a geom_ the first argument is the aes and the second is the data frame. Just specifying the first argument is data will resolve the error message

ggplot() +
  geom_density_2d(data = faithful, aes(x = eruptions, y = waiting))

or switching the order of the arguments

ggplot() +
  geom_density_2d(aes(x = eruptions, y = waiting), faithful)
2 Likes

Toryn you legend.

(Another half a day lost to not declaring a parameter!)

Thanks so much :slight_smile:

2 Likes

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.