Home > database >  geom_contour with Lambert projection
geom_contour with Lambert projection

Time:08-04

I'm trying to plot a map of the Northeast Atlantic, with specific isobaths highlighted. Given the area, I'd prefer to use the LAEA projection. However, this seems to cause geom_contour to fail.

Here is the background code:

library(tidyverse)
library(marmap)
library(rnaturalearth)

#Set boundaries
bbox <- tibble(x = c(-20, 10), y = c(45, 60))
#Add coordinates converted to LAEA 
bbox <- bbox %>%
      bind_cols(bbox %>% 
                  st_as_sf(coords = c("x", "y")) %>%
                  st_set_crs(4326) %>% #current CRS is WSG84
                  st_transform(3035) %>% #transform CRS to 3035 (Lambert)
                  mutate(x_laea = unlist(map(geometry, 1)),
                         y_laea = unlist(map(geometry, 2))) %>%
                  st_set_geometry(NULL))

#Extract bathymetry for area of interest
nea <- fortify.bathy(getNOAA.bathy(lon1 = min(bbox$x), 
                                   lon2 = max(bbox$x),
                                   lat1 = min(bbox$y), 
                                   lat2 = max(bbox$y), 
                                   resolution = 5))

This works nicely:

ggplot()  
  geom_sf(data = ne_countries(scale = "medium", 
                              returnclass = "sf"))  
  geom_contour(data = nea,
               aes(x = x, 
                   y = y, 
                   z = z),
               breaks = c(-600))  
  coord_sf(xlim = c(min(bbox$x), 
                    max(bbox$x)),
           ylim = c(min(bbox$y), 
                    max(bbox$y)))

But this does not (only the country layer shows up, not the nea one):

ggplot()  
  geom_sf(data = ne_countries(scale = "medium", 
                              returnclass = "sf"))  
  geom_contour(data = nea,
               aes(x = x, 
                   y = y, 
                   z = z))  
  coord_sf(crs = 3035,
           xlim = c(min(bbox$x_laea), 
                    max(bbox$x_laea)),
           ylim = c(min(bbox$y_laea), 
                    max(bbox$y_laea)))

Even if I first convert nae to LAEA first:

nea_laea <- nea %>% 
  st_as_sf(coords = c("x", "y")) %>%
  st_set_crs(4326) %>% #current CRS is WSG84
  st_transform(3035) %>% #transform CRS to 3035 (Lambert)
  mutate(x = unlist(map(geometry, 1)),
         y = unlist(map(geometry, 2))) %>%
  st_set_geometry(NULL)
ggplot()  
  geom_sf(data = ne_countries(scale = "medium", 
                              returnclass = "sf"))  
  geom_contour(data = nea_laea,
               aes(x = x, 
                   y = y, 
                   z = z))  
  coord_sf(crs = 3035,
           xlim = c(min(bbox$x_laea), 
                    max(bbox$x_laea)),
           ylim = c(min(bbox$y_laea), 
                    max(bbox$y_laea)))

I've looked for solutions and figured that a nice way would be to extract the underlying data from the non-reprojected contour, and then plot them with the LAEA projection as geom_line:

extracted_data <- ggplot_build(ggplot()  
                                 geom_contour(data = nea,
                                              aes(x = x, y = y, z = z),
                                              breaks = c(-600)))$data[[1]] %>% 
  st_as_sf(coords = c("x", "y")) %>%
  st_set_crs(4326) %>% #current CRS is WSG84
  st_transform(3035) %>% #transform CRS to 3035 (Lambert)
  mutate(x = unlist(map(geometry, 1)),
         y = unlist(map(geometry, 2))) %>%
  st_set_geometry(NULL)

This almost works, except that it seems the points are not ordered well, so the resulting plot is all scrambled:

ggplot()  
  geom_sf(data = ne_countries(scale = "medium", 
                              returnclass = "sf"))  
  geom_line(data = extracted_data,
            aes(x = x, 
                y = y, 
                group = group))  
  coord_sf(crs = 3035,
           xlim = c(min(bbox$x_laea), 
                    max(bbox$x_laea)),
           ylim = c(min(bbox$y_laea), 
                    max(bbox$y_laea)))

Any idea how to fix this?

Thanks a lot!

CodePudding user response:

Once you've projected your points to something other than the regular lat-long grid, you can't do a contour using geom_contour without re-interpolating onto a regular grid. help(geom_contour) explains:

Description:

     ggplot2 can not draw true 3D surfaces, but you can use
     ‘geom_contour()’, ‘geom_contour_filled()’, and ‘geom_tile()’ to
     visualise 3D surfaces in 2D. To specify a valid surface, the data
     must contain ‘x’, ‘y’, and ‘z’ coordinates, and each unique
     combination of ‘x’ and ‘y’ can appear at most once. Contouring
     requires that the points can be rearranged so that the ‘z’ values
     form a matrix, with rows corresponding to unique ‘x’ values, and
     columns corresponding to unique ‘y’ values. Missing entries are
     allowed, but contouring will only be done on cells of the grid
     with all four ‘z’ values present. If your data is irregular, you
     can interpolate to a grid before visualising using the
     ‘interp::interp()’ function from the ‘interp’ package (or one of
     the interpolating functions from the ‘akima’ package.)

I had trouble understanding what your code was trying to do in places - you can make it more tidy by removing some of the tidyverse stuff. This:

> bbox %>% select(y_laea) %>% filter(y_laea == min(y_laea)) %>% pull()
[1] 2904046

is equivalent to this:

> min(bbox$y_laea)
[1] 2904046

and similarly for the max and X coords.

If all you want to do is get a particular contour set, use contourLines instead of trying to get values out of a plotting construct. Here's a complete example, I've tried to make this as simple as possible so it only uses sf and marmap:

Use these packages:

library(sf)
library(marmap)

set limits - we're not going to transform these so no point building a data frame with them. It also simplified extracting limits:

xr=c(-20, 10)
yr=c(45,60)

Get bathymetry as a matrix - do not "fortify" it:

bathy <- getNOAA.bathy(lon1 = min(xr), 
                     lon2 = max(xr),
                     lat1 = min(yr),
                     lat2 = max(yr),
                     resolution = 5)

Now get the lat and lon coords from the dimnames. See marmap::as.xyz for where I got this from:

lon = as.numeric(rownames(bathy))
lat = as.numeric(colnames(bathy))

Now we have a matrix and coordinates so we can feed this to contourLines and get the -600 level:

c600_list = contourLines(x=lon, y=lat, z=bathy, levels=-600)

This is a list of elements with $x and $y components, so we loop over them with lapply and build st_linestring objects, then join that list of geometry primitives using st_sfc:

c600 = do.call(st_sfc,
             lapply(c600_list, function(l){st_linestring(cbind(l$x, l$y))}) 
             )

So far everything has been lat-long epsg:4326 so let's set that:

st_crs(c600) = 4326

and now we can convert to LAEA epsg:3035:

c600_laea = st_transform(c600, 3035)

You can use ggplot2 to plot:

library(ggplot2)
ggplot()   geom_sf(data=c600_laea)

enter image description here

or use the tmap package in interactive mode to get some context and make sure we're in the right place:

library(tmap)
tmap_mode("view")
# tmap mode set to interactive viewing
tm_shape(c600_laea)   tm_lines()

enter image description here

  •  Tags:  
  • r sf
  • Related