13. Graphics¶
This open-access textbook is, and will remain, freely available for everyone’s enjoyment (also in PDF; a paper copy can also be ordered). It is a non-profit project. Although available online, it is a whole course, and should be read from the beginning to the end. Refer to the Preface for general introductory remarks. Any bug/typo reports/fixes are appreciated. Make sure to check out Minimalist Data Wrangling with Python [27], too.
The R project homepage advertises our free software as an environment for statistical computing and graphics. Hence, had we not dealt with the latter use case, our course would have been incomplete.
R is nowadays equipped with two independent (incompatible, yet coexisting) systems for graphics generation; see Figure 13.1.
The (historically) newer one, grid (e.g., [48]), is very flexible but might seem complicated. Some readers might have come across the lattice [53] and ggplot2 [60, 63] packages before: they are built on top of grid.
On the other hand, its traditional (S-style) counterpart, base graphics (e.g., [7]), is much easier to master. It still gives their users complete control over the drawing process. It is simple, fast, and minimalist, which makes it very attractive from the perspective of this course’s philosophy.
This is why we only cover the second system here. Most importantly, all figures in this book were generated using graphics and its dependants. They are sufficiently aesthetic, aren’t they? Form precedes essence (but see [56, 59]).
13.1. Graphics primitives¶
In graphics, we do not choose from a superfluity of virtual objects to be placed on an abstract canvas, letting some algorithm decide how and when to delineate them. We just draw. We do so by calling functions that plot the following graphics primitives (see, e.g., [36, 44]):
symbols (e.g., pixels, circles, stars) of different shapes and colours,
line segments of different styles (e.g., solid, dashed, dotted),
polygons (optionally filled),
text (using available fonts),
raster images (bitmaps).
That’s it. It will turn out that all other shapes (smooth curves, circles) may be easily approximated using the above.
Of course, in practice, we do not always have to be so low-level. There are many functions that provide the most popular chart types: histograms, bar plots, dendrograms, etc. They will suit our basic needs. We will review them in Section 13.3.
The more primitive routines we discuss next will still be of service for fine-tuning our figures and adding further details. However, if the prefabricated components are not what we are after, we will be able to create any drawing from scratch.
Important
In graphics, most of the function calls have immediate effects. Objects are drawn on the active plot one by one, and their state cannot be modified later.
Figure 13.2 depicts some graphics primitives, which we plotted using the following program. We will detail the meaning of all the functions in the next sections, but they should be self-explanatory enough for us to be able to find the corresponding shapes in the plot.
par(mar=rep(0.5, 4)) # small plot margins (bottom, left, top, right)
plot.new() # start a new plot
plot.window(c(0, 6), c(0, 2), asp=1) # x range: 0–6, y: 0–2; proportional
x <- c(0, 0, NA, 1, 2, 3, 4, 4, 5, 6)
y <- c(0, 2, NA, 2, 1, 2, 2, 1, 0.25, 0)
points(x[-(1:6)], y[-(1:6)]) # symbols
lines(x, y) # line segments
text(c(0, 6), c(0, 2), c("(0, 0)", "(6, 2)"), col="red") # two text labels
rasterImage(
matrix(c(1, 0, # 2x3 pixel "image"; 0=black, 1=white
0, 1,
0, 0), byrow=TRUE, ncol=2),
5, 0.5, 6, 2, # position: xleft, ybottom, xright, ytop
interpolate=FALSE
)
polygon(
c(4, 5, 5.5, 4), # x coordinates of the vertices
c(0, 0, 1, 0.75), # y coordinates
lty="dotted", # border style
col="#ffff0044" # fill colour: semi-transparent yellow
)
13.1.1. Symbols (points)¶
The points function can draw a series
of symbols (by default, circles) on the two-dimensional
plot region, relative to the user coordinate system.
We specify the points’ coordinates using the x
and y
arguments
(two vectors of equal lengths; no recycling).
Alternatively, we may give a matrix or a data frame with two columns:
its first column (regardless of how and if it is named)
defines the abscissae, and the second column determines the ordinates.
This function permits us to plot each point differently if this
is what we desire. Thus, it is ideal for drawing scatter plots,
possibly for grouped data (see Figure 13.17 below).
It is vectorised with respect to, amongst others,
the col
(colour; see Section 13.2.1), cex
(scale, defaults to 1),
and pch
(plotting character or symbol, defaults to 1, i.e., a circle)
arguments.
Figure 13.3 gives an overview of the plotting symbols available. The most often used ones are:
NA_integer_
– no symbol,0
, …,14
and15
, …,18
– unfilled and filled symbols, respectively;19
, …,25
– filled symbols with a border of widthlwd
; for codes21
, …,25
, the fill colour is controlled separately by thebg
parameter,"."
– a tiny point (a “pixel”),"a"
,"1"
, etc. – a single character (not all Unicode characters can be drawn); strings longer than one will be truncated.
par(mar=rep(0.5, 4)); plot.new(); plot.window(c(0.9, 9.1), c(0.9, 4.1))
points(
cbind(1:9, 1), # or x=1:9, y=rep(1, 9); bottom row
col="red",
pch=c("A", "B", "a", "b", "Spanish Inquisition", "*", "!", ".", "9")
)
xy <- expand.grid(1:9, 4:2)
text(xy, labels=0:(NROW(xy)-1), pos=1, cex=0.89, offset=0.75, col="darkgray")
points(xy, pch=0:(NROW(xy)-1), bg="yellow")
## Warning in plot.xy(xy.coords(x, y), type = type, ...): unimplemented pch
## value '26'
13.1.2. Line segments¶
lines can draw connected line segments whose mid- and endpoints are given in a similar manner as in the points function. A series of segments can be interrupted by defining an endpoint whose coordinate is a missing value; compare Figure 13.2.
Actually, points and lines are wrappers around the same function, plot.xy, which we usually do not call directly.
Their type
arguments determine the object to draw;
the only difference is that by default the former uses type="p"
whilst the latter relies on type="l"
.
Changing these to type="b"
(both) or type="o"
(overplot) will give
their combination. Moreover, type="s"
and type="S"
creates step
functions (with post- and pre-increments, respectively),
and type="h"
draws bar plot-like vertical lines.
For an illustration, see Figure 13.4
(implement something similar yourself as an exercise).
The col
argument controls the line colour (see Section 13.2.1),
and lwd
determines the line width (1 by default). Six named line types
(lty
) are available, which can also be specified via their respective
numeric identifiers, lty=1
, …, lty=6
; see Figure 13.5
(implementing a similar plot is left as an exercise). Additionally, custom
dashes can be defined using strings of up to eight hexadecimal digits.
Consecutive digits give the lengths of the dashes and blanks (alternating).
For instance, lty="1343"
yields a dash of length 1, followed by a space of
length 3, then a dash of length 4, followed by a blank of length 3.
The whole sequence will be recycled for as long as necessary.
lines can be used for plotting empirical cumulative distribution functions (we will suggest it as an exercise later), regression models (e.g., lines, splines of different degrees), time series, and any other mathematical functions, even when they are smooth and curvy. The naked eye cannot tell the difference between a densely sampled piecewise linear approximation of an object and its original version. The code below illustrates this (sad for the high-hearted idealists) truth using the sine function; see Figure 13.6.
ns <- c(seq(3, 25, by=2), 50, 100)
par(mar=rep(0.5, 4)); plot.new(); plot.window(c(0, length(ns)*pi), c(-1, 1))
for (i in seq_along(ns)) {
x <- seq((i-1)*pi, i*pi, length.out=ns[i])
lines(x, sin(x))
text((i-0.5)*pi, 0, ns[i], cex=0.89)
}
Implement your version of the segments function using a call to lines.
(*) Implement a simplified version of the arrows function,
where the length of edges of the arrowhead is given in user coordinates
(and not inches; you will be equipped with skills to achieve this
later; see Section 13.2.4).
Use the ljoin
and lend
arguments (see help("par")
for admissible values)
to change the line end and join styles (from the default rounded caps).
13.1.3. Polygons¶
polygon draws a polygon with a border of specified
colour and line type (border
, lty
, lwd
).
If the col
argument is not missing, the polygon is filled
(or hatched; cf. the density
and angle
arguments).
Let’s draw a few regular (equilateral and equiangular) polygons; see Figure 13.7. By increasing the number of sides, we can obtain an approximation to a circle.
regular_poly <- function(x0, y0, r, n=101, ...)
{
theta <- seq(0, 2*pi, length.out=n+1)[-1]
polygon(x0+r*cos(theta), y0+r*sin(theta), ...)
}
par(mar=rep(0.5, 4)); plot.new(); plot.window(c(0, 9.5), c(-1, 1), asp=1)
regular_poly(1, 0, 1, n=3)
regular_poly(3.5, 0, 1, n=7, density=15, angle=45, col="tan", border="red")
regular_poly(6, 0, 1, n=10, density=8, angle=-60, lty=3, lwd=2)
regular_poly(8.5, 0, 1, n=100, border="brown", col="lightyellow")
Note the asp=1
argument to the plot.window function
(which we detail below) that guarantees the same scaling of the
x- and y-axes. This way, the circle looks like one and not an oval.
Notice that the last vertex adjoins the first one. Also, if we are absent-minded (or particularly creative), we can produce self-intersecting or otherwise degenerate shapes.
Implement your version of the rect function using a call to polygon.
13.1.4. Text¶
A call to text draws arbitrary strings (newlines and tabs
are supported) centred at the specified points.
Moreover, by setting the pos
argument, the labels may be placed
below, to the left of, etc., the pivots.
Some further position adjustments are also possible (adj
, offset
);
see Figure 13.8.
col
specifies the colour, cex
affects the size, and srt
changes the rotation of the text.
On many graphics devices, we have little but crude control over
the font face used: family
chooses a generic font family
("sans"
, "serif"
, "mono"
), and font
selects between the normal variant (1
), bold (2
),
italic (3
), or bold italic (4
).
See, however, Section 13.2.6 for some workarounds.
Note
(*) There is limited support for mathematical symbols and formulae. It relies
on some quirky syntax that we enter using unevaluated R expressions
(Chapter 15). Still, it should be enough to meet our most basic
needs. For instance, passing quote(beta[i]^j)
as the labels
argument to text will output “\(\beta_i^j\)”.
See help("plotmath")
for more details.
For more sophisticated mathematical typesetting, see the tikzDevice graphics device mentioned in Section 13.2.6. It outputs plot specifications that can be rendered by the LaTeX typesetting system.
13.1.5. Raster images (bitmaps) (*)¶
Raster images are encoded in the form of bitmaps, i.e., matrices whose elements represent pixels (see Figure 13.2 for an example). They can be used for drawing heat maps or backgrounds of contour plots; see Section 13.3.4.
Optionally, bilinear interpolation can be applied if the drawing area is larger than the true bitmap size, and we would like to smoothen the colour transitions out. Figure 13.9 presents a very stretched \(4\times 3\) pixel image, with and without interpolation.
par(mar=rep(0.5, 4)); plot.new(); plot.window(c(0, 9), c(0, 1))
I <- matrix(nrow=4, byrow=TRUE,
c( "red", "yellow", "white",
"yellow", "yellow", "orange",
"yellow", "orange", "orange",
"white", "orange", "red")
)
rasterImage(I, 0, 0, 4, 1) # interpolate=TRUE; left subplot
rasterImage(I, 5, 0, 9, 1, interpolate=FALSE) # right subplot
13.2. Graphics settings¶
par can be used to query and modify (as long
as they are not read-only) many graphics options.
For instance, we have several parameters related to the current page
or device settings,
e.g., the plot’s margins (see Section 13.2.2)
or user coordinates (see Section 13.2.3).
The reference list of available parameters is given in help("par")
.
Below we discuss the most noteworthy ones.
Moreover, some functions source[1] the values of their default
arguments by querying par. This is the case of,
e.g., col
, pch
, lty
in the points and lines
function.
Study the following pseudocode.
lines(x, y) # use the default `lty`, i.e., par("lty") == "solid"
old_settings <- par(lty="dashed") # change setting, save old for reference
lines(x, y) # use the new default `lty`, i.e., par("lty") == "dashed"
lines(x, y, lty=3) # use the given `lty`, but only for this call
lines(x, y) # default lty="dashed" again
par(old_settings) # restore the previous settings
lines(x, y) # lty="solid" now
13.2.1. Colours¶
Many functions allow for customising colours of the plotted objects
or their parts; compare, e.g., col
and border
arguments
to polygon, or col
and bg
to points.
There are a few ways to specify colours
(see the Colour Specification section of help("par")
for more details).
We can use a
"colour name"
string, being one of the 657 predefined tags known to the colours function:sample(colours(), 8) # this is just a sample ## [1] "grey23" "darksalmon" "tan3" "violetred4" ## [5] "lightblue1" "darkorchid3" "darkseagreen1" "slategray3"
We can pass a
"#rrggbb"
string, which specifies a position in the RGB colour space: three series of hexadecimal numbers of two digits each, i.e., between \(\text{00}_\text{hex}=0\) (off) and \(\text{FF}_\text{hex}=255\) (full on), giving the intensity of the red, green, and blue channels[2].In practice, the col2rgb and rgb functions can convert between the decimal and hexadecimal representations:
C <- c("black", "red", "green", "blue", "cyan", "magenta", "yellow", "grey", "lightgrey", "pink") # example colours (M <- structure(col2rgb(C), dimnames=list(c("R", "G", "B"), C))) ## black red green blue cyan magenta yellow grey lightgrey pink ## R 0 255 0 0 0 255 255 190 211 255 ## G 0 0 255 0 255 0 255 190 211 192 ## B 0 0 0 255 255 255 0 190 211 203 structure(rgb(M[1, ], M[2, ], M[3, ], maxColorValue=255), names=C) ## black red green blue cyan magenta yellow ## "#000000" "#FF0000" "#00FF00" "#0000FF" "#00FFFF" "#FF00FF" "#FFFF00" ## grey lightgrey pink ## "#BEBEBE" "#D3D3D3" "#FFC0CB"
An
"#rrggbbaa"
string is similar, but has the added alpha channel (two additional hexadecimal digits): from \(\text{00}_\text{hex}=0\) denoting fully transparent, to \(\text{FF}_\text{hex}=255\) indicating fully visible (lit) colour; see Figure 13.2 for an example.Semi-transparency (translucency) can significantly enhance the expressivity of our data visualisations; see Figure 13.18 and Figure 13.19.
We can rely on an integer index to select an item from the current palette (with recycling), which we can get or set by a call to palette. Moreover,
0
identifies the background colour, par("bg")
.Integer colour specifiers are particularly valuable when plotting data in groups defined by factor objects. The underlying integer level codes can be mapped to consecutive colours from any palette; see Figure 13.17 below for an example.
We recommend memorising the colours in the default palette:
palette() # get current palette
## [1] "black" "#DF536B" "#61D04F" "#2297E6" "#28E2E5" "#CD0BBC" "#F5C710"
## [8] "gray62"
These are[3], in order: black, red, green, blue, cyan, magenta, yellow, and grey; see[4] Figure 13.10.
k <- length(palette())
par(mar=rep(0.5, 4)); plot.new(); plot.window(c(0, k+1), c(0, 1))
points(1:k, rep(0.5, k), col=1:k, pch=16, cex=3)
text(1:k, 0.5, palette(), pos=rep(c(1, 3), length.out=k), col=1:k, offset=1)
text(1:k, 0.5, 1:k, pos=rep(c(3, 1), length.out=k), col=1:k, offset=1)
Choosing usable colours requires talents that most programmers lack. Therefore, we will find ourselves relying on the built-in colour sets. palette.pals and hcl.pals return the names of the available discrete (qualitative) palettes. Then, palette.colors and hcl.colors (note the American spelling) can generate a given number of colours from a particular named set.
Continuous (quantitative) palettes are also available, see rainbow, heat.colors, terrain.colors, topo.colors, cm.colors, and gray.colors. They transition smoothly between predefined pivot colours, e.g., from blue through green to brown (like in a topographic map with elevation colouring). They may be of use, e.g., when drawing contour plots; compare Figure 13.27.
Create a demo of the aforementioned palettes in a similar (or nicer) style to that in Figure 13.11.
13.2.2. Plot margins and clipping regions¶
A device (page) region represents a single plot window, one raster image file, or a page in a PDF document (see Section 13.2.6 for more information on graphics devices). As we will learn from Section 13.2.5, it is capable of holding many figures.
Usually, however, we draw one figure per page. In such a case, the device region is divided into the following parts:
outer margins, which can be set via, e.g., the
oma
graphics parameter (in text lines, based on the height of the default font); by default, they are equal to 0;figure region:
a) inner (plot) margins, by default
mar=
c(5.1, 4.1, 4.1, 2.1)
text lines (bottom, left, top, right, respectively); this is where we usually emplace the figure title, axes labels, etc.b) plot region, where we draw graphics primitives positioned relative to the user coordinates.
Note
Typically, all drawings are clipped to the plot region,
but this can be changed with the xpd
parameter;
see also the more flexible clip function.
Figure 13.12 shows the default page layout. In the code chunk below, note the use of mtext to print a text line in the inner margins, box to draw a rectangle around the plot or figure region, axis to add the two axes (labels and tick marks), and title to print the descriptive labels.
plot.new(); plot.window(c(-2, 2), c(-1, 1)) # whatever
for (i in 1:4) { # some text lines on the inner margins
for (j in seq_len(par("mar")[i]))
mtext(sprintf("Text line %d on inner margin %d", j, i),
side=i, line=j-1, col="lightgray")
}
title(main="Main", sub="sub", xlab="xlab", ylab="ylab")
box("figure", lty="dashed") # a box around the figure region
box("plot") # a box around the plot region
axis(1) # horizontal axis (bottom)
axis(2) # vertical axis (left)
rect(-10, -10, 10, 10, col="lightgray") # rectangle (clipped to plot region)
text(0, 0, "Plot region")
lines(c(-3, 0, 3), c(-2, 2, -2)) # standard clipping (plot region)
lines(c(-3, 0, 3), c(-2, 1.25, -2), xpd=TRUE, lty=3) # clip to figure region
13.2.3. User coordinates and axes¶
plot.window sets the user coordinates. It accepts the following parameters:
xlim
,ylim
– vectors of length two giving the minimal and maximal ranges on the respective axes; by default, they are extended by 4% in each direction for aesthetic reasons (see, e.g., Figure 13.12) but we can disable this behaviour by setting thexaxs
andyaxs
graphics parameters;asp
– aspect ratio \((y/x)\); defaults toNA
, i.e., no adjustment; useasp=1
for circles to look like ones, and not ovals;log
– logarithmic scaling on particular axes:""
(none; default),"x"
,"y"
, or"xy"
.
The graphics parameter usr
can be used to read (and set)
the extremes of the user coordinates in the form
\((x_1, x_2, y_1, y_2)\).
plot.new()
plot.window(c(-1, 1), c(1, 1000), log="y", yaxs="i")
par("usr")
## [1] -1.08 1.08 0.00 3.00
Indeed, the x-axis range was extended by 4% in each direction
(xaxs="r"
). We have turned this behaviour off for the y-axis (yaxs="i"
),
which uses the base-10 logarithmic scale. In this case, its actual range is
10^
par("usr")[3:4]
because \(\log_{10} 1=0\) and \(\log_{10} 1000=3\).
Implement your version of the abline function using lines.
Even though axes (labels and tick marks) can be drawn manually using the aforementioned graphics primitives, it is usually too tedious a work. This is why we tend to rely on the axis function, which draws the object on one of the plot sides (as usual, 1=bottom, …, 4=right).
Once plot.window is called, axTicks
can be called to guesstimate the tasteful (round) locations for the tick
marks relative to the current plot size.
By default, they are based on the xaxp
and yaxp
graphics parameters,
which give the axis ranges and the number of intervals between
the tick marks.
plot.new(); plot.window(c(-0.9, 1.05), c(1, 11))
par("usr") # (x1, x2, y1, y2)
## [1] -0.978 1.128 0.600 11.400
par("yaxp") # (y1, y2, n)
## [1] 2 10 4
axTicks(2) # left y-axis
## [1] 2 4 6 8 10
par("xaxp") # (x1, x2, n)
## [1] -0.5 1.0 3.0
axTicks(1) # bottom x-axis
## [1] -0.5 0.0 0.5 1.0
par(xaxp=c(-0.9, 1.0, 5)) # change
axTicks(1)
## [1] -0.90 -0.52 -0.14 0.24 0.62 1.00
axis relies on the same algorithm as axTicks. Alternatively, we can provide custom tick locations and labels.
Most of the plots in this book use the following graphics settings
(except las=1
to axis(2)
); see
Figure 13.13. Check out
help("par")
, help("axis")
, etc. and tune them up to suit your needs.
par(mar=c(2.2, 2.2, 1.2, 0.6))
par(tcl=0.25) # the length of the tick marks (fraction of text line height)
par(mgp=c(1.1, 0.2, 0)) # axis title, axis labels, and axis line location
par(cex.main=1, font.main=2) # bold, normal size - main in title
par(cex.axis=0.8889)
par(cex.lab=1, font.lab=3) # bold italic, normal size
plot.new(); plot.window(c(0, 1), c(0, 1))
# a "grid":
rect(par("usr")[1], par("usr")[3], par("usr")[2], par("usr")[4],
col="#00000010")
abline(v=axTicks(2), col="white", lwd=1.5, lty=1)
abline(h=seq(0, 1, length.out=4), col="white", lwd=1.5, lty=1)
# set up axes:
axis(2, at=seq(0, 1, length.out=4), c("0", "1/3", "2/3", "1"), las=1)
axis(1)
title(xlab="xlab", ylab="ylab", main="main (use sparingly)")
box()
13.2.4. Plot dimensions (*)¶
Certain sizes can be read or specified in inches (1” is exactly 25.4 mm):
pin
– plot dimensions (width, height),fin
– figure region dimensions,din
– page (device) dimensions,mai
– plot (inner) margin size,omi
– outer margins,cin
– the size of the “default” character (width, height).
If the figure is scaled, the virtual inch (the one reported by R) will not match the physical one (e.g., the actual size in the printed version of this book or on the computer screen).
Important
Most objects’ positions are specified
in virtual user coordinates, as given by usr
.
They are automatically mapped to the physical device region,
taking into account the page size, outer and inner margins, etc.
Knowing the above, some scaling can be used to convert between the user
and physical sizes (in inches). It is based on the ratios
(usr[2]-usr[1])/pin[1]
and (usr[4]-usr[3])/pin[2]
;
compare the xinch and yinch functions.
(*) Figure 13.14 shows how we can pinpoint the edges of the figure and device region in user coordinates.
(*) We cannot use mtext to print
text on the right inner margin rotated by 180 degrees
compared to what we see in Figure 13.12.
Write your version of this function that will allow you to
do so.
Hint: use text, the cin
graphics parameter,
and what you can read from Figure 13.14.
13.2.5. Many figures on one page (subplots)¶
It is possible to create many figures on one page. In such a case, each subplot has its own inner margins and plot region.
A call to par(mfrow=
c(nr, nc))
or par(mfcol=
c(nr, nc))
splits the page into a regular grid with nr
rows and nc
columns.
Each invocation of plot.new starts a new figure.
Consecutive figures are either placed rowwisely (mfrow
)
or in the column-major order (mfcol
). Alternatively,
any subplot can be activated by referring to the mfg
parameter.
Figure 13.15 depicts an example page with four figures aligned on a \(2\times 2\) grid.
par(oma=rep(1.2, 4)) # outer margins (default 0)
par(mfrow=c(2, 2)) # a 2x2 plot grid
for (i in 1:4) {
plot.new()
par(mar=c(3, 3, 2, 2)) # each subplot will have the same inner margins
plot.window(c(i-1, i+1), c(-1, 1)) # separate user coordinates for each
text(i, 0, sprintf("Plot region (plot %d)\n(%d, %d)", i,
par("mfg")[1], par("mfg")[2]))
box("figure", lty="dashed") # a box around the figure region
box("plot") # a box around the plot region
axis(1) # horizontal axis (bottom)
axis(2) # vertical axis (left)
}
box("outer", lty="dotdash") # a box around the whole page
for (i in 1:4)
mtext(sprintf("Outer margin %d", i), side=i, outer=TRUE)
Thanks to mfrow
and mfcol
, we can create, e.g., a scatter plot matrix
or different trellis plots. If an irregular grid is required, we can call
the slightly more sophisticated layout function
(which is incompatible with mfrow
and mfcol
).
Examples will follow later; see
Figure 13.24 and Figure 13.26.
Also, the fig
parameter (with new=TRUE
to suppress the
creation of a new figure) creates a subplot in an arbitrary rectangular
region of the current page.
Certain grid sizes might affect the mex
and cex
parameters
and hence the default font sizes (amongst others).
Refer to the documentation of par for more details.
13.2.6. Graphics devices¶
Where our plots are displayed depends on our development environment
(Section 1.2). Users of JupyterLab see the plots embedded
into the current notebook, consumers of RStudio display them
in a dedicated Plots pane, working from the console opens a new graphics
window (unless we work in a text-only environment), whereas compiling
utils::
Sweave or knitr
markup files brings about an image file that will be included
in the output document.
In practice, we might be interested in exercising our creative endeavours on different devices. For instance, to draw something in a PDF file, we can call:
Cairo::CairoPDF("figure.pdf", width=6, height=3.5) # open "device"
# ... calls to plotting functions...
dev.off() # save file, close device
Similarly, a call to CairoPNG or CairoSVG
creates a PNG or a SVG file.
In both cases, as we rely on the Cairo library,
we can customise the font family by calling
Cairo::
CairoFonts.
Note
Typically, web browsers can display PNG, JPEG, and SVG files. On the other hand, PDF is a popular choice in printed publications (e.g., articles or books).
It is worth knowing that PNG and JPEG are raster graphics formats, i.e., they store figures as bitmaps (pixel matrices). They are fast to render, but the file sizes might become immense if we want decent image quality (high resolution). Most importantly, they should not be scaled: it is best to display them at their original widths and heights. However, JPEG uses lossy compression. Therefore, it is not a particularly fortunate file format for data visualisations. It does not support transparency either.
On the other hand, SVG and PDF files store vector graphics, where all primitives are described geometrically. This way, the image can be redrawn at any size and is always expected to be aesthetic. Unfortunately, scatter plots with millions of points will result in considerable files size and relatively slow rendition times (but there are tricks to remedy this).
Users of TeX should take note of
tikzDevice::
tikz, which creates
TikZ files that can be rendered as standalone PDF files
or embedded in LaTeX documents (and its variants).
It allows for typesetting beautiful equations using the standard
"$...$"
syntax within any R string.
Many other devices are listed in help("Devices")
.
Note
(*) The opened graphics devices form a stack. Calling dev.off will return to the last opened device (if any). See dev.list and other functions listed in its help page for more information.
Each device has separate graphics parameters. When opening a new device, we start with default settings in place.
Also, dev.hold and dev.flush can suppress the immediate display of the plotted objects, which might increase the drawing speed on certain interactive devices.
The current plot can be copied to another device (e.g., a PDF file) using dev.print.
(*) Create an animated PNG displaying a large point sliding along the sine curve. Generate a series of video frames like in Figure 13.16. Store each frame in a separate PNG file. Then, use ImageMagick (compare Section 7.3.2 or rely on another tool) to combine these files as a single animated PNG.
13.3. Higher-level functions¶
Higher-level plotting commands call plot.new, plot.window, axis, box, title, etc., and draw graphics primitives on our behalf. They provide ready-to-use implementations of the most common data visualisation tools, e.g., box-and-whisker plots, histograms, pairs plots, etc. Below we review a few of them. We also show how they can be customised or even rewritten from scratch if we are not completely happy with them. They will inspire us to practice lower-level graphics programming.
Check out the meaning of the ask
, new
, xaxt
, yaxt
, and ann
graphics parameters and how they affect plot.new, axis,
title, and so forth.
13.3.1. Scatter and function plots with plot.default and matplot¶
The default method for the S3 generic plot is a convenient wrapper around points and lines.
plot.default can draw a scatter plot of a set of points
in \(\mathbb{R}^2\) possibly grouped by another categorical variable.
From Section 10.3.2 we know that a factor is represented as a vector
of small natural numbers. Therefore, its underlying level codes
can be used directly as col
or pch
specifiers;
see Figure 13.17 for a demonstration.
Take note of a call to the legend function.
plot(
jitter(iris[["Sepal.Length"]]), # x (it is a numeric vector)
jitter(iris[["Petal.Width"]]), # y (it is a numeric vector)
col=as.numeric(iris[["Species"]]), # colours (integer codes)
pch=as.numeric(iris[["Species"]]), # plotting symbols (integer codes)
xlab="Sepal length", ylab="Petal width",
asp=1 # y/x aspect ratio
)
legend(
"bottomright",
legend=levels(iris[["Species"]]),
col=seq_along(levels(iris[["Species"]])),
pch=seq_along(levels(iris[["Species"]])),
bg="white"
)
Pass ann=FALSE
and axes=FALSE
to plot to
suppress the addition of axes and labels. Then, draw them manually
using the functions discussed in the previous section.
Draw a plot of the \(y=\sin x\) function using plot. Then, call lines to add \(y=\cos x\). Later, do the same using a single reference to matplot. Include a legend.
Semi-transparency may convey additional information. Figure 13.18 shows two scatter plots of adult females’ weights vs heights. If the points are fully opaque, we cannot judge the density around them. On the other hand, translucent symbols somewhat imitate the two-dimensional histograms that we will later depict in Figure 13.29.
nhanes <- read.csv(paste0("https://raw.githubusercontent.com/gagolews/",
"teaching-data/master/marek/nhanes_adult_female_bmx_2020.csv"),
comment.char="#", col.names=c("weight", "height", "armlen", "leglen",
"armcirc", "hipcirc", "waistcirc"))
par(mfrow=c(1, 2))
for (col in c("black", "#00000010"))
plot(nhanes[["height"]], nhanes[["weight"]], col=col,
pch=16, xlab="Height", ylab="Weight")
Figure 13.19 depicts the average monthly temperatures in your next holiday destination: Warsaw, Poland (a time series). Note that the translucent ribbon representing the low-high average temperature intervals was added using a call to polygon.
# Warsaw monthly temperatures; source: https://en.wikipedia.org/wiki/Warsaw
high <- c( 0.6, 1.9, 6.6, 13.6, 19.5, 21.9,
24.4, 23.9, 18.4, 12.7, 5.9, 1.6)
mean <- c(-1.8, -0.6, 2.8, 8.7, 14.2, 17.0,
19.2, 18.3, 13.5, 8.5, 3.3, -0.7)
low <- c(-4.2, -3.6, -0.6, 3.9, 8.9, 11.8,
13.9, 13.1, 9.1, 4.8, 0.6, -3.0)
matplot(1:12, cbind(high, mean, low), type="o", col=c(2, 1, 4), lty=1,
xlab="month", ylab="temperature [°C]", xaxt="n", pch=16, cex=0.5)
axis(1, at=1:12, labels=month.abb, line=-0.25, lwd=0, lwd.ticks=1)
polygon(c(1:12, rev(1:12)), c(high, rev(low)), border=NA, col="#ffff0033")
legend("bottom", c("average high", "mean", "average low"),
lty=1, col=c(2, 1, 4), bg="white")
Figure 13.20 depicts a scatter plot similar to Figure 13.18, but now with the points’ hue being a function of a third variable.
midpoints <- function(x) 0.5*(x[-1]+x[-length(x)])
z <- nhanes[["waistcirc"]]
breaks <- seq(min(z), max(z), length.out=10)
zf <- cut(z, breaks, include.lowest=TRUE)
col <- hcl.colors(nlevels(zf), "Viridis", alpha=0.5)
layout(matrix(c(1, 2), nrow=1), # two plots in one page
widths=c(1, lcm(3))) # second one is of width "3cm" (scaled)
# first subplot:
plot(nhanes[["height"]], nhanes[["weight"]], col=col[as.numeric(zf)],
pch=16, xlab="Height", ylab="Weight")
# second subplot:
par(mar=c(2.2, 0.6, 2.2, 0.6))
plot.new(); plot.window(c(0, 1), c(0, nlevels(zf)))
rasterImage(as.matrix(rev(col)), 0, 0, 1, nlevels(zf), interpolate=FALSE)
text(0.5, 1:nlevels(zf)-0.5, sprintf("%3.0f", midpoints(breaks)))
mtext("Waist Ø", side=3)
Implement your version of pairs, being the function to draw a scatter plot matrix (a pairs plot).
ecdf returns an object of the S3 classes ecdf
and stepfun
.
There are plot methods overloaded for them. Inspect their source
code. Then, inspired by this, create a function to compute and display
the empirical cumulative distribution function corresponding to a given
numeric vector.
spline performs cubic spline interpolation,
whereas smooth.spline determines a smoothing spline
of a given two-dimensional dataset.
Plot different splines for cars[["dist"]]
as a function
of cars[["speed"]]
. Which of these two functions is more appropriate
for depicting this dataset?
13.3.2. Bar plots and histograms¶
A bar plot is drawn using a series of rectangles (i.e., certain polygons) of different heights (or widths, if we request horizontal alignment).
Let’s visualise the dataset listing the most frequent causes of medication errors (data are fabricated):
cat_med = c(
"Unauthorised drug", "Wrong IV rate", "Wrong patient", "Dose missed",
"Underdose", "Wrong calculation","Wrong route", "Wrong drug",
"Wrong time", "Technique error", "Duplicated drugs", "Overdose"
)
counts_med = c(1, 4, 53, 92, 7, 16, 27, 76, 83, 3, 9, 59)
A Pareto chart combines a bar plot featuring bars of decreasing heights with a cumulative percentage curve; see Figure 13.21.
o <- order(counts_med)
cato_med <- cat_med[o]
pcto_med <- counts_med[o]/sum(counts_med)*100
cumpcto_med <- rev(cumsum(rev(pcto_med)))
# bar plot of percentages
par(mar=c(2.2, 0.6, 2.2, 6.6)) # wide left margin
midp <- barplot(pcto_med, horiz=TRUE, xlab="%",
col="white", xlim=c(0, 25), xaxs="r", yaxs="r", yaxt="n",
width=3/4, space=1/3)
text(pcto_med, midp, sprintf("%.1f%%", pcto_med), pos=4, cex=0.89)
axis(4, at=midp, labels=cato_med, las=1)
box()
# cumulative percentage curve in a new coordinate system
par(usr=c(-4, 104, par("usr")[3], par("usr")[4])) # 0-100 with 4% addition
lines(cumpcto_med, midp, type="o", col=4, pch=18)
axis(3, col=4)
mtext("cumulative %", side=3, line=1.2, col=4)
text(cumpcto_med, midp, sprintf("%.1f%%", cumpcto_med), cex=0.89, col=4,
pos=c(4, 2)[(cumpcto_med>80)+1], offset=0.5)
Note that barplot returned the midpoints of the bars,
which we put in good use.
By default, it sets the xaxs="i"
graphics parameter and thus
does not extend the x-axis range by 4% on both sides. This would not make
us happy here, therefore we needed to change it manually.
Draw a bar plot summarising, for each passenger class and sex,
the number of adults who did not survive the sinking of the deadliest
1912 cruise; see Figure 13.22 and the
Titanic
dataset.
Implement your version of barplot, but where the bars are placed precisely at the positions specified by the user, e.g., allowing the bar midpoints to be consecutive integers.
We will definitely not cover the (in)famous pie charts in our book. The human brain is not very skilled at judging the relative differences between the areas of geometric objects. Also, they are ugly (pie charts, not geometric objects in general).
Moving on: a histogram is a simple density estimator for continuous data. It can be thought of as a bar plot with bars of heights proportional to the number of observations falling into the corresponding disjoint intervals. Most often, there is no space between the bars to emphasise that the intervals cover the whole data range.
A histogram can be computed and drawn using the high-level function hist; see Figure 13.23.
par(mfrow=c(1, 2))
for (breaks in list("Sturges", 25)) {
# Sturges (a heuristic) is the default; any value is merely a suggestion
hist(iris[["Sepal.Length"]], probability=TRUE, xlab="Sepal length",
main=NA, breaks=breaks, col="white")
box() # oddly, we need to add it manually
}
Study the source code of hist.default.
Note the invisibly-returned list of the S3 class histogram
.
Then, study graphics:::
plot.histogram.
Implement similar functions yourself.
Modify your function to draw a scatter plot matrix so that it gives the histograms of the marginal distributions on its diagonal.
Using layout mentioned in Section 13.2.5,
we can draw a scatter plot with marginal histograms;
see Figure 13.24.
Note that we split the page into four plots of unequal sizes,
but the upper right part of the grid is unused.
We use hist for binning only (plot=FALSE
). Then,
barplot is utilised for drawing as it gives greater control
over the process (e.g., supports vertical layout).
layout(matrix(
c(1, 1, 1, 0, # the first row: the first plot of width 3 and nothing
3, 3, 3, 2, # the third plot (square) and the second (tall) in 3 rows
3, 3, 3, 2,
3, 3, 3, 2), nrow=4, byrow=TRUE))
par(mex=1, cex=1) # the layout function changed this!
x <- jitter(iris[["Sepal.Length"]])
y <- jitter(iris[["Sepal.Width"]])
# the first subplot (top)
par(mar=c(0.2, 2.2, 0.6, 0.2), ann=FALSE)
hx <- hist(x, plot=FALSE, breaks=seq(min(x), max(x), length.out=20))
barplot(hx[["density"]], space=0, axes=FALSE, col="#00000011")
# the second subplot (right)
par(mar=c(2.2, 0.2, 0.2, 0.6), ann=FALSE)
hy <- hist(y, plot=FALSE, breaks=seq(min(y), max(y), length.out=20))
barplot(hy[["density"]], space=0, axes=FALSE, horiz=TRUE, col="#00000011")
# the third subplot (square)
par(mar=c(2.2, 2.2, 0.2, 0.2), ann=TRUE)
plot(x, y, xlab="Sepal length", ylab="Sepal width",
xlim=range(x), ylim=range(y)) # default xlim, ylim
(*)
Kernel density estimators (KDEs) are another way to guesstimate
the data distribution. The density function, for a given
numeric vector, returns a list with, amongst others,
the x
and y
coordinates of the points that we can pass directly to
the lines function.
Below we depict the KDEs of data split into three groups;
see Figure 13.25.
adjust_transparency <- function(col, alpha)
rgb(t(col2rgb(col)/255), alpha=alpha) # alpha in [0, 1]
pal <- adjust_transparency(palette(), 0.2)
kdes <- lapply(split(iris[["Sepal.Length"]], iris[["Species"]]), density)
matplot(sapply(kdes, `[[`, "x"), sapply(kdes, `[[`, "y"),
type="l", xlab="Sepal length", ylab="density", lwd=1.5)
for (i in seq_along(kdes))
polygon(kdes[[i]][["x"]], kdes[[i]][["y"]], col=pal[i], border=NA)
legend("topright", legend=levels(iris[["Species"]]), bg="white", lwd=1.5,
col=seq_along(levels(iris[["Species"]])),
lty=seq_along(levels(iris[["Species"]])))
(*) Implement a function that draws kernel density estimators for a given numeric variable split by a combination of three factor levels; see Figure 13.26 for an example.
grid_kde <- function(data, values, x, y, hue) ...to.do...
tips <- read.csv(paste0("https://raw.githubusercontent.com/gagolews/",
"teaching-data/master/other/tips.csv"),
comment.char="#", stringsAsFactors=TRUE)
head(tips, 3) # preview this example dataset
## total_bill tip sex smoker day time size
## 1 16.99 1.01 Female No Sun Dinner 2
## 2 10.34 1.66 Male No Sun Dinner 3
## 3 21.01 3.50 Male No Sun Dinner 3
grid_kde(tips, values="tip", x="smoker", y="time", hue="sex")
13.3.3. Box-and-whisker plots¶
We have already seen a chart generated by the boxplot function in Figure 5.1. Tinkering with it will give us robust practice, which in turn shall make us perfect.
Modify the code generating Figure 5.1 so that:
same
dose
s are grouped together (more space between differentdose
s added; also, on the x-axis, only uniquedose
s are printed),different
supp
s have different colours (add a legend explaining them).
Write a function for drawing box plots using graphics primitives.
(*) Write a function for drawing violin plots. They are similar to box plots but use kernel density estimators.
(*) Implement a bag plot, which is a two-dimensional version of a box plot. Use chull to compute the convex hull of a point set.
13.3.4. Contour plots and heat maps¶
image is a convenient wrapper around rasterImage, which can draw contour plots, two-dimensional histograms, heatmaps, etc. In particular, when plotting a function of two variables like \(z=f(x, y)\), the magnitude of the \(z\) component can be expressed using colour brightness or hue.
Figure 13.27 presents a filled contour plot of Himmelblau’s function, \(f(x,y)=(x^{2}+y-11)^{2}+(x+y^{2}-7)^{2}\), for \(x\in[-5, 5]\) and \(y\in[-4, 4]\). A call to contour adds labelled contour lines (which is actually a nontrivial operation).
x <- seq(-5, 5, length.out=250)
y <- seq(-4, 4, length.out=200)
z <- outer(x, y, function(xg, yg) (xg^2 + yg - 11)^2 + (xg + yg^2 - 7)^2)
image(x, y, z, col=grey(seq(1, 0, length.out=16)))
contour(x, y, z, nlevels=16, add=TRUE)
In image, the number of rows in z
matches the length of x
,
whereas the number of columns is equal to the size of y
.
This might be counterintuitive; when z
is printed,
the image is its 90-degree rotated version.
Figure 13.28 presents an example heatmap
depicting Pearson’s correlations between all pairs of variables
in the nhanes
data frame which we loaded some time ago.
o <- c(6, 5, 1, 7, 4, 2, 3) # order of rows/cols (by similarity)
R <- cor(nhanes[o, o])
par(mar=c(2.8, 7.6, 1.2, 7.6), ann=FALSE)
image(1:NROW(R), 1:NCOL(R), R,
ylim=c(NROW(R)+0.5, 0.5),
zlim=c(-1, 1),
col=hcl.colors(20, "BluGrn", rev=TRUE),
xlab=NA, ylab=NA, asp=1, axes=FALSE)
axis(1, at=1:NROW(R), labels=dimnames(R)[[1]], las=2, line=FALSE, tick=FALSE)
axis(2, at=1:NCOL(R), labels=dimnames(R)[[2]], las=1, line=FALSE, tick=FALSE)
text(arrayInd(seq_along(R), dim(R)),
labels=sprintf("%.2f", R),
col=c("white", "black")[abs(R<0.8)+1],
cex=0.89)
Check out the heatmap function, which uses hierarchical clustering to find an aesthetic reordering of the matrix’s items.
Figure 13.29 depicts a two-dimensional histogram. It approaches the idea of reflecting the points’ density differently from the semi-transparent symbols in Figure 13.18.
histogram_2d <- function(x, y, k=25, ...)
{
breaksx <- seq(min(x), max(x), length.out=k)
fx <- cut(x, breaksx, include.lowest=TRUE)
breaksy <- seq(min(y), max(y), length.out=k)
fy <- cut(y, breaksy, include.lowest=TRUE)
C <- table(fx, fy)
image(midpoints(breaksx), midpoints(breaksy), C,
xaxs="r", yaxs="r", ...)
}
par(mfrow=c(1, 2))
for (k in c(25, 50))
histogram_2d(nhanes[["height"]], nhanes[["weight"]], k=k,
xlab="Height", ylab="Weight",
col=c("#ffffff00", hcl.colors(25, "Viridis", rev=TRUE))
)
(*) Implement some two-dimensional kernel density estimator and plot it using contour.
13.4. Exercises¶
Answer the following questions.
Can functions from the graphics package be used to adjust the plots generated by lattice and ggplot2?
What are the most common graphics primitives?
Can all high-level functions be implemented using low-level ones? As an example, discuss the key ingredients used in barplot.
Some high-level functions discussed in this chapter carry the
add
parameter. What is its purpose?What are the admissible values of
pch
andlty
? Also, in the default palette, what is the meaning of colours1
,2
, …,16
? Can their meaning be changed?Can all graphics parameters be changed?
What is the difference between passing
xaxt="n"
to plot.default vs setting it with par, and then calling plot.default?Which graphics parameters are set by plot.window?
What is the meaning of the
usr
parameter when using the logarithmic scale on the x-axis?(*) How to place a plotting symbol exactly 1 centimetre from the top-left corner of the current page (following the page’s diagonal)?
Semi-transparent polygons are nice, right?
Can an ellipse be drawn using polygon?
What happens when we set the graphics parameter
mfrow=
c(2, 2)
?How to export the current plot to a PDF file?
Draw the 2022 BTC-to-USD close rates time series. Then, add the 7- and 30-day moving averages. (*) Also, fit a local polynomial (moving) regression model using the Savitzky–Golay filter (see loess).
(*) Draw (from scratch) a candlestick plot for the 2022 BTC-to-USD rates.
(*) Create a function to draw a normal quantile-quantile (Q-Q) plot, i.e., for inspecting whether a numeric sample might come from a normal distribution.
(*) Draw a map of the world, where each country is filled with a colour whose brightness or hue is linked to its Gini index of income inequality. You can easily find the data on Wikipedia. Try to find an open dataset that gives the borders of each country as vertices of a polygon (e.g., in the form of a (geo)JSON file).
Next time you see a pleasant data visualisation somewhere, try to reproduce it using base graphics.
For further information on graphics generation in R, see, e.g., Chapter 12 of [58], [48], and [52]. Good introductory textbooks to data visualisation as an art include [56, 59].
In this chapter, we were only interested in static graphics, e.g., for use in printed publications or plain websites. Interactive plots that a user might tinker with in a web browser are a different story.
And so the second part of our delightful course is ended.