-
Notifications
You must be signed in to change notification settings - Fork 0
/
Overview.Rmd
279 lines (190 loc) · 10.3 KB
/
Overview.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
---
title: "Using the precisePlacement Package"
author: "Jasper Watson"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Overview}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
knitr::opts_chunk$set(dev.args = list(png = list(type = "cairo")))
```
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.
## Conventions
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')`.
## Preliminaries: The Device Region and Margin Lines
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.
```{r echo = TRUE, fig.height = 5, fig.width = 5}
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.
## Plot Coordinates
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.
## Unit Conversions
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.
```{r, echo = TRUE, fig.height = 3, fig.width = 3}
plot(1:10)
getLinesPerInch()
getInchesPerLine()
getDataPerLine()
getLinesPerDatum()
getDataPerInch()
getInchesPerDatum()
getPixelsPerInch()
getInchesPerPixel()
getPixelsPerLine()
getLinesPerPixel()
getPixelsPerDatum()
getDataPerPixel()
```
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.
```{r, echo = TRUE, fig.height = 5, fig.width = 5}
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.
```{r, echo = TRUE, fig.height = 5, fig.width = 5}
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`.
```{r, echo = TRUE, fig.height = 5, fig.width = 5}
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)
```
## Subfigures
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.
```{r, echo = TRUE, fig.height = 7, fig.width = 7}
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.
## Multiple Plots Per Device
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')`.
```{r, echo = FALSE, fig.height = 7, fig.width = 7}
par(xpd = NA)
oldPar <- par(mfrow = c(2, 2), oma = c(1.1,2.1,3.5,4.5))
##******
plot(1:10, pch = 19)
highlightDeviceRegion()
highlightFigureRegion()
highlightPlotRegion()
highlightDataRegion()
showMarginLines()
##******
par(mar = c(5,5,5,5))
plot(1:10, pch = 19)
highlightDeviceRegion()
highlightFigureRegion()
highlightPlotRegion()
highlightDataRegion()
showMarginLines()
##******
par(mar = c(2,2,2,2))
plot(1:10, pch = 19)
highlightDeviceRegion()
highlightFigureRegion()
highlightPlotRegion()
highlightDataRegion()
showMarginLines()
legend('topleft',
c('Data Region', 'Plot Region', 'Figure Region', 'Device Region'),
text.col = c('darkgreen', 'red', 'orange', 'skyblue'), bty = 'n', text.font = 2,
cex = 0.8, xjust = 0.5, yjust = 0.5)
##******
par(mar = c(2,5,2,2))
plot(1:10, pch = 19)
highlightDeviceRegion()
highlightFigureRegion()
highlightPlotRegion()
highlightDataRegion()
showMarginLines()
##******
showOuterMarginLines()
cat("par('mfg'): ", par('mfg'), '\n')
cat('Boundaries of data region:', getBoundaries('data'), '\n')
cat('Boundaries of plot region:', getBoundaries('plot'), '\n')
cat('Boundaries of figure region:', getBoundaries('figure'), '\n')
cat('Boundaries of device region:', getBoundaries('device'), '\n')
```
```{r, echo = TRUE, fig.height = 7, fig.width = 7}
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'))
print(getBoundaries('device', units = 'lines'))
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)
```