Effective Data Visualisation with R
Customisation

Paul Murrell
The University of Auckland

Review

  • A data visualisation consists of data symbols, guides, and labels.

  • We produce data symbols by mapping data values to the visual features of a shape.

  • Which visual feature we choose depends on:

    • Whether we have quantitative or qualitative data.
    • The accuracy and capacity of the visual feature.
    • The question we are interested in answering.

Review

  • The effectiveness of a data visualisation also depends on coordination between different elements.

    • Contrast.
    • Repetition.
    • Alignment.
    • Proximity.
  • There may also be a justification for elements that do not map directly to data values.

Customisation

  • With {ggplot2} we are able to specify a high-level description of a data visualisation.

    • A lot of the details are filled in for us.
    • It can be difficult to control all of the details.
  • This topic explores a different approach to producing a data visualisation.

    • We control all of the details.
    • We have to specify all of the details ourselves.

  • There are some things that are hard to do with {ggplot2}.

Customisation

  • The {grid} package

  • Integrating {grid} with {ggplot2}

  • Integrating {ggplot2} with {grid}

The {grid} Package

  • With the {ggplot2} package, we describe a data visualisation in terms of higher-level concepts, such as mappings from data to aesthetics.

    ggplot(crimeGenderTotal) +
        geom_col(aes(x=prop, y="", fill=gender))

  • With the {grid} package, we just draw shapes.

    library(grid)
    grid.newpage()
    grid.rect(gp=gpar(col=NA, fill="grey90"))

  • grid.rect() draws rectangles

  • gpar() controls graphical settings like (border) colour and fill colour.

    grid.rect(gp=gpar(col=NA, fill="grey90"))

  • grid.segments() draws line segments.

  • gpar(lwd) controls line width.

    grid.rect(gp=gpar(col=NA, fill="grey90"))
    grid.segments(0, .5, 1, .5, gp=gpar(col="white", lwd=.5))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white", lwd=2))

  • Positions are proportions of the page (by default)
    (0 = left/bottom, 1 = right/top)

  • These are "npc" (normalised parent) coordinates.

    grid.rect(gp=gpar(col=NA, fill="grey90"))
    grid.segments(0, .5, 1, .5, gp=gpar(col="white", lwd=.5))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white", lwd=2))

  • Shapes can be based on data!

    widths <- crimeGenderTotal$prop
    widths
    [1] 0.2995225 0.7004775
    fills <- scales::hue_pal()(2)
    fills
    [1] "#F8766D" "#00BFC4"

  • Shapes can be based on data!

    grid.rect(gp=gpar(col=NA, fill="grey90"))
    grid.segments(0, .5, 1, .5, gp=gpar(col="white", lwd=.5))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white", lwd=2))
    grid.rect(c(1, 0), width=widths, height=.8, 
              hjust=c(1, 0), gp=gpar(col=NA, fill=fills))

  • Drawing is always relative to a viewport.

    vp <- viewport(width=.8, height=.6)

  • Drawing is always relative to a viewport.

    pushViewport(vp)
    grid.rect(gp=gpar(col=NA, fill="grey90"))
    grid.segments(0, .5, 1, .5, gp=gpar(col="white", lwd=.5))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white", lwd=2))
    grid.rect(c(1, 0), width=widths, height=.8, 
              hjust=c(1, 0), gp=gpar(col=NA, fill=fills))
    popViewport()

  • Drawing is always relative to a coordinate system.

  • vp(xscale, yscale) define "native" coordinates.

  • "npc" coordinates are also still available.

    vp <- viewport(width=.8, height=.6, xscale=c(-.05, 1.05))

  • Drawing is always relative to a coordinate system.

  • default.units selects the default coordinates.

    grid.segments(0, .5, 1, .5, gp=gpar(col="white"))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white"), 
                  default.units="native")

  • Drawing is always relative to a coordinate system.

    pushViewport(vp)
    grid.rect(gp=gpar(col=NA, fill="grey90"))
    grid.segments(0, .5, 1, .5, gp=gpar(col="white"))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white"), 
                  default.units="native")
    grid.rect(c(1, 0), width=crimeGenderTotal$prop, height=.8, 
              hjust=c(1, 0), gp=gpar(col=NA, fill=fills), 
              default.units="native")

  • unit() associates values with a coordinate system.

  • There are "mm" and "in" coordinates available.

  • "npc" and "native" are still also available.

    twoMM <- unit(2, "mm")

  • unit() associates values with a coordinate system.

    pushViewport(vp)
    grid.rect(gp=gpar(col=NA, fill="grey90"))
    grid.segments(0, .5, 1, .5, gp=gpar(col="white"))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white"), 
                  default.units="native")
    grid.rect(c(1, 0), width=crimeGenderTotal$prop, height=.8, 
              hjust=c(1, 0), gp=gpar(col=NA, fill=fills), 
              default.units="native")
    grid.segments(0:4/4, 0, 0:4/4, -twoMM, default.units="native")

  • grid.text() draws text.

    pushViewport(vp)
    grid.rect(gp=gpar(col=NA, fill="grey90"))
    grid.segments(0, .5, 1, .5, gp=gpar(col="white"))
    grid.segments(0:4/4, 0, 0:4/4, 1, gp=gpar(col="white"), 
                  default.units="native")
    grid.rect(c(1, 0), width=crimeGenderTotal$prop, height=.8, 
              hjust=c(1, 0), gp=gpar(col=NA, fill=fills), 
              default.units="native")
    grid.segments(0:4/4, 0, 0:4/4, -twoMM, default.units="native")
    grid.text(0:4/4, unit(0:4/4, "native"), unit(-1, "lines"))

