Powered by AppSignal & Oban Pro

Theme elements: theme

notebooks/theme.livemd

Theme elements: theme

Mix.install([
  {:giggly, path: Path.join(__DIR__, "..")},
  {:kino, "~> 0.19"}
  ])

alias Giggly.Plot

mtcars = Giggly.Datasets.mtcars()

render = fn plot ->
  plot
  |> Giggly.to_svg()
  |> IO.iodata_to_binary()
  |> Kino.Image.new(:svg)
end

About this notebook

This notebook follows the examples on ggplot2's reference page for theme(), in the order the page gives them. Each example shows ggplot2's code first and then Giggly's. An example Giggly cannot draw yet keeps its ggplot2 code and names what it is waiting for, so the page is also a list of what remains.

A theme draws everything in a plot that is not data: titles, labels, fonts, backgrounds, gridlines, legends and strips. theme() changes parts of one plot's theme. Each part is an element, such as element_text(), element_line(), element_rect(), or element_blank() to draw nothing, and an element takes what it does not set from a more general one: axis.title.x.bottom from axis.title.x, that from axis.title, and that from text. Giggly changes a plot's theme with Giggly.Plot.theme/2, each element named as ggplot2 names it with underscores for its dots (axis_title_x_bottom): a line, a rectangle or a text is a keyword list of its properties, element_blank() is :none, rel(2) is {:rel, 2}, and a length such as unit(.25, "cm") is {0.25, :cm}. It has no element_geom(), and no palettes in the theme (palette.colour.continuous). A plot is described with Giggly.Plot and drawn with Giggly.to_svg/2, which returns an SVG document. The setup cell aliases Giggly.Plot, so the cells write Plot.new/2, and defines render, which draws a plot and shows it below the cell with Kino.Image. The data in these examples is Giggly.Datasets.mtcars/0, R's mtcars: 32 cars from the 1974 Motor Trend US magazine, which the setup cell binds to mtcars, R's name for it.

A plot with a title

p1 <- ggplot(mtcars, aes(wt, mpg)) +
  geom_point() +
  labs(title = "Fuel economy declines as weight increases")
p1
p1 =
  mtcars
  |> Plot.new(%{x: :wt, y: :mpg})
  |> Plot.geom_point()
  |> Plot.labs(title: "Fuel economy declines as weight increases")

render.(p1)

The plot's title and background

# Plot ---------------------------------------------------------------------
p1 + theme(plot.title = element_text(size = rel(2)))
p1 + theme(plot.background = element_rect(fill = "green"))

rel(2) doubles the title's size. CSS's "green" is #008000, darker than R's, #00FF00, so this background is darker than ggplot2's.

p1
|> Plot.theme(plot_title: [size: {:rel, 2}])
|> render.()
p1
|> Plot.theme(plot_background: [fill: "green"])
|> render.()

The panel's background, border and gridlines

# Panels --------------------------------------------------------------------

p1 + theme(panel.background = element_rect(fill = "white", colour = "grey50"))
p1 + theme(panel.border = element_rect(linetype = "dashed"))
p1 + theme(panel.grid.major = element_line(colour = "black"))
p1 + theme(
  panel.grid.major.y = element_blank(),
  panel.grid.minor.y = element_blank()
)

R's "grey50" is a name CSS lacks, so the cell writes its hex, "#7F7F7F". The border is drawn over the points and never filled.

p1
|> Plot.theme(panel_background: [fill: "white", colour: "#7F7F7F"])
|> render.()
p1
|> Plot.theme(panel_border: [linetype: :dashed])
|> render.()
p1
|> Plot.theme(panel_grid_major: [colour: "black"])
|> render.()
p1
|> Plot.theme(
  panel_grid_major_y: :none,
  panel_grid_minor_y: :none
)
|> render.()

Gridlines over the data

# Put gridlines on top of data
p1 + theme(
  panel.background = element_rect(fill = NA),
  panel.grid.major = element_line(colour = "grey50"),
  panel.ontop = TRUE
)

R's NA, no fill, is :none, and "grey50" is written as its hex, "#7F7F7F".

p1
|> Plot.theme(
  panel_background: [fill: :none],
  panel_grid_major: [colour: "#7F7F7F"],
  panel_ontop: true
)
|> render.()

Axis lines, text and ticks

# Axes ----------------------------------------------------------------------
# Change styles of axes texts and lines
p1 + theme(axis.line = element_line(linewidth = 3, colour = "grey80"))
p1 + theme(axis.text = element_text(colour = "blue"))
p1 + theme(axis.ticks = element_line(linewidth = 2))

theme_gray() draws no axis lines until a setting gives them. R's "grey80" is a name CSS lacks, so the cell writes its hex, "#CCCCCC".

p1
|> Plot.theme(axis_line: [linewidth: 3, colour: "#CCCCCC"])
|> render.()
p1
|> Plot.theme(axis_text: [colour: "blue"])
|> render.()
p1
|> Plot.theme(axis_ticks: [linewidth: 2])
|> render.()

The y axis's title

# Change the appearance of the y-axis title
p1 + theme(axis.title.y = element_text(size = rel(1.5), angle = 90))
p1
|> Plot.theme(axis_title_y: [size: {:rel, 1.5}, angle: 90])
|> render.()

The length and direction of ticks

# Make ticks point outwards on y-axis and inwards on x-axis
p1 + theme(
  axis.ticks.length.y = unit(.25, "cm"),
  axis.ticks.length.x = unit(-.25, "cm"),
  axis.text.x = element_text(margin = margin(t = .3, unit = "cm"))
)

unit(.25, "cm") is {0.25, :cm}, and a negative length points the x axis's ticks into the panel. margin(t = .3, unit = "cm") is a keyword list of the sides it sets, [top: {0.3, :cm}].

