Powered by AppSignal & Oban Pro

Stacking: position_stack

notebooks/position_stack.livemd

Stacking: position_stack

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

alias Giggly.Plot

mtcars = Giggly.Datasets.mtcars()
diamonds = Giggly.Datasets.diamonds()

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 position_stack(), which covers position_fill() too, 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.

position_stack() stacks bars, areas and other objects that share an x on top of one another; position_fill() stacks them too, then stretches every stack to the same height, so its parts show proportions. A stack runs in the reverse order of the group aesthetic, so its colours line up with the default legend, and positive values stack upward from zero while negative ones stack downward. vjust places an object that has a position rather than a size, such as a label, within its part of the stack, and reverse turns the order over. Giggly stacks bars and areas: it is the default position: :stack of Giggly.Plot.geom_bar/3, Giggly.Plot.geom_col/3, Giggly.Plot.geom_histogram/3 and Giggly.Plot.geom_area/3, and Giggly.Plot.geom_density/3 stacks its curves with position: :stack. It has none of the rest yet: no position_fill(), neither of position_stack()'s options, vjust and reverse, and no position option for lines, points or text. 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. It binds R's mtcars and ggplot2's diamonds, Giggly.Datasets.mtcars/0 and Giggly.Datasets.diamonds/0, to their names in R.

Stacking and filling bars

# Stacking and filling ------------------------------------------------------

# Stacking is the default behaviour for most area plots.
# Fill makes it easier to compare proportions
ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
  geom_bar()
ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
  geom_bar(position = "fill")

Giggly draws the first plot, writing factor(cyl) as {:factor, [:cyl]}.

mtcars
|> Plot.new(%{x: {:factor, [:cyl]}, fill: {:factor, [:vs]}})
|> Plot.geom_bar()
|> render.()

Not in Giggly yet: position_fill().

Stacking and filling histograms

ggplot(diamonds, aes(price, fill = cut)) +
  geom_histogram(binwidth = 500)
ggplot(diamonds, aes(price, fill = cut)) +
  geom_histogram(binwidth = 500, position = "fill")

Giggly draws the first plot: Giggly.Plot.geom_histogram/3 stacks its bars, and fills cut, an ordered factor, with viridis in the order of its levels, as R does.

diamonds
|> Plot.new(%{x: :price, fill: :cut})
|> Plot.geom_histogram(binwidth: 500)
|> render.()

Not in Giggly yet: position_fill().

Stacked areas

# Stacking is also useful for time series
set.seed(1)
series <- data.frame(
  time = c(rep(1, 4),rep(2, 4), rep(3, 4), rep(4, 4)),
  type = rep(c('a', 'b', 'c', 'd'), 4),
  value = rpois(16, 10)
)
ggplot(series, aes(time, value)) +
  geom_area(aes(fill = type))

R's rpois() draws the values at random, the same ones every time after set.seed(1); the cell writes out the 16 it draws.

series = %{
  "time" => Enum.flat_map([1.0, 2.0, 3.0, 4.0], fn time -> List.duplicate(time, 4) end),
  "type" => List.flatten(List.duplicate(["a", "b", "c", "d"], 4)),
  "value" => [8, 10, 7, 11, 14, 12, 11, 9, 14, 11, 8, 2, 8, 10, 7, 12]
}

series
|> Plot.new(%{x: :time, y: :value})
|> Plot.geom_area(%{fill: :type})
|> render.()

Setting the stacking order

# Stacking order ------------------------------------------------------------
# The stacking order is carefully designed so that the plot matches
# the legend.

# You control the stacking order by setting the levels of the underlying
# factor. See the forcats package for convenient helpers.
series$type2 <- factor(series$type, levels = c('c', 'b', 'd', 'a'))
ggplot(series, aes(time, value)) +
  geom_area(aes(fill = type2))

factor()'s levels give type2 its order, which Giggly.Factor.new/2 gives a column in the table.

series =
  Map.put(series, "type2", Giggly.Factor.new(series["type"], levels: ["c", "b", "d", "a"]))

series
|> Plot.new(%{x: :time, y: :value})
|> Plot.geom_area(%{fill: :type2})
|> render.()

Ordering the legend

# You can change the order of the levels in the legend using the scale
ggplot(series, aes(time, value)) +
  geom_area(aes(fill = type)) +
  scale_fill_discrete(breaks = c('a', 'b', 'c', 'd'))
series
|> Plot.new(%{x: :time, y: :value})
|> Plot.geom_area(%{fill: :type})
|> Plot.scale_fill_discrete(breaks: ["a", "b", "c", "d"])
|> render.()

Reversing the stack

# If you've flipped the plot, use reverse = TRUE so the levels
# continue to match
ggplot(series, aes(time, value)) +
  geom_area(aes(fill = type2), position = position_stack(reverse = TRUE)) +
  coord_flip() +
  theme(legend.position = "top")

Not in Giggly yet: position_stack()'s reverse option and coord_flip().

Stacking lines and points

# Non-area plots ------------------------------------------------------------

# When stacking across multiple layers it's a good idea to always set
# the `group` aesthetic in the ggplot() call. This ensures that all layers
# are stacked in the same way.
ggplot(series, aes(time, value, group = type)) +
  geom_line(aes(colour = type), position = "stack") +
  geom_point(aes(colour = type), position = "stack")

Not in Giggly yet: geom_line()'s and geom_point()'s position option, here position = "stack".

A line on each area

ggplot(series, aes(time, value, group = type)) +
  geom_area(aes(fill = type)) +
  geom_line(aes(group = type), position = "stack")

Not in Giggly yet: geom_line()'s position option, here position = "stack".

Stacking labels

# You can also stack labels, but the default position is suboptimal.
ggplot(series, aes(time, value, group = type)) +
  geom_area(aes(fill = type)) +
  geom_text(aes(label = type), position = "stack")

Not in Giggly yet: geom_text()'s position option, here position = "stack".

Centring labels

# You can override this with the vjust parameter. A vjust of 0.5
# will center the labels inside the corresponding area
ggplot(series, aes(time, value, group = type)) +
  geom_area(aes(fill = type)) +
  geom_text(aes(label = type), position = position_stack(vjust = 0.5))

Not in Giggly yet: geom_text()'s position option, and position_stack()'s vjust option.

Negative values

# Negative values -----------------------------------------------------------

df <- data.frame(
  x = rep(c("a", "b"), 2:3),
  y = c(1, 2, 1, 3, -1),
  grp = c("x", "y", "x", "y", "y")
)

ggplot(data = df, aes(x, y, group = grp)) +
  geom_col(aes(fill = grp), position = position_stack(reverse = TRUE)) +
  geom_hline(yintercept = 0)

R's rep() repeats "a" twice and "b" three times. The one negative value stacks downward from zero, apart from the positive ones.

Not in Giggly yet: position_stack()'s reverse option and geom_hline().

Labels in the middle of each part

ggplot(data = df, aes(x, y, group = grp)) +
  geom_col(aes(fill = grp)) +
  geom_hline(yintercept = 0) +
  geom_text(aes(label = grp), position = position_stack(vjust = 0.5))

Not in Giggly yet: geom_hline(), geom_text()'s position option, and position_stack()'s vjust option.