Skip to content
Winston Chang edited this page Jun 10, 2012 · 8 revisions

New theme system

This is a description of the changes for the new ggplot2 theming system, to be in version 0.9.2. It shouldn't break any code from the previous versions of ggplot2, but it adds some new features, and makes some things easier.

Renamed opts() to theme()

The opts() function is now theme().

Renamed and changed theme_xx functions

The functions theme_text, theme_rect, theme_line, and theme_segment have been renamed to element_xx. These new names are less confusing than the old ones -- these are theme elements as opposed to themes, like theme_grey().

The theme_xx functions still work, but they now print deprecation messages.

Previously, theme_line() and theme_segment() were distinct. Now, there is only element_line() -- the segment version was no longer necessary and was removed.

Plot title

Previously, to set the title of a plot, you would use opts(title="my title"). It has been changed:

qplot(1:3, 1:3) + labs(title="my title")
# Another method
qplot(1:3, 1:3) + ggtitle("my title")

There is a new element in the inheritance tree which has the same name, but different purpose. It is an element_text with properties that are inherited by other elements (see inheritance graph below).

# Will make some of text items red
qplot(1:3, 1:3) + theme(title = element_text(colour='red'))

Note that there is a special case: if you use opts() instead of theme(), as in opts(title="my title"), it will use the old behavior, but will also print deprecation messages.

Inheritance

The theme system supports inheritance. That is, theme elements can inherit properties from other theme elements. For example, axis.title.x and axis.title.y inherit their properties from axis.title, which inherits from title, which inherits from text. This makes it possible to control the appearance of many elements, by modifying an element higher in the inheritance tree.

You can set the size of axis.title.x like this:

mytheme <- theme_grey() + theme(axis.title.x = element_text(size = 8))

# View definition of axis.title.x
mytheme$axis.title.x
# List of 8
#  $ family    : NULL
#  $ face      : NULL
#  $ colour    : NULL
#  $ size      : num 8
#  $ hjust     : NULL
#  $ vjust     : NULL
#  $ angle     : NULL
#  $ lineheight: NULL
#  - attr(*, "class")= chr [1:2] "element" "element_text"

In this case axis.title.x will inherit all the NULL properties (family, face, colour, etc) from its parent element, but size will be set to 8.

The function calc_element will the calculate the properties of an element with inheritance, and return an element object:

calc_element("axis.title.x", mytheme, verbose = TRUE)
# axis.title.x --> axis.title
# axis.title --> title
# title --> text
# text --> nothing (top level)
# List of 8
#  $ family    : chr "Helvetica"
#  $ face      : chr "plain"
#  $ colour    : chr "black"
#  $ size      : num 8
#  $ hjust     : num 0.5
#  $ vjust     : num 0.5
#  $ angle     : num 0
#  $ lineheight: num 0.9
#  - attr(*, "class")= chr [1:2] "element" "element_text"

If you instead changed axis.title, its properties would be inherited by axis.title.x and axis.title.y. Some elements are never used directly, but are only used for inheritance. Examples include text and the axis.title.

All of the elements with text inherit from text, directly or indirectly. Similarly, all elements that have rectangle grobs inherit from rect.

The inheritance is defined by a data structure ggplot2:::.element_tree, which you can see at the bottom of theme-elements.r.

Here is a graph of the inheritance structure, generated with igraph:

Element inheritance graph

Multiple inheritance is also supported by this system but it's not actually used for any existing elements.

Theme objects are simple nested lists

Theme objects are now simple nested lists. Previously, a theme (such as the one returned by theme_grey()) was a list of mostly functions, which return grobs. Functions like theme_rect() would return functions which returned grobs.

Now a theme is a list of lists. element_rect() returns a list (of class element_rect), instead of a function that returns a grob.

You can see how themes are specified in theme-defaults.r. Presently, theme_grey is the default theme, and theme_bw is simply a modification of it.

Relative sizing

Relative sizing is specified with rel(). For example, axis.text inherits from text. This would make axis.text have a font size that is half the size of text:

theme(axis.text = element_text(size=rel(0.5)))

Modifying a theme

You can still add calls to theme() to modify theme settings for a particular graph.

To modify a theme object, you can do something like the following:

mytheme <- theme_grey() +
  theme(text = element_text(family="Times", colour="red", size=16))

For creating themes, you may want to use the %+replace% operator. The difference between + and %+replace% is that the former will only update properties of elements, whereas the latter will replace the element entirely. The + operator cannot be used to set an element property to NULL (which means that it should inherit the property from its parent). In cases where the property must be set to NULL, the %+replace% operator must be used.

The + operator will be used in most cases; the %+replace% operator be used mostly for creating new themes.

For example:

# Only change the 'colour' property of 'text'
mytheme1 <- theme_grey() + theme(text = element_text(colour="red"))
mytheme1$text
# List of 8
#  $ family    : chr "Helvetica"
#  $ face      : chr "plain"
#  $ colour    : chr "red"
#  $ size      : num 12
#  $ hjust     : num 0.5
#  $ vjust     : num 0.5
#  $ angle     : num 0
#  $ lineheight: num 0.9
#  - attr(*, "class")= chr [1:2] "element" "element_text"