p1
|> Plot.theme(
  axis_ticks_length_y: {0.25, :cm},
  axis_ticks_length_x: {-0.25, :cm},
  axis_text_x: [margin: [top: {0.3, :cm}]]
)
|> render.()

A plot with legends

# Legend --------------------------------------------------------------------
p2 <- ggplot(mtcars, aes(wt, mpg)) +
  geom_point(aes(colour = factor(cyl), shape = factor(vs))) +
  labs(
    x = "Weight (1000 lbs)",
    y = "Fuel economy (mpg)",
    colour = "Cylinders",
    shape = "Transmission"
   )
p2

Giggly writes factor(cyl) as {:factor, [:cyl]}.

p2 =
  mtcars
  |> Plot.new(%{x: :wt, y: :mpg})
  |> Plot.geom_point(%{colour: {:factor, [:cyl]}, shape: {:factor, [:vs]}})
  |> Plot.labs(
    x: "Weight (1000 lbs)",
    y: "Fuel economy (mpg)",
    colour: "Cylinders",
    shape: "Transmission"
  )

render.(p2)

Where the legends go

# Position
p2 + theme(legend.position = "none")
p2 + theme(legend.justification = "top")
p2 + theme(legend.position = "bottom")

A position is an atom, and so is a justification given by name: :top hangs the legends right of the panels from their top.

p2
|> Plot.theme(legend_position: :none)
|> render.()
p2
|> Plot.theme(legend_justification: :top)
|> render.()
p2
|> Plot.theme(legend_position: :bottom)
|> render.()

Legends inside the panel

# Or place legends inside the plot using relative coordinates between 0 and 1
# legend.justification sets the corner that the position refers to
p2 + theme(
  legend.position = "inside",
  legend.position.inside = c(.95, .95),
  legend.justification = c("right", "top"),
  legend.box.just = "right",
  legend.margin = margin_auto(6)
)

c(.95, .95) is {0.95, 0.95}, shares of the panel's width and height, and c("right", "top") is {:right, :top}. margin_auto(6), 6 points on every side, is 6.

p2
|> Plot.theme(
  legend_position: :inside,
  legend_position_inside: {0.95, 0.95},
  legend_justification: {:right, :top},
  legend_box_just: :right,
  legend_margin: 6
)
|> render.()

A box around the legends

# The legend.box properties work similarly for the space around
# all the legends
p2 + theme(
  legend.box.background = element_rect(),
  legend.box.margin = margin_auto(6)
)

element_rect() with nothing set is []: a white background with a black border, as ggplot2 draws it, since the box's background takes nothing from the theme's other rectangles.

p2
|> Plot.theme(
  legend_box_background: [],
  legend_box_margin: 6
)
|> render.()

Legend keys, text and titles

# You can also control the display of the keys
# and the justification related to the plot area can be set
p2 + theme(legend.key = element_rect(fill = "white", colour = "black"))
p2 + theme(legend.text = element_text(size = 8, colour = "red"))
p2 + theme(legend.title = element_text(face = "bold"))

A text's face is an atom, :bold.

p2
|> Plot.theme(legend_key: [fill: "white", colour: "black"])
|> render.()
p2
|> Plot.theme(legend_text: [size: 8, colour: "red"])
|> render.()
p2
|> Plot.theme(legend_title: [face: :bold])
|> render.()

Strips

ggplot2's ~ cyl names the column to facet by; Giggly names it as a mapping does, by an atom or a string.

# Strips --------------------------------------------------------------------

p3 <- ggplot(mtcars, aes(wt, mpg)) +
  geom_point() +
  facet_wrap(~ cyl)
p3
p3 =
  mtcars
  |> Plot.new(%{x: :wt, y: :mpg})
  |> Plot.geom_point()
  |> Plot.facet_wrap(:cyl)

render.(p3)
p3 + theme(strip.background = element_rect(colour = "black", fill = "white"))
p3 + theme(strip.text.x = element_text(colour = "white", face = "bold"))
# More direct strip.text.x here for top
# as in the facet_wrap the default strip.position is "top"
p3 + theme(strip.text.x.top = element_text(colour = "white", face = "bold"))
p3 + theme(panel.spacing = unit(1, "lines"))

unit(1, "lines") is {1, :lines}.

p3
|> Plot.theme(strip_background: [colour: "black", fill: "white"])
|> render.()
p3
|> Plot.theme(strip_text_x: [colour: "white", face: :bold])
|> render.()
p3
|> Plot.theme(strip_text_x_top: [colour: "white", face: :bold])
|> render.()
p3
|> Plot.theme(panel_spacing: {1, :lines})
|> render.()

Default colours for layers

# Colours -------------------------------------------------------------------

p4 <- ggplot(mtcars, aes(wt, mpg)) +
  geom_point() +
  annotate(label = "Text Annotation", x = 5, y = 30, geom = "text")

# You can use the 'ink' setting to set colour defaults for all layers
p4 + theme(geom = element_geom(ink = "dodgerblue"))

Not in Giggly yet: element_geom(), here to set the colour every layer draws in.

Accent and paper colours

# Alternate colours are derived from the 'accent' and 'paper' settings
p4 + geom_smooth(method = "lm", formula = y ~ x) +
  theme(geom = element_geom(accent = "tomato", paper = "orchid"))

Not in Giggly yet: geom_smooth()'s formula option, here y ~ x, and element_geom(), here to set the accent and paper colours layers draw with.

A default palette

# You can also set default palettes in the theme
p4 + aes(colour = drat) +
  theme(palette.colour.continuous = c("white", "pink", "hotpink"))

Not in Giggly yet: adding a mapping with + aes(), and the theme's palette.colour.continuous setting, the palette a continuous colour scale draws from.