Powered by AppSignal & Oban Pro

Continuous position scales: scale_continuous

notebooks/scale_continuous.livemd

Continuous position scales: scale_continuous

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

alias Giggly.Plot

mpg = Giggly.Datasets.mpg()

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 scale_x_continuous(), which covers scale_y_continuous(), scale_x_log10(), scale_y_log10(), scale_x_reverse(), scale_y_reverse(), scale_x_sqrt() and scale_y_sqrt() 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.

scale_x_continuous() and scale_y_continuous() are the default scales for a continuous x and y. Setting one by hand changes its title, limits, breaks or labels, or transforms it; the log10, square root and reverse scales are shortcuts for common transformations. Giggly has each of these functions, Giggly.Plot.scale_x_continuous/2 and the rest, with their titles, limits, breaks, minor breaks, labels, expansion and transformations, and formatters for labels in Giggly.Format; it has no sec.axis option (sec_axis()), and no transformation of your own (scales::new_transform()). 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 cells draw Giggly.Datasets.mpg/0, ggplot2's mpg: fuel economy for 234 cars of 38 popular models from 1999 to 2008, which the setup cell binds to mpg, ggplot2's name for it.

The default scales

Both columns are numbers, so both axes get continuous scales, each titled by its column.

p1 <- ggplot(mpg, aes(displ, hwy)) +
  geom_point()
p1
p1 =
  mpg
  |> Plot.new(%{x: :displ, y: :hwy})
  |> Plot.geom_point()

render.(p1)

Axis titles

# Manipulating the default position scales lets you:
#  * change the axis labels
p1 +
  scale_x_continuous("Engine displacement (L)") +
  scale_y_continuous("Highway MPG")

R's first argument is the scale's name, which titles its axis; Giggly's is name:.

p1
|> Plot.scale_x_continuous(name: "Engine displacement (L)")
|> Plot.scale_y_continuous(name: "Highway MPG")
|> render.()

Removing axis titles

# You can also use the short-cut labs().
# Use NULL to suppress axis labels
p1 + labs(x = NULL, y = NULL)

Giggly writes R's NULL as :none, which removes each title and the room it took.

p1
|> Plot.labs(x: :none, y: :none)
|> render.()

Axis limits

#  * modify the axis limits
p1 + scale_x_continuous(limits = c(2, 6))
p1 + scale_x_continuous(limits = c(0, 10))

Limits narrower than the data drop the cars outside them, which ggplot2 warns of and Giggly does not; wider ones leave room beyond the data.

p1
|> Plot.scale_x_continuous(limits: {2, 6})
|> render.()
p1
|> Plot.scale_x_continuous(limits: {0, 10})
|> render.()

The xlim() shortcut

# you can also use the short hand functions `xlim()` and `ylim()`
p1 + xlim(2, 6)
p1
|> Plot.xlim(2, 6)
|> render.()

Where the ticks go

#  * choose where the ticks appear
p1 + scale_x_continuous(breaks = c(2, 4, 6))
p1
|> Plot.scale_x_continuous(breaks: [2, 4, 6])
|> render.()

Labels of your own

#  * choose your own labels
p1 + scale_x_continuous(
  breaks = c(2, 4, 6),
  label = c("two", "four", "six")
)

R matches label to the argument it begins, labels, which Giggly spells out.

p1
|> Plot.scale_x_continuous(breaks: [2, 4, 6], labels: ["two", "four", "six"])
|> render.()

Formatting labels

The example makes a small table of random numbers with R's set.seed(1), so that its labels have large numbers and fractions to format.

# Typically you'll pass a function to the `labels` argument.
# Some common formats are built into the scales package:
set.seed(1)
df <- data.frame(
  x = rnorm(10) * 100000,
  y = seq(0, 1, length.out = 10)
)
p2 <- ggplot(df, aes(x, y)) + geom_point()
p2 + scale_y_continuous(labels = scales::label_percent())
p2 + scale_y_continuous(labels = scales::label_dollar())
p2 + scale_x_continuous(labels = scales::label_comma())

The cell writes out the x R drew, each number to the last digit R holds, and works out y as R's seq() does, each step a ninth. Giggly's formatters are Giggly.Format's, each handed every break so that the labels share their decimals, as the scales package's are: percent/2, currency/2, which writes what label_dollar() writes at its defaults, and comma/2. The first two plots' x axis, left to its default labels, reads 0 and 100000 where R writes 0e+00 and 1e+05, since Giggly writes numbers as Elixir does.

df = %{
  "x" => [
    -62645.38107423324, 18364.332422208223, -83562.86124100472, 159528.08021377915,
    32950.777181536054, -82046.83841180152, 48742.90524284853, 73832.47051292173,
    57578.13516534923, -30538.838715635604
  ],
  "y" => Enum.map(0..9, fn step -> step * (1 / 9) end)
}

p2 =
  df
  |> Plot.new(%{x: :x, y: :y})
  |> Plot.geom_point()

p2
|> Plot.scale_y_continuous(labels: &Giggly.Format.percent/1)
|> render.()
p2
|> Plot.scale_y_continuous(labels: &Giggly.Format.currency/1)
|> render.()
p2
|> Plot.scale_x_continuous(labels: &Giggly.Format.comma/1)
|> render.()

Transformations

# You can also override the default linear mapping by using a
# transformation. There are three shortcuts:
p1 + scale_y_log10()
p1 + scale_y_sqrt()
p1 + scale_y_reverse()
p1
|> Plot.scale_y_log10()
|> render.()
p1
|> Plot.scale_y_sqrt()
|> render.()
p1
|> Plot.scale_y_reverse()
|> render.()

Other transformations

# Or you can supply a transformation in the `trans` argument:
p1 + scale_y_continuous(transform = scales::transform_reciprocal())

# You can also create your own. See ?scales::new_transform

Giggly names a transformation by an atom, :identity, :log10, :sqrt, :reverse or :reciprocal, where R takes the scales package's objects; it has none of the package's others, and makes none of your own.

p1
|> Plot.scale_y_continuous(transform: :reciprocal)
|> render.()