This package provides a selection of tools that make it easier to place elements onto a (base R) plot exactly where you want them. It provides translations between inches, pixels, margin lines, data units, and proportions of the plotting space so that elements can easily be added relative to each other or to the figure boundaries.
The “How big is your graph?” cheat sheet written by Steve Simon, found at https://www.rstudio.com/wp-content/uploads/2016/10/how-big-is-your-graph.pdf, was very helpful. If you find yourself referring to that often then perhaps this package may be of interest.
All four-unit values are returned in the order of (bottom, left, top,
right) like par('mar')
and not like par('usr')
which is (left, right, bottom, top).
All two-unit values are returned in the order of (x-axis, y-axis)
like par('pin')
.
Plots created with base R contain four different “regions”. The first
is the innermost rectangle that the data points are displayed in. I
refer to this as the “data region”. By default this is extended by 4% in
both the x and y axes before the axis labels are added. This is the
“plot region”. This behaviour can be disabled using the parameters
xaxs = 'i'
and yaxis = 'i'
, either passed to
par()
or to plot()
. If so the plot region will
be the same size as the data region. Next is the “figure region”. This
includes the plot region plus the margin lines given by
par(mar)
. By default this is
c(5, 4, 4, 2) + 0.1
; if set to zero for all four sides then
the figure region will be the same size as the plot region. Finally
there is the device region. This contains the figure region(s) plus any
outer margin lines. There are none by default, meaning the device region
is the same size as the figure region for single-panel plots, but outer
margins can be specified using par(oma)
.
The plot below illustrates these regions. The functions used to highlight the regions and lines can be useful for diagnostic purposes.
library(precisePlacement)
par(xpd = NA, oma = 1:4)
plot(1:10, pch = 19)
highlightDeviceRegion()
highlightFigureRegion()
highlightPlotRegion()
highlightDataRegion()
showMarginLines()
showOuterMarginLines()
legend('topleft', c('Data Region', 'Plot Region', 'Figure Region', 'Device Region'),
text.col = c('darkgreen', 'red', 'orange', 'skyblue'), bty = 'n', text.font = 2,
xjust = 0.5, yjust = 0.5)
These illustrations make it easier to judge what value is needed for
the line
parameter in the likes of axis
,
mtext
, rug
, and many more. There is also the
function lineLocations
to help with this, which returns the
coordinates of each margin line in the same units as the data axes.
The regions of a plot can be measured in units of inches, pixels, margin lines, or data (meaning whatever is being plotted).
The getBoundaries
function will return the four corners
of either the data, plot, figure, or device region, in units of either
data or margin lines. (This is similar to par('usr')
which
returns the boundaries of the plot region in units of data.)
The getRange
function will return the distance between
the left and right boundaries and the bottom and top boundaries. Again
this can be for the data, plot, figure, or device region but in addition
to data and margin lines it can also give units of inches or pixels.
For relative conversions between inches, pixels, margin lines, and data units there are a number of functions of the form “getXperY” as shown in the example below. These return numeric vectors of length two giving the value for the x and y axes, respectively.
getLinesPerInch()
#> [1] 5 5
getInchesPerLine()
#> [1] 0.2 0.2
getDataPerLine()
#> [1] 1.104545 1.675862
getLinesPerDatum()
#> [1] 0.9053498 0.5967078
getDataPerInch()
#> [1] 5.522727 8.379310
getInchesPerDatum()
#> [1] 0.1810700 0.1193416
getPixelsPerInch()
#> [1] 192 192
getInchesPerPixel()
#> [1] 0.005208333 0.005208333
getPixelsPerLine()
#> [1] 38.4 38.4
getLinesPerPixel()
#> [1] 0.02604167 0.02604167
getPixelsPerDatum()
#> [1] 34.76543 22.91358
getDataPerPixel()
#> [1] 0.02876420 0.04364224
For absolute conversions we can use the convertUnits
function which will transform the units of a specific point on a plot.
In this case the possible units are data points, margin lines, and
proportions of the data/plot/figure/device region.
plot(seq(as.Date('2018-01-01'), as.Date('2019-01-01'), length.out = 10), 1:10,
pch = 19, xlab = '', ylab = '')
## Identify the center of the plot.
abline(h = convertUnits('proportion', 0.5, 'data', axis = 'y'),
col = 'red', lwd = 4)
abline(v = convertUnits('proportion', 0.5, 'data', axis = 'x'),
col = 'blue', lwd = 4)
## Change the region we are defining the proportions from.
abline(v = convertUnits('proportion', 0.75, 'data', axis = 'x', region = 'plot'),
col = 'darkgreen', lwd = 4)
abline(v = convertUnits('proportion', 0.75, 'data', axis = 'x', region = 'device'),
col = 'orange', lwd = 4)
This is useful if we want to place a legend somewhere specific.
par(xpd = NA, oma = c(0, 0, 0, 5))
plot(1:10, pch = 19)
legend(x = 11,
y = convertUnits('proportion', 0.5, 'data', axis = 'y', region = 'device'),
LETTERS[1:8], ncol = 1, bty = 'n', text.font = 2, text.col = 1:8,
xjust = 0.5, yjust = 0.5, cex = 3
)
We can also use the relative unit conversions to easily size elements
for plotting. Below is a contrived example where we create a simple
timeline chart - note how easy it is to select a value for
lwd
so that we don’t have to use rect
.
projects <- list(A = as.Date(c('2018-01-01', '2018-06-04')),
B = as.Date(c('2018-02-01', '2018-11-01')),
C = as.Date(c('2018-11-01', '2018-12-01')),
D = as.Date(c('2018-03-01', '2018-05-01')),
E = as.Date(c('2018-01-01', '2018-03-01')),
F = as.Date(c('2018-09-01', '2018-12-01')),
G = as.Date(c('2018-05-01', '2018-07-01'))
)
x <- seq(as.Date('2018-01-01'), as.Date('2019-01-01'), length.out = length(projects))
y <- 1:length(projects)
plot(x, y, xlim = range(x), ylim = c(0.5, length(projects) + 0.5),
type = 'n', xlab = 'Date', ylab = '', axes = FALSE, xaxs = 'i', yaxs = 'i')
axis(1, pretty(x), pretty(x))
axis(2, at = y, labels = names(projects), las = 2)
box()
lineWidth <- (getRange('plot', 'data')[2] / length(projects)) * getPixelsPerDatum()[2]
for (ii in seq_along(projects))
lines(projects[[ii]], rep(ii, 2), lwd = lineWidth, ljoin = 2, lend = 1)
The omiForSubFigure
function allows users to easily
create a vector of four values with units of inches to pass to
par('omi')
and create a new plot as a sub-figure of an
existing one.
plot(1:10, pch = 19)
originalPar <- par()
## Select a region in terms of proportions of the existing one.
omi <- omiForSubFigure(0.5, 0.05, 0.95, 0.5, region = 'device')
par(omi = omi, new = TRUE, xpd = NA)
plot(1:10, pch = 19, col = 'red')
highlightFigureRegion()
## Reset to the original par otherwise we will be referencing the subplot.
originalPar[c('cin', 'cra', 'csi', 'cxy', 'din', 'page')] <- NULL
par(originalPar)
## Select a new region in terms of the original plotting units.
omi <- omiForSubFigure(2, 6, 5, 10, units = 'data')
par(omi = omi, mar = c(0, 0, 0, 0))
plot(1:10, pch = 19, col = 'red', xlab = '', ylab = '')
highlightFigureRegion()
Note that when specifying the proportions for
omiForSubFigure
the top and right sides are defined with
reference to the bottom and left. This is because I found it more
intuitive to specify, for example, the fraction of the plot between 0.4
and 0.7 rather than counting 0.4 from one side and 0.3 from the
other.
All of the functions mentioned above can also be used in cases of
multiple plots per device. Those that pertain to only one of the plots
will apply to the currently active one as per
par('mfg')
.
#> par('mfg'): 2 2 2 2
#> Boundaries of data region: 1 1 10 10
#> Boundaries of plot region: 0.64 0.64 10.36 10.36
#> Boundaries of figure region: -0.6749051 -3.866536 11.67491 12.16261
#> Boundaries of device region: -1.398103 -21.78843 26.3258 16.2185
par(xpd = NA, mfrow = c(2, 2))
plot(1:10, pch = 19)
plot(1:10, pch = 19)
plot(1:10, pch = 19)
plot(1:10, pch = 19)
print(getBoundaries('device', units = 'data'))
#> [1] -3.531204 -15.806265 30.957847 11.731374
print(getBoundaries('device', units = 'lines'))
#> [1] 5.10000 25.18434 25.18434 2.10000
x <- getBoundaries('device', units = 'lines')
## lineLocations is a shortcut to using convertUnits.
abline(h = lineLocations(side = 1, 0:x[1]), col = 'red', lty = 2)
abline(v = lineLocations(side = 2, 0:x[2]), col = 'blue', lty = 2)
abline(h = lineLocations(side = 3, 0:x[3]), col = 'green', lty = 2)
abline(v = lineLocations(side = 4, 0:x[4]), col = 'black', lty = 2)