\documentclass[a4paper]{article} %\VignetteIndexEntry{Frames and packing grobs} %\VignettePackage{grid} \newcommand{\grid}{{\tt grid}} \newcommand{\R}{{\tt 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 <>= 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 {\it either} the parent object dictating both location and size (layouts) {\it 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 {\it frame} and then {\it 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 {\tt "frame"} grob} You can create a {\tt "frame"} graphical object using the function {\tt frameGrob()}. You must assign the result to a variable in order to pack grobs into it. <>= gf <- frameGrob() @ \section*{The {\tt packGrob()} function} Having created a frame, you can then pack other graphical objects into it using the {\tt 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[{\tt frame}] is a {\tt "frame"} object created by {\tt grid.frame}. \item[{\tt 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[{\tt 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[{\tt row}] is like {\tt col} but for rows. \item[{\tt col.after}] specifies that the grob should be put in a new column inserted between {\tt col.after} and {\tt col.after + 1}. \item[{\tt col.before}] specifies that the grob should be put in a new column inserted between {\tt col.before} and {\tt col.before + 1}. \item[{\tt row.after} and {\tt row.before}] do what you would expect. \item[{\tt side}] specifies which side to append the new grob to. The valid values are {\tt "left"}, {\tt "right"}, {\tt "bottom"}, and {\tt "top"}. \item[{\tt 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[{\tt height}] is like {\tt 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 {\tt 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. <>= gf <- packGrob(gf, textGrob("Hello frame!")) @ \section*{{\tt "grobwidth"} and {\tt "grobheight"} units} A {\tt "frame"} object allows a grob to specify its size by making use of {\tt "grobwidth"} and {\tt "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 {\tt "text"} grob as follows: <>= 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 {\tt "strwidth"} and {\tt "strheight"} units, but {\tt "grobwidth"} and {\tt "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. <>= 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 {\tt "st"}: <>= grid.edit("st", gp=gpar(fontsize=20)) <>= 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: <>= grid.edit("st", label="some different text") <>= 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 {\tt widthDetails} and {\tt heightDetails} generic functions} The calculation of {\tt "grobwidth"} and {\tt "grobheight"} units is a bit complicated, but fortunately most of it is automated. The simple part is that a grob provides a normal {\tt "unit"} object to express its width or height. The complication comes because that {\tt "unit"} object has to be evaluated in the correct context; in particular, if the grob has a non-{\tt NULL} {\tt 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 {\tt 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 {\tt drawDetails()} method for your grob; they should go in a {\tt preDrawDetails()} method. All that needs to be written (usually) are the functions that provide the {\tt "unit"} objects. These functions need to be {\tt widthDetails} and {\tt heightDetails} methods. The default methods return {\tt unit(1, "null")} so your grob will be of this size unless you write your own methods. The classic example methods are those for {\tt "text"} grobs; these return {\tt unit(1, "mystrwidth", )} and {\tt unit(1, "mystrheight", )} respectively. The other very important examples of these methods are those for {\tt "frame"} grobs. These return the {\tt 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 {\tt "frame"} grob is packed within another {\tt "frame"} grob the parent automatically leaves enough room for the child. Another useful pair of examples are those for {\tt "rect"} grobs. These methods make use of the {\tt 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., {\tt "inches"}, {\tt "cm"}, {\tt "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., {\tt "npc"} or {\tt "native"}; i.e., the grob needs to know about it's parent's size before it can figure out its own). The {\tt absolute.size} function leaves absolute units alone, but converts relative units to {\tt "null"} units (i.e., the child says to the parent, ``you decide how big I should be''), so you can return something like {\tt absolute.size(width())} 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 {\tt "frame"} grob and {\tt packGrob()} function. Some points to note are: \begin{itemize} \item The use of {\tt 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 {\tt "text"} grobs. \item The heights of the rows in the legend will be the maximum of ${\tt vgap} + \verb|unit(1, "lines")|$ and ${\tt vgap} + \verb|unit(1, "grobheight", )|$. \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 {\tt grid.legend()} function being used procedurally; the output is shown below the code. <>= 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 {\tt 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 {\tt "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 {\tt "frame"} grob. \item The dimensions of the ``plot'' default to {\tt unit(1, "null")} because {\tt "collection"} grobs have no width or height methods, which means that the plot fills up whatever space remains once the legend has been accomodated. \end{itemize} <>= top.vp <- viewport(w=0.8, h=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"), w=.99, h=.99) @ \begin{center} \includegraphics{frame-plot} \end{center} @ \section*{Notes} \begin{enumerate} \item There are {\tt grid.frame()} and {\tt 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, {\it 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 {\tt 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} % Start a new page % Not echoed, not evaluated % ONLY here for checkVignettes so that all output doesn't % end up on one enormous page <>= grid.newpage() @ \end{document}