Rev 42945 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
\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<<echo=FALSE, results=hide>>=library(grDevices)library(stats) # for runiflibrary(grid)ps.options(pointsize=12)options(width=60)@Grid contains a lot of support for locating, sizing, andarranging graphical components on a device and with respect toeach other. However, most of this support relies on {\it either} the parentobject dictating both location and size (layouts) {\it or}the child dictating bothlocation and size.Some sorts of arrangements are moreconveniently handled by having the parent dictate the location, butletting thechild dictate the size.This is the situation for GUI-builders (software which formsarrangements of GUI components or widgets). The approach takenby (many ?) GUI-builders is to allow the user to create a parent{\it frame} and then {\it pack} widgets into this frame. Theframe locates and arranges the children with the help of hints such as``place this widget at the bottom of the frame'', and the childrendictate what size they would like to be.This document describes a first attempt at such an interface forarranging 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 orderto pack grobs into it.<<results=hide>>=gf <- frameGrob()@\section*{The {\tt packGrob()} function}Having created a frame, you can then pack other graphical objects intoit using the {\tt packGrob()} function. This function has a complexinterface which allows for a variety of methods of packing graphicalobjects. 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 frameand possibly how much space the grob should occupy. The frame is effectivelyjust a layout; you can add grobs to existing rows and columnsor you can append the grob in a new row and/or column. If thegrob is added to an existing row then the height ofthat row becomes the maximum of the new height andthe previous height. If the row is new then it justgets 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 thanthe 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 anew column inserted between {\tt col.after} and {\tt col.after + 1}.\item[{\tt col.before}] specifies that the grob should be put in anew 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 packedinto. 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 possibleto add a grob to a row and force that row to have the specified heightby setting {\tt force.height=TRUE} (and similarly for column widths).It is also possible to pack a graphical object into several rows orcolumns at once (although you cannot simultaneously affect the heights orwidths of those rows and columns).The result of this function is the modified frame, so you mustassign the result to a variable.<<results=hide>>=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 makinguse of {\tt "grobwidth"} and {\tt "grobheight"} units.These units may, of course, be used outside of frames too sotheir use is described here.Consider a simple example where I want to draw a rectangle arounda piece of text. I can get the size of the piece of text from the{\tt "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 {\tt "strwidth"}and {\tt "strheight"} units, but {\tt "grobwidth"} and {\tt "grobheight"}give you a lot more power. The biggest gain is that youcan get the size of other objects besides pieces of text (more on thatsoon). Another thing you can do is provide a ``reference'' to a grobrather 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 thegrob affect the unit. The following is a dynamic version of theprevious 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 whathappens if I modify the text grob named {\tt "st"}:<<results=hide>>=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>>=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 {\tt widthDetails} and {\tt heightDetails} generic functions}The calculation of {\tt "grobwidth"} and {\tt "grobheight"} unitsis a bit complicated, but fortunately most of it is automated.The simple part is that a grob provides a normal {\tt "unit"} objectto express its width or height. The complication comes because that{\tt "unit"} object has to be evaluated in the correct context; inparticular, if the grob has a non-{\tt NULL} {\tt vp} argument thenthose viewports have to be pushed so that the size of the grob isthe size it would be when it is drawn. This is achieved by calling the{\tt preDrawDetails()} function for the grob and in most cases whathappens by default will be correct. The thing to avoid is havingany viewport operations in a {\tt drawDetails()} method for yourgrob; they should go in a {\tt preDrawDetails()} method.All that needs to be written (usually) are the functions thatprovide 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; thesereturn {\tt unit(1, "mystrwidth", <text grob label>)} and{\tt unit(1, "mystrheight", <text grob label>)} respectively.The other very important examples of these methods are those for{\tt "frame"} grobs. These return the {\tt sum} of thewidths (heights) of the columns (rows) of the layout that has beenbuilt up by packing grobs into the frame. This means thatwhen a {\tt "frame"} grob is packed within another {\tt "frame"} grobthe 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 respondwith 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'swidth 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 figureout its own). The {\tt absolute.size} function leaves absoluteunits alone, but converts relative units to {\tt "null"} units(i.e., the child says to the parent, ``you decide how big I should be''), soyou can return something like {\tt absolute.size(width(<grob>))} in orderto always give a sensible answer.@\section*{Examples}The original motivating example for this GUI-builder approach wasto be able to produce a quite general-purpose legend grob.A legend consists of data symbols and associated textualdescriptions. In order to be quite general, it would be niceto allow, for example, multiple lines of text per data symbol. Rather thanhaving to look at the text supplied for the legend in order todetermine the arrangement of the legend, it would be nice to be ableto simply specify the composition of the legend and let it figureout the arrangement for us. The code below defines just sucha 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 legendcomponents.\item The size of the data symbol component is specified, but thesize 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", <text grob>)|$.\item We have two functions, one for generating a grob and onefor 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 beingused 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 ratherthan using frames and packing, but the next example shows howuseful it can be.Suppose you want to arrange a legend next to a plot. This requiresleaving enough space for the legend and then filling the remainingspace with the plot. This requires figuring out how much space thelegend needs, and that is a task that is neither trivial noreasy to cater for in the general case. Ideally, we want to knowas little as possible about the legend.With the GUI-builderapproach this becomes extremely simple. The code belowshows how the construction of such a scene might be performed; theoutput 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 wasconstructed; 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 wedid not do this then the plot would be forced to be the height ofthe legend (because of the way that {\tt "null"} units interactwith other units).\item The width of the legend is calculated from the contents of thelegend 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, whichmeans that the plot fills up whatever space remains once thelegend has been accomodated.\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:3labels <- 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 {\tt grid.frame()} and {\tt grid.pack()} equivalentsfor these functions, but these are really only useful to see thechanges 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 thearrangement by hand. There is consequently a penalty to pay interms of memory (inconsequential I think) and in terms of speed(noticeably slower).Perhaps one sensible use of these functions is tobuild an image interactively using the simple arguments,which will be slow, then attempt to speed up the drawingby exploring some of the more advanced arguments.One way to speed things up a bit is to specify the layout whenthe frame is initially created and then use the {\tt placeGrob()} functionto put grobs into existing rows and columns.The speed penalty in the cases I have seen are mostly due to the timetaken to generate the (sometimes very) complicated unit objects thatexpress the heights and widths of the rows and columns of the framelayout. Future effort will be put into speeding up the creation ofunit 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<<eval=FALSE, echo=FALSE>>=grid.newpage()@\end{document}