Powered by AppSignal & Oban Pro

Dodging: position_dodge

notebooks/position_dodge.livemd

Dodging: position_dodge

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

alias Giggly.Plot

mtcars = Giggly.Datasets.mtcars()
mpg = Giggly.Datasets.mpg()
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_dodge(), which covers position_dodge2() 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.

Dodging keeps each mark's vertical position and sets the marks that share an x side by side. position_dodge() dodges by group, so it needs the groups; position_dodge2() needs none, dodging each mark by its own extent and leaving a little padding between them, which suits box plots of different widths. width gives the width to dodge within, for marks narrower than the bars they sit beside, and preserve keeps either the total width at each position or the width of each mark. Giggly dodges bars and histograms as position = "dodge" does, with position: :dodge, which has no options, and bars, histograms and box plots as position_dodge2() does, with position: :dodge2 or Giggly.Plot.position_dodge2/1 and its preserve:, padding: and reverse:. Not in Giggly yet: position_dodge()'s options and position_dodge2()'s width option. The setup cell binds R's mtcars and ggplot2's mpg and diamonds, Giggly.Datasets.mtcars/0, Giggly.Datasets.mpg/0 and Giggly.Datasets.diamonds/0, to their names in R.

Dodging without groups

ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
  geom_bar(position = "dodge2")

Giggly writes factor(cyl) as {:factor, [:cyl]} and position = "dodge2" as position: :dodge2.

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

The width of each bar

# By default, dodging with `position_dodge2()` preserves the total width of
# the elements. You can choose to preserve the width of each element with:
ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
  geom_bar(position = position_dodge2(preserve = "single"))

Giggly.Plot.position_dodge2/1 takes ggplot2's options, a word as an atom.

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

Dodged histograms

ggplot(diamonds, aes(price, fill = cut)) +
  geom_histogram(position="dodge2")
# see ?geom_bar for more examples

Giggly bins and dodges the diamonds with Giggly.Plot.geom_histogram/3 and position: :dodge2, 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(position: :dodge2)
|> render.()

Frequency polygons instead

# In this case a frequency polygon is probably a better choice
ggplot(diamonds, aes(price, colour = cut)) +
  geom_freqpoly()
diamonds
|> Plot.new(%{x: :price, colour: :cut})
|> Plot.geom_freqpoly()
|> render.()

Columns dodged by group

# Dodging with various widths -------------------------------------
# To dodge items with different widths, you need to be explicit
df <- data.frame(
  x = c("a","a","b","b"),
  y = 2:5,
  g = rep(1:2, 2)
)
p <- ggplot(df, aes(x, y, group = g)) +
  geom_col(position = "dodge", fill = "grey50", colour = "black")
p

rep(1:2, 2) is 1, 2, 1, 2, so each x has a column for each group. position: :dodge sets the columns side by side within the width one column takes, and fill: and colour: fill and border them. R's "grey50" has no name in CSS, so the fill is written as its hex, "#7F7F7F".

df = %{"x" => ["a", "a", "b", "b"], "y" => [2, 3, 4, 5], "g" => [1, 2, 1, 2]}

p =
  df
  |> Plot.new(%{x: :x, y: :y, group: :g})
  |> Plot.geom_col(position: :dodge, fill: "#7F7F7F", colour: "black")

render.(p)

A line range has no width

# A line range has no width:
p + geom_linerange(aes(ymin = y - 1, ymax = y + 1), position = "dodge")

With no width to dodge within, the line ranges are not moved apart, and do not sit on their columns.

Not in Giggly yet: line ranges and geom_linerange().

Giving the width

# So you must explicitly specify the width
p + geom_linerange(
  aes(ymin = y - 1, ymax = y + 1),
  position = position_dodge(width = 0.9)
)

Dodged within 0.9, the width the columns take, each line range sits on its column.

Not in Giggly yet: geom_linerange() and position_dodge()'s options.

Error bars

# The same principle applies to error bars, which are usually
# narrower than the bars
p + geom_errorbar(
  aes(ymin = y - 1, ymax = y + 1),
  width = 0.2,
  position = "dodge"
)
p + geom_errorbar(
  aes(ymin = y - 1, ymax = y + 1),
  width = 0.2,
  position = position_dodge(width = 0.9)
)

"dodge" sets the error bars side by side within their own width, 0.2, so they huddle at the middle of each x; dodged within 0.9, each sits on its column.

Not in Giggly yet: error bars, geom_errorbar() and its width option, and position_dodge()'s options.

Box plots

# Box plots use position_dodge2 by default, and bars can use it too
ggplot(mpg, aes(factor(year), displ)) +
  geom_boxplot(aes(colour = hwy < 30))

Giggly writes hwy < 30 as {&Kernel.</2, [:hwy, 30]}, which titles the legend hwy < 30, as R does.

Plot.new(mpg, %{x: {:factor, [:year]}, y: :displ})
|> Plot.geom_boxplot(%{colour: {&Kernel.</2, [:hwy, 30]}})
|> render.()

Box plots of different widths

ggplot(mpg, aes(factor(year), displ)) +
  geom_boxplot(aes(colour = hwy < 30), varwidth = TRUE)

varwidth = TRUE makes each box's width grow with the square root of its number of rows, and position_dodge2() dodges the boxes by their own widths.

Plot.new(mpg, %{x: {:factor, [:year]}, y: :displ})
|> Plot.geom_boxplot(%{colour: {&Kernel.</2, [:hwy, 30]}}, varwidth: true)
|> render.()

Bars dodged the same way

ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
  geom_bar(position = position_dodge2(preserve = "single"))

ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
  geom_bar(position = position_dodge2(preserve = "total"))

Every 8-cylinder car has vs 0, so that x has one bar: as wide as one bar of a pair with preserve = "single", and as wide as the whole pair with preserve = "total".

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