blob: e9b760909622cc69bd6fa6a2b8519de4a31043df [file] [log] [blame]
% File src/library/grid/vignettes/frame.Rnw
% Part of the R package, https://www.R-project.org
% Copyright 2001-13 Paul Murrell and the R Core Team
% Distributed under GPL 2 or later
\documentclass[a4paper]{article}
%\VignetteIndexEntry{Frames and packing grobs}
%\VignettePackage{grid}
\newcommand{\code}[1]{\texttt{#1}}
\newcommand{\pkg}[1]{{\normalfont\fontseries{b}\selectfont #1}}
\newcommand{\grid}{\pkg{grid}}
\newcommand{\R}{{\sffamily R}}
\setlength{\parindent}{0in}
\setlength{\parskip}{.1in}
\setlength{\textwidth}{140mm}
\setlength{\oddsidemargin}{10mm}
\title{A GUI-Builder Approach to \grid{} Graphics}
\author{Paul Murrell}
\begin{document}
\maketitle
<<echo=FALSE, results=hide>>=
library(grDevices)
library(stats) # for runif
library(grid)
ps.options(pointsize = 12)
options(width = 60)
@
Grid contains a lot of support for locating, sizing, and arranging
graphical components on a device and with respect to each other.
However, most of this support relies on \emph{either} the parent
object dictating both location and size (layouts) \emph{or} the child
dictating both location and size.
Some sorts of arrangements are more conveniently handled by having the
parent dictate the location, but letting the child dictate the size.
This is the situation for GUI-builders (software which forms
arrangements of GUI components or widgets). The approach taken by
(many ?) GUI-builders is to allow the user to create a parent
\emph{frame} and then \emph{pack} widgets into this frame. The frame
locates and arranges the children with the help of hints such as
``place this widget at the bottom of the frame'', and the children
dictate what size they would like to be.
This document describes a first attempt at such an interface for
arranging Grid graphical objects.
\section*{The \code{"frame"} grob}
You can create a \code{"frame"} graphical object using the function
\code{frameGrob()}. You must assign the result to a variable in order
to pack grobs into it.
<<results=hide>>=
gf <- frameGrob()
@
\section*{The \code{packGrob()} function}
Having created a frame, you can then pack other graphical objects into
it using the \code{packGrob()} function. This function has a complex
interface which allows for a variety of methods of packing graphical
objects. The required arguments are:
\begin{description}
\item[\code{frame}] is a \code{"frame"} object created by \code{grid.frame}.
\item[\code{grob}] is the grob to pack into the frame.
\end{description}
The remaining arguments specify where the grob is located in the frame
and possibly how much space the grob should occupy. The frame is effectively
just a layout; you can add grobs to existing rows and columns
or you can append the grob in a new row and/or column. If the
grob is added to an existing row then the height of
that row becomes the maximum of the new height and
the previous height. If the row is new then it just
gets the specified height. Similar rules apply for column widths.
\begin{description}
\item[\code{col}] is the column to put the grob in. This can be 1 greater than
the existing number of columns (in which case a new column is added).
\item[\code{row}] is like \code{col} but for rows.
\item[\code{col.after}] specifies that the grob should be put in a
new column inserted between \code{col.after} and \code{col.after + 1}.
\item[\code{col.before}] specifies that the grob should be put in a
new column inserted between \code{col.before} and \code{col.before + 1}.
\item[\code{row.after} and \code{row.before}] do what you would expect.
\item[\code{side}] specifies which side to append the new grob to.
The valid values are \code{"left"}, \code{"right"}, \code{"bottom"},
and \code{"top"}.
\item[\code{width}] is the width of the row that the grob is being packed
into. If this is not given then the grob supplies the width.
\item[\code{height}] is like \code{width} but for rows.
\end{description}
It is possible to modify this default behaviour. For example, it is possible
to add a grob to a row and force that row to have the specified height
by setting \code{force.height=TRUE} (and similarly for column widths).
It is also possible to pack a graphical object into several rows or
columns at once (although you cannot simultaneously affect the heights or
widths of those rows and columns).
The result of this function is the modified frame, so you must
assign the result to a variable.
<<results=hide>>=
gf <- packGrob(gf, textGrob("Hello frame!"))
@
\section*{\code{"grobwidth"} and \code{"grobheight"} units}
A \code{"frame"} object allows a grob to specify its size by making
use of \code{"grobwidth"} and \code{"grobheight"} units.
These units may, of course, be used outside of frames too so
their use is described here.
Consider a simple example where I want to draw a rectangle around
a piece of text. I can get the size of the piece of text from the
\code{"text"} grob as follows:
<<frame1, include=FALSE, width=4, height=2, fig=TRUE, results=hide>>=
st <- grid.text("some text")
grid.rect(width = unit(1, "grobwidth", st),
height = unit(1, "grobheight", st))
@
\begin{center}
\includegraphics[height=1in, width=2in]{frame-frame1}
\end{center}
@
You could do the same thing with simple \code{"strwidth"}
and \code{"strheight"} units, but \code{"grobwidth"} and \code{"grobheight"}
give you a lot more power. The biggest gain is that you
can get the size of other objects besides pieces of text (more on that
soon). Another thing you can do is provide a ``reference'' to a grob
rather than the grob itself; you do this by giving the name of a grob.
What this does is make the unit ``dynamic'' so that changes in the
grob affect the unit. The following is a dynamic version of the
previous example.
<<frame1, include=FALSE, width=4, height=2, fig=TRUE, results=hide>>=
grid.text("some text", name = "st")
grid.rect(width = unit(1, "grobwidth", "st"),
height = unit(1, "grobheight", "st"))
@
Now watch what happens if I modify the text grob named \code{"st"}:
<<results=hide, eval=FALSE>>=
grid.edit("st", gp = gpar(fontsize = 20))
<<frame2, echo=FALSE, include=FALSE, width=4, height=2, fig=TRUE, results=hide>>=
my.text <- textGrob("some text")
my.text <- editGrob(my.text, gp = gpar(fontsize = 20))
my.rect <- rectGrob(width = unit(1, "grobwidth", my.text),
height = unit(1, "grobheight", my.text))
grid.draw(my.text)
grid.draw(my.rect)
@
\begin{center}
\includegraphics[height=1in, width=2in]{frame-frame2}
\end{center}
@
Similarly, I can change the text itself:
<<results=hide, eval=FALSE>>=
grid.edit("st", label="some different text")
<<frame3, echo=FALSE, include=FALSE, width=4, height=2, fig=TRUE, results=hide>>=
my.text <- textGrob("some text")
my.text <- editGrob(my.text, gp = gpar(fontsize = 20))
my.text <- editGrob(my.text, label = "some different text")
my.rect <- rectGrob(width = unit(1, "grobwidth", my.text),
height = unit(1, "grobheight", my.text))
grid.draw(my.text)
grid.draw(my.rect)
@
\begin{center}
\includegraphics[height=1in, width=2in]{frame-frame3}
\end{center}
@
\section*{The \code{widthDetails} and \code{heightDetails} generic functions}
The calculation of \code{"grobwidth"} and \code{"grobheight"} units
is a bit complicated, but fortunately most of it is automated.
The simple part is that a grob provides a normal \code{"unit"} object
to express its width or height. The complication comes because that
\code{"unit"} object has to be evaluated in the correct context; in
particular, if the grob has a non-\code{NULL} \code{vp} argument then
those viewports have to be pushed so that the size of the grob is
the size it would be when it is drawn. This is achieved by calling the
\code{preDrawDetails()} function for the grob and in most cases what
happens by default will be correct. The thing to avoid is having
any viewport operations in a \code{drawDetails()} method for your
grob; they should go in a \code{preDrawDetails()} method.
All that needs to be written (usually) are the functions that
provide the \code{"unit"} objects. These functions need to be
\code{widthDetails} and \code{heightDetails} methods.
The default methods return \code{unit(1, "null")}
so your grob will be of this size unless you write your own methods.
The classic example methods are those for \code{"text"} grobs; these
return \code{unit(1, "mystrwidth", <text grob label>)} and
\code{unit(1, "mystrheight", <text grob label>)} respectively.
The other very important examples of these methods are those for
\code{"frame"} grobs. These return the \code{sum} of the
widths (heights) of the columns (rows) of the layout that has been
built up by packing grobs into the frame. This means that
when a \code{"frame"} grob is packed within another \code{"frame"} grob
the parent automatically leaves enough room for the child.
Another useful pair of examples are those for \code{"rect"} grobs.
These methods make use of the \code{absolute.size} function.
When a grob is asked to specify its size, it makes sense to respond
with the grob's width and height if the grob has an ``absolute'' size
(e.g., \code{"inches"}, \code{"cm"}, \code{"lines"}, etc;
i.e., the grob knows exactly how big itself is).
On the other hand, it does not make sense to respond with the grob's
width and height if the grob has a ``relative'' size (e.g., \code{"npc"} or
\code{"native"}; i.e.,
the grob needs to know about it's parent's size before it can figure
out its own). The \code{absolute.size} function leaves absolute
units alone, but converts relative units to \code{"null"} units
(i.e., the child says to the parent, ``you decide how big I should be''), so
you can return something like \code{absolute.size(width(<grob>))} in order
to always give a sensible answer.
@
\section*{Examples}
The original motivating example for this GUI-builder approach was
to be able to produce a quite general-purpose legend grob.
A legend consists of data symbols and associated textual descriptions.
In order to be quite general, it would be nice to allow, for example,
multiple lines of text per data symbol. Rather than having to look at
the text supplied for the legend in order to determine the arrangement
of the legend, it would be nice to be able to simply specify the
composition of the legend and let it figure out the arrangement for
us. The code below defines just such a legend grob, using the new
\code{"frame"} grob and \code{packGrob()} function. Some points to
note are:
\begin{itemize}
\item The use of \code{border}s to create space around the legend
components.
\item The size of the data symbol component is specified, but the
size of the text components are taken from the \code{"text"} grobs.
\item The heights of the rows in the legend will be the maximum of
$\code{vgap} + \code{unit(1, "lines")}$ and $\code{vgap} +
\code{unit(1, "grobheight", <text grob>)}$.
\item We have two functions, one for generating a grob and one
for producing output.
\end{itemize}
<<>>=
legendGrob <- function(pch, labels, frame = TRUE,
hgap = unit(1, "lines"), vgap = unit(1, "lines"),
default.units = "lines",
vp = NULL) {
nkeys <- length(labels)
gf <- frameGrob(vp = vp)
for (i in 1:nkeys) {
if (i == 1) {
symbol.border <- unit.c(vgap, hgap, vgap, hgap)
text.border <- unit.c(vgap, unit(0, "npc"), vgap,
hgap)
} else {
symbol.border <- unit.c(vgap, hgap, unit(0, "npc"), hgap)
text.border <- unit.c(vgap, unit(0, "npc"), unit(0, "npc"), hgap)
}
gf <- packGrob(gf, pointsGrob(0.5, 0.5, pch = pch[i]),
col = 1, row = i, border = symbol.border,
width = unit(1, "lines"),
height = unit(1, "lines"), force.width = TRUE)
gf <- packGrob(gf, textGrob(labels[i], x = 0, y = 0.5,
just = c("left", "centre")),
col = 2, row = i, border = text.border)
}
gf
}
grid.legend <- function(pch, labels, frame = TRUE,
hgap = unit(1, "lines"), vgap = unit(1, "lines"),
default.units = "lines", draw = TRUE,
vp = NULL) {
gf <- legendGrob(pch, labels, frame, hgap, vgap, default.units, vp)
if (draw) grid.draw(gf)
gf
}
@
The next piece of code shows the \code{grid.legend()} function being
used procedurally; the output is shown below the code.
<<legend, include=FALSE, width=4, height=2, fig=TRUE, results=hide>>=
grid.legend(1:3, c("one line", "two\nlines", "three\nlines\nof text"))
@
\begin{center}
\includegraphics[height=2in, width=4in]{frame-legend}
\end{center}
@
The legend example might not seem too difficult to do by hand rather
than using frames and packing, but the next example shows how
useful it can be.
Suppose you want to arrange a legend next to a plot. This requires
leaving enough space for the legend and then filling the remaining
space with the plot. This requires figuring out how much space the
legend needs, and that is a task that is neither trivial nor
easy to cater for in the general case. Ideally, we want to know
as little as possible about the legend.
With the GUI-builder
approach this becomes extremely simple. The code below
shows how the construction of such a scene might be performed; the
output from the code is again shown below.
The following points are noteworthy:
\begin{itemize}
\item We don't need to know anything about how the legend was
constructed; it could be any sort of grob.
\item We specify the height of the legend to be \code{unit(1, "null")}
so that it will occupy the full height of the plot. If we
did not do this then the plot would be forced to be the height of
the legend (because of the way that \code{"null"} units interact
with other units).
\item The width of the legend is calculated from the contents of the
legend because the legend is a \code{"frame"} grob.
\item The dimensions of the ``plot'' default to \code{unit(1, "null")}
because \code{"collection"} grobs have no width or height methods, which
means that the plot fills up whatever space remains once the
legend has been accommodated.
\end{itemize}
<<plot, echo=FALSE, include=FALSE, fig=TRUE, results=hide>>=
top.vp <- viewport(width = 0.8, height = 0.8)
pushViewport(top.vp)
x <- runif(10)
y1 <- runif(10)
y2 <- runif(10)
pch <- 1:3
labels <- c("Girls", "Boys", "Other")
gf <- frameGrob()
plt <- gTree(children = gList(rectGrob(),
pointsGrob(x, y1, pch = 1),
pointsGrob(x, y2, pch = 2),
xaxisGrob(),
yaxisGrob()))
gf <- packGrob(gf, plt)
gf <- packGrob(gf, legendGrob(pch, labels),
height = unit(1, "null"), side = "right")
grid.rect(gp = gpar(col = "grey"))
grid.draw(gf)
popViewport()
grid.rect(gp = gpar(lty = "dashed"), width = .99, height = .99)
@
\begin{center}
\includegraphics{frame-plot}
\end{center}
@
\section*{Notes}
\begin{enumerate}
\item There are \code{grid.frame()} and \code{grid.pack()} equivalents
for these functions, but these are really only useful to see the
changes in the frame as each packing operation takes place.
\item This frame-and-packing stuff is easier to use, \emph{but}
(in almost all cases) it is less efficient than specifying the
arrangement by hand. There is consequently a penalty to pay in
terms of memory (inconsequential I think) and in terms of speed
(noticeably slower).
Perhaps one sensible use of these functions is to
build an image interactively using the simple arguments,
which will be slow, then attempt to speed up the drawing
by exploring some of the more advanced arguments.
One way to speed things up a bit is to specify the layout when
the frame is initially created and then use the \code{placeGrob()} function
to put grobs into existing rows and columns.
The speed penalty in the cases I have seen are mostly due to the time
taken to generate the (sometimes very) complicated unit objects that
express the heights and widths of the rows and columns of the frame
layout. Future effort will be put into speeding up the creation of
unit objects.
\end{enumerate}
\end{document}