# Replace the 'text' element entirely
mytheme2 <- theme_grey() %+replace% theme(text = element_text(colour="red"))
mytheme2$text
# List of 8
#  $ family    : NULL
#  $ face      : NULL
#  $ colour    : chr "red"
#  $ size      : NULL
#  $ hjust     : NULL
#  $ vjust     : NULL
#  $ angle     : NULL
#  $ lineheight: NULL
#  - attr(*, "class")= chr [1:2] "element" "element_text"

This is actually how theme_bw() is now defined. Notice that, for a top-level element like text, it's necessary to that all the properties like family, face, colour, and so on, are non-NULL.

Another way to modify element properties is to change the nested list items directly. Generally speaking, it's probably best to avoid doing it this way.

mytheme <- theme_grey()
mytheme$text$family <- "Times"
mytheme$text$colour <-"red"
mytheme$text$size   <- 16


# Can also set individual properties to NULL -- which isn't possible with
# the + operator, and can be inelegant with the %+replace% operator.
mytheme$axis.text
# List of 7
#  $ family    : NULL
#  $ face      : NULL
#  $ colour    : chr "grey50"
#  $ size      :Class 'rel'  num 0.8
#  $ hjust     : NULL
#  $ vjust     : NULL
#  $ angle     : NULL
#  $ lineheight: NULL
#  - attr(*, "class")= chr [1:2] "element" "element_text"

# Note the ugly syntax needed for setting a list item to NULL, instead of deleting it.
mytheme$axis.text['colour'] <- list(NULL)

# Same effect as
mytheme <- mytheme %+replace% theme(axis.text = element_text(size=rel(0.8)))

Complete and incomplete theme objects

There are complete theme objects, and incomplete theme objects. Complete theme objects include theme_grey() and theme_bw(). Incomplete objects include things like theme(text = element_text(colour='red')).

Complete themes and an incomplete themes have slightly different behavior when they're added to a ggplot object. When adding an incomplete theme object to a ggplot object, it is in effect added to the current default theme, so any NULL element properties in the theme object are ignored and don't affect the plot's appearance.

# Set the default theme
theme_set(theme_grey())

t <- theme(axis.text = element_text(size=14, colour=NULL))

# theme_grey has axis.text$colour="grey50"
# Adding t does not reset the colour to NULL (which would result in black text)
qplot(1:3, 1:3) + t

This behavior can be problematic when you want to replace the default theme entirely. For example, suppose the default is theme_grey() and you want to add theme_bw() to it. There are some element properties that are non-NULL in the former, but NULL in the latter. The result is when you do something like qplot(1:3, 1:3) + theme_bw(), those properties don't get reset to NULL -- they simply carry over from the default theme.

To deal with this issue, theme_bw() is marked as a complete theme, which has different behavior. When a complete theme is added to a plot, it completely overrides the current default theme.

This is implemented with an attribute "complete", which is TRUE for complete themes and FALSE for incomplete themes. For example:

attr(theme_grey(), "complete")
# [1] TRUE

attr(theme(text=element_text(colour='red')), "complete")
# [1] FALSE


# When adding a complete theme with an incomplete them, the result is complete
attr(theme_grey() + theme(text=element_text(colour='red')), "complete")
# [1] TRUE
attr(theme(text=element_text(colour='red'))+ theme_grey(), "complete")
# [1] TRUE


# When adding two incomplete themes, the result is incomplete
attr(theme(text=element_text(colour='red')) +
     theme(axis.text=element_text(colour='blue')), "complete")
# [1] FALSE

To create new themes, you can simply add to theme_grey(), and the result will be a complete theme (this is how theme_bw() is defined). Alternatively, you can start fresh, specifying all the theme elements, and use complete=TRUE (this is how theme_grey() is defined).

mytheme <- theme(
  text = element_text( ... ),
  ... <more elements>,
  complete = TRUE)

Independent control over some x and y elements

It's now possible to independently control the x and y components of following:

  • panel.grid.major.x and .y
  • panel.grid.minor.x and .y
  • axis.ticks.x and .y
  • axis.line.x and .y

For example, panel.grid.major.x inherits from panel.grid.major, and so if the .x version is not specified, it simply inherits all properties from panel.grid.major.

It is now possible to disable just horizontal or just vertical grid lines without any ugly hacks. For example:

# Remove horizontal grid lines
qplot(1:5, 1:5) +
  theme(panel.grid.major.y = element_blank(),
       panel.grid.minor.y = element_blank())

Notes

Element graph

This is the code that generates the element inheritance graph.

build_element_graph <- function(tree) {
  require(igraph)

  inheritdf <- function(name, item) {
    if (length(item$inherit) == 0)
      data.frame()
    else
      data.frame(child = name, parent = item$inherit)
  }

  edges <- rbind.fill(mapply(inheritdf, names(tree), tree))

  # Explicitly add vertices (since not all are in edge list)
  vertices <- data.frame(name = names(tree))
  graph.data.frame(edges, vertices = vertices)
}

g <- build_element_graph(ggplot2:::.element_tree)
V(g)$label <- V(g)$name

png('element_graph.png', width=700, height=600)
set.seed(324)
par(mar=c(0,0,0,0)) # Remove unnecessary margins
plot(g, layout=layout.fruchterman.reingold, vertex.size=4, vertex.label.dist=.25)
dev.off()