{grid} Shapes

gpar() Settings

{grid} units

  • {grid} means more work, but you get complete control.

  • {grid} means more work, but you get complete control.

  • {grid} means more work, but you get complete control.

  • {grid} means more work, but you get complete control.

  • {grid} means more work, but you get complete control.

Integrating {grid} with {ggplot2}

Cats and Dogs

  • The pets data frame contains an estimate of the number of pet cats and dogs globally.

    pets <- data.frame(pet=c("dog", "cat"), count=c(471, 373))
    pets
      pet count
    1 dog   471
    2 cat   373

Cats and Dogs

ggplot(pets) +
    geom_col(aes(x=count, y=pet))

Cats and Dogs

  • The raster images cat and dog depict silhouettes of a cat and a dog.

    grid.rect(gp=gpar(col=NA, fill="grey"))
    grid.raster(catImg, x=1/3, height=1/4)
    grid.raster(dogImg, x=2/3, height=1/4)

{grid} Grobs

  • Instead of drawing {grid} output directly, we can create a grob (graphical objects), but draw nothing, and then draw the grob later.

    catGrob <- rasterGrob(catImg, x=1/3, height=1/4)
    dogGrob <- rasterGrob(dogImg, x=2/3, height=1/4)

{grid} Grobs

grid.rect(gp=gpar(col=NA, fill="grey"))
grid.draw(catGrob)
grid.draw(dogGrob)

{grid} Grobs

  • We can combine grobs into a single gTree (grob Tree).

    petGrob <- grobTree(catGrob, dogGrob)
    grid.rect(gp=gpar(col=NA, fill="grey"))
    grid.draw(petGrob)

The {gggrid} package

  • The {gggrid} package allows us to mix {grid} drawing with a {ggplot2} plot.

    library(gggrid)

  • The grid_panel() function draws a grob on a {ggplot2} plot.

    ggplot(pets) +
        geom_col(aes(x=count, y=pet)) +
        grid_panel(petGrob)

  • The grobs are drawn within a viewport corresponding to the {ggplot2} panel.

    catGrob2 <- rasterGrob(catImg, x=.2, y=1/4, height=1/4)
    dogGrob2 <- rasterGrob(dogImg, x=.2, y=3/4, height=1/4)
    ggplot(pets) +
        geom_col(aes(x=count, y=pet)) +
        grid_panel(catGrob2) +
        grid_panel(dogGrob2)

  • grid_panel() also accepts a function that calculates what to draw.

  • The function is called with data and coords (transformed data) for the {ggplot2} plot panel.

  • The function returns a grob or gTree for {ggplot2} to draw.

    demo <- function(data, coords) {
        print(data)
        print(coords)
        rectGrob()
    }

  • grid_panel() also accepts a function that calculates what to draw.

  • The function is called with data and coords (transformed data) for the {ggplot2} plot panel.

    ggplot(pets) +
        geom_col(aes(x=count, y=pet)) +
        grid_panel(demo, aes(x=count, y=pet))
        x y PANEL group
    1 471 2     1     2
    2 373 1     1     1
              x         y PANEL group
    1 0.9545455 0.7272727     1     2
    2 0.7653928 0.2727273     1     1

  • grid_panel() also accepts a function that calculates what to draw.

  • The function returns a grob or gTree for {ggplot2} to draw.

    petFun <- function(data, coords) {
        grobTree(rasterGrob(catImg, y=coords$y[2], height=1/4,
                            x=unit(coords$x[2], "npc") - unit(1, "cm"),
                            just="right"),
                 rasterGrob(dogImg, y=coords$y[1], height=1/4,
                            x=unit(coords$x[1], "npc") - unit(1, "cm"),
                            just="right"))
    }

