Creating a new geom - janeshdev/ggplot2 GitHub Wiki

This page aims at presenting the major steps involved in creating a new geom for the ggplot2 graphing package.

You can download a self-contained file with all the code of this page at http://code.google.com/p/ggextra/source/browse/trunk/demos/geom-field-test.r

A few things to know

ggplot2 is based on Grid graphics, and uses the proto framework. Below is a list of useful links to get some familiarity with these.

proto, http://code.google.com/p/r-proto/ and in particular the tutorial, http://cran.r-project.org/web/packages/proto/vignettes/proto.pdf

Grid, http://www.stat.auckland.ac.nz/~paul/grid/grid.html and in particular this presentation on writing new grobs, http://www.stat.auckland.ac.nz/~paul/Talks/gridfunc.pdf

Create the graphical object (grob)

We want to plot small segments defined by their middle point (x,y), orientation, and length. Additional aesthetic parameters will include the line width, colour, and optional arrow ending. For this purpose we wrap a call to segmentsGrob() with custom defaults,

fieldGrob <- function(x, y, angle, length, size, 
	colour="black", linetype=1, arrow=NULL){
	
   segmentsGrob(
		x0= x - 0.5*length*cos(angle), 
		y0= y - 0.5*length*sin(angle),
		x1= x + 0.5*length*cos(angle), 
		y1= y + 0.5*length*sin(angle), 
		default.units="native",
	        gp = gpar(col=colour, 
                          lwd=size*ggplot2:::.pt, 
                          lty=linetype, 
                          lineend = "butt"), 
	        arrow = arrow
               )
}

We can test the output,

g <-
fieldGrob(0.5, 0.5, pi/6, 0.2,1,  col="blue")
pushViewport(vp=viewport(width=1, height=1))
grid.draw(g)

Tips:

  • sizes should be in mm
  • use names that are consistent with the rest of ggplot2 (e.g. lty is spelled out as linetype)
  • a new grob should respect the standard syntax of Grid objects ( not the case above)
  • it is often useful to be able to specify the coordinates either in “mm” or “npc”, for instance the legend and data don’t use the same coordinate system.

Creating the proto geom

From this grob, we can create a geom as follows,

GeomField <- proto(Geom, {
  draw <- function(., data, scales, coordinates, ...) 
{
	# draw the field grobs in relation to the data
}
 
  draw_legend <- function(., data, ...) {
    # show a grob in the key that represents the data
  }
 
  objname <- "field" # name of the geom in lowercase. Must correspond to GeomField.
  desc <- "Single line segments"
 
  default_stat <- function(.) StatIdentity
  required_aes <- c("x", "y", "angle", "length") 
  default_aes <- function(.) aes(colour="black", angle=pi/4, length=1, size=0.5, linetype=1)
  guide_geom <- function(.) "field"
 
  icon <- function(.) # a grob representing the geom for the webpage
 
  desc_params <- list( # description of the (optional) parameters of draw
	 )
 
  seealso <- list(
    geom_path = GeomPath$desc,
    geom_segment = GeomPath$desc,
    geom_line = GeomLine$desc
  )
 
  examples <- function(.) {
    # examples of the geom in use
  }
  
})

A few rules to follow,

  • make sure the geom object itself has a new name
  • make sure the objname property is unique
  • when a geom has both colour and fill, alpha should be applied to the fill
  • The geom is a proto object that contains several elements, in no particular order. Of particular importance are the draw and draw_legend methods that will plot the data and the legend respectively for this geom.

Below is an implementation of the draw function,

draw <- function(., data, scales, coordinates, arrow=NULL, ...) {
  with(coordinates$transform(data, scales), 
        fieldGrob(x, y, angle, length, size, 
        col=colour, linetype, arrow)
      )    
  }

The data are first transformed according to the specific coordinates system of the plot, and the resulting values are passed to the fieldGrob function.

draw_legend needs to produce a grob to go in the legend. The coordinates system must be “npc”, as the corresponding viewport will be a small square.

  draw_legend <- function(., data, ...) {
    data <- aesdefaults(data, .$default_aes(), list(...))
 
    with(data, ggname(.$my_name(),
		fieldGrob(0.5, 0.5, angle, length, size,  col=colour, linetype) )
	)
  }

The legend retrieves the name of the variable mapped to the aesthetic, and adds the default aesthetic values to the data, plus some optional values (?).

This geom needs to be registered as a proto geom object, which is done by,

geom_field <- GeomField$build_accessor()

This step must come after sourcing the geom definition (important when building a package, for example).

Adding scales

The data that is to be drawn needs to be scaled according to certain rules that depend on the variable.

“When adding a default scale (e.g. when you use sides as an aesthetic), ggplot2 categorises the type of data into date, datetime, continuous and discrete. It then looks for (e.g.) a scale named scale_sides_discrete or scale_sides_continuous. If it doesn’t find a particular type of scale, it uses the generic, scale_sides. So if you want different behaviour for continuous and discrete values, then you need to create scale_sides_continuous and scale_sides_discrete.” — //Hadley, ggplot2 list//.

Colours are mapped differently than size, for instance. Here we introduced two new scales in the parameters angle and length. A scale definition is a proto object,

ScaleLength <- proto(ScaleContinuous, expr={
  doc <- TRUE
  common <- NULL
  
  new <- function(., name=NULL, limits=NULL, breaks=NULL, labels=NULL, trans = NULL, to = NULL) {
    .super$new(., name=name, limits=limits, breaks=breaks, labels=labels, trans=trans, variable = "length", to = to)
  }
  
  map <- function(., values) {
    # rescale(values, .$to, .$input_set())
   values
  }
  output_breaks <- function(.) .$map(.$input_breaks())
  
  objname <- "length"
  desc <- "Length scale for continuous variable"
  
})

we can define a version for discrete variables where the length is quantized to pre-defined levels,

ScaleLengthDiscrete <- proto(ScaleDiscrete, expr={
  common <- NULL
  objname <- "length_discrete"
  .input <- .output <- "length"
  desc <- "Length scale for discrete variables"
  doc <- FALSE
 
  max_levels <- function(.) 11
  output_set <- function(.) seq(1, 5, length=11)
 
})

As for the geom, these scales need to be registered,

 scale_length <- ScaleLength$build_accessor()
 scale_length_discrete <- ScaleLengthDiscrete$build_accessor()

We can add a manual version where the user specifies the length directly,

scale_length_manual <- ScaleManual$build_accessor(list(variable = "\"length\""))