Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I manually fit a viewport with a fixed aspect ratio into its parent such that no space is wasted like ggplot can do?

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')
like image 814
akraf Avatar asked Dec 08 '17 20:12

akraf


2 Answers

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))))
like image 185
user9169915 Avatar answered Oct 27 '22 00:10

user9169915


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

like image 45
akraf Avatar answered Oct 26 '22 23:10

akraf