How do I manually fit a viewport with a fixed aspe

2019-04-09 00:10发布

问题:

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.

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.

# -- 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')

回答1:

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))))


回答2:

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)

})