7

Similar to ggplot2: How to rotate a graph in a specific angle?, but I don't want the image/square rotated, I'd like the data rotated within the frame.

For instance, if I start with this:

library(ggplot2)
usa <- maps::map('usa', plot=FALSE)

ggplot(as.data.frame(usa[c("x","y")]), aes(x,y)) +
  coord_quickmap() +
  geom_path()

usa map, north-up

I'd like to be able to generate this:

usa map, rotated

  • Rotation needs to preserve aspect-ratio of the spatial data
  • I need to support arbitrary rotation, not constrained by 90-degree jumps
  • I'd like if the actual grid can be retained (to preserve the default behaviors within ggplot2), but if not I can generate them manually
  • I'd like the enclosing rectangle of the generic plot to be unchanged, including (not shown here) titles and subtitles
  • I'd like it if the coordinate system is unchanged, meaning that I can add other layers that will subsequently automatically appreciate the rotation. @qdread's answer gives the alternative to this, where a single function can calculate the rotation (though I don't know how to do this mathematically with spatial data)
  • Please disregard:
    • the grid problems in the top-left corner, my photoshopping was incomplete
    • the x-axis labels, the numbers are wrong (y-axis is good-ish) (they can be removed, not required)

I wonder if this can be accomplished with CRS/projections, but I'm not smart enough on them to work with it formally/correctly.

r2evans
  • 141,215
  • 6
  • 77
  • 149
  • 1
    [Maybe maybe a start](https://stackoverflow.com/a/66401206/1851712)? – Henrik Mar 31 '21 at 14:29
  • I think that's most of what I was thinking and could not find. I"ll give it a shot, and perhaps this question is a dupe of that. Thank you, @Henrik! – r2evans Mar 31 '21 at 15:25
  • @Henrik, are you aware of an ability to scale both axes simultaneously? `scale_x_continuous` and `_y_` don't work here since the transformation is bivariate, but a `scale_xy_*` might be better. – r2evans Mar 31 '21 at 15:30
  • 2
    I'm very rusty on ggplot internals, but maybe a custom `coord` function could work instead of a `scale_xy_*`, say built off of `coord_quickmap` or `coord_map` with a transformation argument. – Gregor Thomas Mar 31 '21 at 15:35

2 Answers2

10

edit2: Using the oblique mercator projection to rotate a map:

#crs with 45degree shift using +gamma
# lat_0 and lonc approximate centroid of US
crs_string = +proj=omerc +lat_0=36.934 +lonc=-90.849 +alpha=0 +k_0=.7 +datum=WGS84 +units=m +no_defs +gamma=45"

# states data & libraries in code chunk below
ggplot(states) +
    geom_sf() +
    geom_sf(data = x, color = 'red') +
    coord_sf(crs = crs_string, 
             xlim = c(-3172628,2201692),    #wide limits chosen for animation
             ylim = c(-1951676,2601638)) +  # set as needed
    theme_bw() +
    theme(axis.text = element_blank()) 

enter image description here

Animation of gamma from 0:360 in 10deg increments, alpha constant at 0. The artifacts are from gif compression, actual plots look like the one above labelled gamma 45. enter image description here

earlier answer: I think you can 'rotate' the plot (including graticules) by 'looking' at the earth from a different perspective by changing the projection to a Lambert azimuthal equal area and adjusting +lon_0=x in the projection string.

This should meet most of your goals, but I don't know how to get an exact rotation in degrees.

Below I've transformed the states_sf sf object manually before plotting. It may be easier to transform the plot (and all the sf data being plotted) by working with crs 4326 for the data, and adding + coord_sf(crs = "+proj=laea +x_0=0 +y_0=0 +lon_0=-140 +lat_0=40") to the end of the ggplot() + call.

library(sf)
#> Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
library(urbnmapr)
library(tidyverse)

# get sf of the us, remove AK & HI, 
#  transform to crs 4326 (lat & lon)
  states_sf <- get_urbn_map("states", sf = TRUE) %>%
    filter(!state_abbv %in% c('AK', 'HI')) %>%
    st_transform(4326)

  #centroid of us to 'look' at the US from directly above the center
  us_centroid <- st_union(states_sf) %>% st_centroid() %>% st_transform(4326)

  st_coordinates(us_centroid)
#>           X        Y
#> 1 -99.38208 39.39364
  
# Plots, changing the +lon_0=xxx of the projection
  p1 <- states_sf %>% 
  st_transform("+proj=laea +x_0=0 +y_0=0 +lon_0=-99.382 +lat_0=39.394") %>%
  ggplot(aes()) +
  geom_sf(fill = "black", color = "#ffffff") +
    ggtitle('US from above its centroid 39.3N 99.4W')

  p2 <- states_sf %>% 
    st_transform("+proj=laea +x_0=0 +y_0=0 +lon_0=-70 +lat_0=40") %>%
    ggplot(aes()) +
    geom_sf(fill = "black", color = "#ffffff") +
    ggtitle('US from 40N 70W')
  
  p3 <- states_sf %>% 
    st_transform("+proj=laea +x_0=0 +y_0=0 +lon_0=-50 +lat_0=40") %>%
    ggplot(aes()) +
    geom_sf(fill = "black", color = "#ffffff") +
    ggtitle('US from 40N 50W')
  
  p4 <- states_sf %>% 
    st_transform("+proj=laea +x_0=0 +y_0=0 +lon_0=-30 +lat_0=40") %>%
    ggplot(aes()) +
    geom_sf(fill = "black", color = "#ffffff") +
    ggtitle('US from 40N 30W')
  
  p5 <- states_sf %>% 
    st_transform("+proj=laea +x_0=0 +y_0=0 +lon_0=-10 +lat_0=40") %>%
    ggplot(aes()) +
    geom_sf(fill = "black", color = "#ffffff") +
    ggtitle('US from 40N 10W')
  
  p6 <- states_sf %>% 
    st_transform("+proj=laea +x_0=0 +y_0=0 +lon_0=-140 +lat_0=40") %>%
    ggplot(aes()) +
    geom_sf(fill = "black", color = "#ffffff") +
    ggtitle('US from 40N 140W')
  
 p1

 p3

 p5

 p6

Created on 2021-03-31 by the reprex package (v0.3.0)

edit:

It looks like you can get any angle with a combination of alpha and gamma when using the projection "+proj=omerc +lonc=-90 +lat_0=40 +gamma=0 +alpha=0". I don't know exactly how they relate (something to do with azimuths), but this should help visualize it:

# Basic template for rotating, keeping US map centered.
#  adjust alpha and gamma by trial & error
crs <- "+proj=omerc +lonc=90 +lat_0=40 +gamma=90 +alpha=0"
 
 states_sf %>% 
   ggplot() + 
   geom_sf(fill = 'green') +
   coord_sf(crs = crs)

enter image description here

Animation of a broader range of alpha & gamma can be found here.

mrhellmann
  • 5,069
  • 11
  • 38
  • Thanks! I've long wondered if the PROJ can be adjusted to accommodate that. https://proj.org/operations/transformations/affine.html suggests that the PROJ project does support it within the proj string, but that doesn't seem to work (yet?) in the `sf` or related R packages. Using the view-perspective as a way to rotate it is an interesting way to get that. I'm a little concerned about the skew/aspect-ratio of some of the rotated plots, perhaps that can be mitigated. Thanks again! – r2evans Mar 31 '21 at 20:44
  • 1
    Making maps *not* point north is apparently difficult. Maybe gis.stackoverflow.com can help, there are definitely limitations to the above method. Custom crs's might be possible: https://proj.org/operations/projections/ob_tran.html?highlight=rotate#id1 – mrhellmann Mar 31 '21 at 20:56
  • Yes, I'd looked at similar docs. Adding `+o_alpha=` (and related) to the proj does not appear to provide rotation, not sure why. – r2evans Mar 31 '21 at 22:07
  • see edit: the omerc projection is a little easier to work with: https://proj.org/operations/projections/omerc.html – mrhellmann Apr 01 '21 at 03:21
  • @r2evans looks like I was foggy last night. set +alpha=0 and changing +gamma= from 0:360 will rotate the map (clockwise). lonc and lat_0 should be approximate centroid of the shape to map. – mrhellmann Apr 01 '21 at 13:15
  • I'm having a difficult time finding a balance of `+lonc`, `+gamma`, and `+alpha` to mimic what I think should be happening (which suggests I don't understand all of those fields). `+lonc` (from https://proj.org/operations/projections/omerc.html) is *"longitude of the central point"*, which for my purposes should be clear, but I'm finding that it has significant effects on rotation and counter-intuitive distortion. It'll take a bit, I know people have full-time jobs dealing with GIS and its projections ... – r2evans Apr 01 '21 at 13:26
  • (I expected `+lonc` to be more of a translational thing without the distortion and rotation.) – r2evans Apr 01 '21 at 13:27
  • I'm not finding a great resource that clearly defines many of the parameters you set. For instance, `gamma` seems to set "north", but I've seen `alpha` referred to as the rotation parameter and `k_0` as scaling. In some maps, I've need to use `k_0=1` instead of 0.7, and I don't understand how to determine a good value without visual confirmation (which is imperfect, admittedly). Do you have a good reference for defining and calculating values for these parameters? – r2evans Aug 24 '23 at 13:22
  • @r2evans it has been a while, but maybe this will help answer some of your questions: https://proj.org/en/9.2/operations/projections/omerc.html – mrhellmann Aug 24 '23 at 17:35
4

In order to preserve the shape of the map when we rotate, we first need to transform from lat-long to a conformal coordinate system where local angles are preserved. We will use a Lambert conformal conic projection, specifically ESRI:102004 for contiguous USA. We coerce the usa object to a sf object and apply the CRS transformation.

lambert_proj4 <- '+proj=lcc +lat_1=33 +lat_2=45 +lat_0=39 +lon_0=-96 +x_0=0 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs'

library(sf)

usa_trans <- usa %>% 
  st_as_sf %>%
  st_transform(crs = lambert_proj4)

The result looks like this:

ggplot(usa_trans) + geom_sf()

enter image description here

Next we modify the procedure in the sf documentation on affine transformations to rotate the geometry around its centroid.

The following function defines the transformation matrix.

rot = function(a) matrix(c(cos(a), sin(a), -sin(a), cos(a)), 2, 2)

Extract the geometry from the transformed object and then get the centroid. This will return ten points because there are 9 large islands included in addition to the mainland. (e.g. Long Island).

usa_geom <- st_geometry(usa_trans)
usa_centroid <- st_geometry(st_centroid(usa_trans))

Therefore we take usa_centroid[1,] which is the centroid of the mainland polygon, subtract it, apply the rotation of 45 degrees counterclockwise, and add back the centroid.

usa_rot_geom <- (usa_geom - usa_centroid[1,]) * rot(-45 * 2*pi/360) + usa_centroid[1,]

The result looks like this:

ggplot(usa_rot_geom) + geom_sf()

enter image description here

Finally if desired you can back-transform to lat-long again before plotting.

usa_rot_latlong <- usa_rot_geom %>%
  st_set_crs(lambert_proj4) %>%
  st_transform(4326) %>%
  st_as_sf

Result:

ggplot(usa_rot_latlong) + geom_sf()

enter image description here

qdread
  • 3,389
  • 19
  • 36
  • 1
    Just saw the linked post in Henrik's comment above which I believe is essentially identical to what I did. – qdread Mar 31 '21 at 14:44
  • I've used that technique for other (non-geospatial) problems in the past, but unfortunately lat/lon data doesn't rotate correctly while preserving aspect. Thanks for the suggestion, though, it confirms some thoughts about where the transforming can be done. – r2evans Mar 31 '21 at 15:20
  • 1
    This is fantastic! I don't think transforming back to latlon is quite right, USA does not extend north of 60°N ... but I get the idea. Thanks! – r2evans Mar 31 '21 at 16:13
  • I'm not sure how to avoid it going north when you rotate -- if rotating counterclockwise the northeast corner will have to go "north" and the southwest corner "south" so the extent of the map will need to be modified. In the case of the USA that will add more to the northern extent because Maine is "pointier" (farther from the centroid) than the SW corner. – qdread Mar 31 '21 at 16:20
  • 1
    I think the point is better said this way: the cartesian assumption of the underlying ggplot2 coord cannot easily be transformed into the rotated coordinates ... so if I want a latlon-accurate grid, I'll need to add it manually (which is fine). Getting the labels on the axes might be an issue, but that's perhaps a different discussion. My comment about 60°N was mostly poking a hole in my expectation, not in your implementation, since I think you were bending to meet the question. – r2evans Mar 31 '21 at 16:27
  • Is there a particular reason you chose NAD83 instead of WGS84? (It seems to work fine with that instead, just curious.) – r2evans Mar 31 '21 at 16:50
  • 1
    I just quickly googled conformal projections for contiguous USA and that's what I got :-D – qdread Mar 31 '21 at 16:52
  • I really appreciate the answer! My only reservation is that it needs to be done manually for every layer going in, and qdread's answer provides a method for effectively changing the rotation of the coordinate system, which should apply to all subsequent layers (if/when needed). If I could upvote multiples I would. Thanks again! – r2evans Apr 01 '21 at 13:27