The {gggrid} package

ggplot(pets) +
    geom_col(aes(x=count, y=pet)) +
    grid_panel(petFun, aes(x=count, y=pet))

  • {gggrid} means greater control, at the cost of more effort.

Integrating {ggplot2} with {grid}

  • A {ggplot2} plot is “just” {grid} drawing in the end.

    ggplot(crimeGenderTotal) +
        geom_col(aes(x=prop, y="", fill=gender))

  • A {ggplot2} plot is “just” {grid} drawing in the end.

    ggplot(crimeGenderTotal) +
        geom_col(aes(x=prop, y="", fill=gender))
    grid.force()
    grid.ls()
    layout
      background.1-13-16-1
        plot.background..rect.462
      panel.9-7-9-7
        panel-1.gTree.428
          grill.gTree.426
            panel.background..rect.419
            panel.grid.minor.x..polyline.421
            panel.grid.major.y..polyline.423
            panel.grid.major.x..polyline.425
          NULL
          geom_rect.rect.415
          NULL
          panel.border..zeroGrob.416

  • We can draw a {ggplot2} plot within a {grid} viewport.

    gg <- 
        ggplot(crimeGenderTotal) +
            geom_col(aes(x=prop, y="", fill=gender))
    pushViewport(viewport(y=0, height=3/4, just="bottom"))
    print(gg, newpage=FALSE)

  • This allows us to draw other content alongside.

    pushViewport(viewport(y=0, height=3/4, just="bottom"))
    print(gg, newpage=FALSE)
    popViewport()
    pushViewport(viewport(y=3/4, height=1/4, just="bottom", clip=TRUE))
    grid.rect(gp=gpar(lwd=3))
    grid.raster(policeLogo, x=.1, just="left", height=.8)

Arranging components

  • The crimeYear data frame contains the number of offenders per year.

Arranging components

  • We can see changes more easily by directly visualising changes.

The {patchwork} Package

  • We can answer more questions if we have both views of the data.

    library(patchwork)
    gg1 <- ggplot(crimeYear) +
        geom_line(aes(x=year, y=total)) +
        scale_x_continuous(limits=c(2010, 2022), 
                           expand=expansion(c(0, 0)),
                           breaks=seq(2010, 2020, 2))
    gg2 <- ggplot(crimeYear) +
        geom_col(aes(x=year, y=change)) +
        scale_x_continuous(limits=c(2010, 2022), 
                           expand=expansion(c(0, 0)),
                           breaks=seq(2010, 2020, 2))

The {patchwork} Package

gg1 + gg2

gg1 + gg2 + plot_layout(ncol=1, heights=c(3, 1))

gg1 + gg2 + plot_layout(ncol=1, heights=c(3, 1), axes="collect")

Summary

Summary

  • We can produce a data visualisation with complete control over all of the details with {grid}
    • Draw basic shapes.
    • Control graphical parameters.
    • Draw within viewports.
    • Associate locations and dimensions with units.

 

  • {ggplot2} is built on top of {grid}
    • We can add {grid} drawing to {ggplot2} with {gggrid}.
    • We can draw {ggplot2} within {grid} viewports.

Exercises

Exercise

  • Can you use {grid} to draw any of these diagrams?

Exercise

  • The male() function creates a male symbol grob.

    male <- function(x=.5, y=.5) {
        vp <- viewport(x, y, width=unit(6, "mm"), height=unit(6, "mm"),
                       gp=gpar(col="white"))
        grobTree(segmentsGrob(.5, .5, 1, 1),
                 segmentsGrob(1, 1, 2/3, 1),
                 segmentsGrob(1, 1, 1, 2/3),
                 circleGrob(1/3, 1/3, r=1/3, gp=gpar(fill="skyblue")),
                 vp=vp)
    }
    grid.rect(gp=gpar(col=NA, fill="grey40"))
    grid.draw(male())

Exercise

  • The female() function creates a female symbol grob.

    female <- function(x=.5, y=.5) {
        vp <- viewport(x, y, width=unit(6, "mm"), height=unit(6, "mm"),
                       gp=gpar(col="white"))
        grobTree(segmentsGrob(2/3, .5, 2/3, -1/3),
                 segmentsGrob(1/3, 0, 1, 0),
                 circleGrob(2/3, 2/3, r=1/3, gp=gpar(fill="pink")),
                 vp=vp)
    }
    grid.rect(gp=gpar(col=NA, fill="grey40"))
    grid.draw(female())

Exercise

  • Can you use the male() and female() functions (and the crimeGenderTotal data frame) to produce the data visualisation below?