Consider the following task: drawing two circles at (1, 0)
and (-1, 0)
with both radius of 1.
theta = seq(0, 2*pi, length = 50)
x1 = cos(theta) + 1
y1 = sin(theta)
x2 = cos(theta) - 1
y2 = sin(theta)
We might think of the following way:
library(grid)
grid.newpage()
pushViewport(viewport(xscale = c(-2, 2), yscale = c(-1, 1)))
grid.lines(x1, y1, default.units = "native")
grid.lines(x2, y2, default.units = "native")
Unfortunately it only works when the width of the image is twice of the height.
To enforce the aspect ratio to 2:1, we may think of using the snpc
unit. Taking
the height of the viewport to 1snpc
and the width of the viewport to 2snpc
.
It works when the height of the image is less than width/2 of the image (in this case, 1snpc
is the height of the image).
grid.newpage()
pushViewport(viewport(xscale = c(-2, 2), yscale = c(-1, 1),
width = unit(2, "snpc"), height = unit(1, "snpc")))
grid.lines(x1, y1, default.units = "native")
grid.lines(x2, y2, default.units = "native")
But when the height of the image is larger than width/2 but smaller than 1width, circles will exceed the image area.
You may set width = unit(1, "snpc"), height = unit(0.5, "snpc")
, but it has the same problem.
Here the core problem is the viewport is dynamically changed. We want the size of the two circles to be automatically adjusted to maximally fill the image. In this post, I will introduce how to construct a dynamic graphical object.
First I create a new grob object in the class double_circle
which contains two circles
(in form of two linesGrob()
calls). I also create a viewport to include the two
line grobs.
vp = viewport(xscale = c(-2, 2), yscale = c(-1, 1))
grob = grobTree(linesGrob(x1, y1, default.units = "native"),
linesGrob(x2, y2, default.units = "native"),
vp = vp, cl = "double_circle")
As you may see, creating a new grob is quite simple. You just integrate a list
of simple grobs (e.g. points, lines) in the grobTree()
function. The class
is defined by a character string assigned to the cl
argument.
In grob
, the viewport is in the vp
slot which we will dynamically update later.
str(grob$vp)
## List of 33
## $ x : 'simpleUnit' num 0.5npc
## ..- attr(*, "unit")= int 0
## $ y : 'simpleUnit' num 0.5npc
## ..- attr(*, "unit")= int 0
## $ width : 'simpleUnit' num 1npc
## ..- attr(*, "unit")= int 0
## $ height : 'simpleUnit' num 1npc
## ..- attr(*, "unit")= int 0
## $ justification : num [1:2] 0.5 0.5
## $ gp : list()
## ..- attr(*, "class")= chr "gpar"
## $ clip : logi FALSE
## $ xscale : num [1:2] -2 2
## $ yscale : num [1:2] -1 1
## $ angle : num 0
## $ layout : NULL
## $ layout.pos.row: NULL
## $ layout.pos.col: NULL
## $ valid.just : num [1:2] 0.5 0.5
## $ valid.pos.row : NULL
## $ valid.pos.col : NULL
## $ name : chr "GRID.VP.4"
## $ parentgpar : NULL
## $ gpar : NULL
## $ trans : NULL
## $ widths : NULL
## $ heights : NULL
## $ width.cm : NULL
## $ height.cm : NULL
## $ rotation : NULL
## $ cliprect : NULL
## $ parent : NULL
## $ children : NULL
## $ devwidth : NULL
## $ devheight : NULL
## $ clippath : NULL
## $ mask : logi TRUE
## $ resolvedmask : NULL
## - attr(*, "class")= chr "viewport"
In grid, when a grob is drawn, a list of functions will be executed in
serial. Here I introduce the generic function makeContext()
which will be
executed before drawing the graphics. To use makeContext()
, you need to
define a S3 method for the grob class. The input is the grob and the output is
an edited grob. makeContext()
is mainly for adjusting the viewport of the graphics in the grob.
In the following example, for the double_circle
grob, the width and the
height of the current viewport are checked. If the width is larger than two
times of the height, the width is adjusted to two times of the height, or else
the height is adjusted to half of the width of the viewport.
makeContext.double_circle = function(x) {
vp_width = convertWidth(x$vp$width, "in", valueOnly = TRUE)
vp_height = convertHeight(x$vp$height, "in", valueOnly = TRUE)
if(vp_width > 2*vp_height) {
x$vp$width = unit(2*vp_height, "in")
x$vp$height = unit(vp_height, "in")
} else {
x$vp$width = unit(vp_width, "in")
x$vp$height = unit(vp_width/2, "in")
}
x
}
Now we can use grid.draw()
to draw the grob without worring the size of the image device.
grid.newpage()
grid.draw(grob)
sessionInfo()
## R version 4.2.0 (2022-04-22)
## Platform: x86_64-apple-darwin17.0 (64-bit)
## Running under: macOS Big Sur/Monterey 10.16
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRlapack.dylib
##
## locale:
## [1] C/UTF-8/C/C/C/C
##
## attached base packages:
## [1] grid stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] knitr_1.43
##
## loaded via a namespace (and not attached):
## [1] bookdown_0.34 digest_0.6.33 R6_2.5.1 jsonlite_1.8.7 evaluate_0.21 highr_0.10
## [7] blogdown_1.18 cachem_1.0.8 rlang_1.1.1 cli_3.6.1 jquerylib_0.1.4 bslib_0.5.0
## [13] rmarkdown_2.23 tools_4.2.0 xfun_0.39 yaml_2.3.7 fastmap_1.1.1 compiler_4.2.0
## [19] htmltools_0.5.5 sass_0.4.7