6

I have a viewport which has to have a fixed aspect ratio as it has to have equal distance between x and y units in its native coordinate system.

I want to fit this viewport into a parent viewport such that it will scale to the largest extent possible, but maintains its aspect ratio.

Using the grid unit 'snpc', I was able to maintain the aspect ratio, though I could not reach the largest extent possible. See my code below, which prints out what I have archieved so far at four different device aspect ratios.

Plot at four different output device widths

While the viewport of interest (gray and with grid) fills the maximal area available when the device has a small width, the approach fails if the device width becomes so large that the device height is the limiting factor for the viewport size. The viewport does not cover the whole possible height. I want the viewport of interest to cover the whole device height in the rightmost plot.

EDIT: I found out that ggplot can do this and have updated my example to show that. Note how ggplot touches the upper and lower device border in the rightmost image and the left and right border at the leftmost image, why my self-made solution does not touch the upper and lower device border in the rightmost image even if there would be space. I cannot use ggplot however, as I want to include a custom drawing built only with grid but which is dependent on equal distances on native x and y coordinate system.

ggplot output at four different output device widths

# -- Helper functions ------------------------------------------------------

# Draw something (inside fun) for different paper sizes
forDifferentSizes <- function(names, width, height, fun, ...){
  cyc <- function(x, along) rep_len(x, length(along))
  mapply( names, cyc(width, names), cyc(height, names)
        , FUN = function(n, w, h){
            png(paste0(n,'.png'), width = w, height = h, ...)
            on.exit(dev.off())
            fun(n, w, h)
        })
}

# -- Own attempt -----------------------------------------------------------
library(grid)

# Coordinate system
x <- c(1,6)
y <- c(1,4)
range <- c(diff(x), diff(y))
dims <- range / max(range)

annot <- function(name){
  grid.rect(gp = gpar(fill = NA))
  grid.text( name,unit(1, 'npc'),unit(0,'npc'), just = c(1,0))
}

forDifferentSizes( paste0('X',letters[1:4]), seq(100, 500, length.out = 4), 250
  , fun = function(...){
  grid.newpage()

  pushViewport(
    viewport( width  = unit( dims[1], 'snpc')
              , height = unit( dims[2], 'snpc')
              , xscale = x
              , yscale = y
    )
  )
  annot('vp2')
  grid.grill(v = x[1]:x[2], h = y[1]:y[2], default.units = 'native')
})

# --- ggplot2 can do it -----------------------------------------------------

library(ggplot2)
data("mtcars")

forDifferentSizes(paste0('G',letters[1:4]), seq(100, 500, length.out = 4), 250
  , pointsize = 8
  , fun = function(...){
  p <- ggplot(mtcars) + aes(x = drat, y = mpg) + geom_point() + 
    theme(aspect.ratio = dims[2]/dims[1])
  print(p)
})

# --- Make the output images for post (imagemagick required) ---------------
system('convert G*.png -bordercolor black -border 1x1 +append G.png')
system('convert X*.png -bordercolor black -border 1x1 +append X.png')
akraf
  • 2,965
  • 20
  • 44
  • You are setting vp1 to have spacing of 2cm on each side. vp2 fills vp1 just fine in the examples shown. vp2 cannot be larger than vp1 as it is inside vp2. How do you expect this to work? – alan ocallaghan Jan 03 '18 at 12:34
  • @aocall The images did not match to the code. The behaviour persists even without the viewport vp1. I corrected that now (removed viewport vp1). I am sorry for the confusion – akraf Jan 03 '18 at 12:56
  • I don't know if snpc are the way to go. As explained in [the documentation](https://www.rdocumentation.org/packages/grid/versions/3.4.3/topics/unit), snpc "uses the lesser of npc-width and npc-height." Thus, since npc-height is less than npc-width, it uses npc-height to calculate the viewport size. – alan ocallaghan Jan 03 '18 at 13:58
  • @aocall You are absolutely correct. Unfortunately, 'snpc' is so far the only mechanism which I could find in grid to make viewports with fixed aspect ratios... So either ggplot uses this mechanism in some very clever way which I could not find out so far, or there is another one which I am not aware of. – akraf Jan 03 '18 at 14:13

2 Answers2

5

ggplot2 uses grid layouts with null units and the respect argument to enforce aspect ratios. Here's an example,

library(grid)

ar <- (1+sqrt(5))/2
gl <- grid.layout(1,1,widths=unit(1,"null"), height=unit(1/ar,"null"), respect = TRUE)
grid.newpage()
grid.rect(vp=vpTree(viewport(layout = gl), 
                    vpList(viewport(layout.pos.row = 1, layout.pos.col = 1))))
user9169915
  • 116
  • 1
1

user9169915 did it! Awesome! I am posting here his solution in procedural grid style, for reference. Additionally, I added the equidistant coordinate system.

ar <- (1+sqrt(5))/2 # aspect ratio
# Native coordinate system of the target viewport: make x and y equidistant
xrange <- c(0,5)
yrange <- xrange/arN

forDifferentSizes( paste0('L',letters[1:4]), seq(100, 500, length.out = 4), 250
  , fun = function(...){

  gl <- grid.layout(1,1,widths=unit(1,"null"), height=unit(1/ar,"null"), respect = TRUE)
  grid.newpage()
  pushViewport(viewport(layout = gl))
  annot('vp1') # see question for definition
  pushViewport(viewport(layout.pos.row = 1, layout.pos.col = 1,
                        xscale = xrange, yscale = yrange))
  annot('vp2')
  grid.grill(h=0:floor(yrange[2]), v=0:floor(xrange[2]), default.units = 'native')
  popViewport(2)

})

plot output in four sizes

akraf
  • 2,965
  • 20
  • 44