Powered by AppSignal & Oban Pro

Scale limits: lims

notebooks/lims.livemd

Scale limits: lims

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 lims(), which covers xlim() and ylim() 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.

xlim() and ylim() set the limits of the x and y scales, and lims() those of any scale, named by its aesthetic, without writing out the scale itself. Values outside the limits become missing and are dropped from the plot; giving the larger limit first reverses the scale, and NA leaves a limit to the data. To zoom without dropping data, ggplot2 sets the limits on the coordinate system instead, with coord_cartesian(). Giggly has Giggly.Plot.xlim/3, Giggly.Plot.ylim/3 and Giggly.Plot.lims/2, nil leaving a limit to the data, but no coord_cartesian(). 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 most 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.

Zooming into a range

# Zoom into a specified area
ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  xlim(15, 20)

ggplot2 drops the cars outside 15 to 20 mpg, and warns that it removed them. Giggly drops them without a warning, since drawing a plot has no side effects.

mtcars
|> Plot.new(%{x: :mpg, y: :wt})
|> Plot.geom_point()
|> Plot.xlim(15, 20)
|> render.()

A reversed scale

# reverse scale
ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  xlim(20, 15)

With the larger limit first the axis runs from 20 down to 15.

mtcars
|> Plot.new(%{x: :mpg, y: :wt})
|> Plot.geom_point()
|> Plot.xlim(20, 15)
|> render.()

One limit from the data

# with automatic lower limit
ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  xlim(NA, 20)

R's NA leaves the lower limit to the data, the smallest mpg, 10.4; Giggly writes it nil.

mtcars
|> Plot.new(%{x: :mpg, y: :wt})
|> Plot.geom_point()
|> Plot.xlim(nil, 20)
|> render.()

Limits wider than the data

Both plots give the colour scale all three cylinder counts, so a count has the same colour in each, although each plot's data has only some of them.

# You can also supply limits that are larger than the data.
# This is useful if you want to match scales across different plots
small <- subset(mtcars, cyl == 4)
big <- subset(mtcars, cyl > 4)

ggplot(small, aes(mpg, wt, colour = factor(cyl))) +
  geom_point() +
  lims(colour = c("4", "6", "8"))

ggplot(big, aes(mpg, wt, colour = factor(cyl))) +
  geom_point() +
  lims(colour = c("4", "6", "8"))

The cell keeps the rows R's subset() keeps, each column's values where cyl passes the test. Giggly writes factor(cyl) as {:factor, [:cyl]}, and names each limit as the column holds it, the numbers 4, 6 and 8, where R names them as text. The legend of each plot has a key for every limit, but a point drawn only in the keys its own data holds.

subset = fn keep? ->
  kept = Enum.map(Keyword.fetch!(mtcars, :cyl), keep?)

  Enum.map(mtcars, fn {name, values} ->
    rows =
      values
      |> Enum.zip(kept)
      |> Enum.filter(fn {_value, keep} -> keep end)
      |> Enum.map(fn {value, _keep} -> value end)

    {name, rows}
  end)
end

small = subset.(fn cyl -> cyl == 4 end)
big = subset.(fn cyl -> cyl > 4 end)

small
|> Plot.new(%{x: :mpg, y: :wt, colour: {:factor, [:cyl]}})
|> Plot.geom_point()
|> Plot.lims(colour: [4, 6, 8])
|> render.()
big
|> Plot.new(%{x: :mpg, y: :wt, colour: {:factor, [:cyl]}})
|> Plot.geom_point()
|> Plot.lims(colour: [4, 6, 8])
|> render.()

Two ways to limit an axis

Sys.Date() is today's date, so the data is the last 60 days, each with a random price, drawn as a line with a smoothed curve through it.

# There are two ways of setting the axis limits: with limits or
# with coordinate systems. They work in two rather different ways.

set.seed(1)
last_month <- Sys.Date() - 0:59
df <- data.frame(
  date = last_month,
  price = c(rnorm(30, mean = 15), runif(30) + 0.2 * (1:30))
)

p <- ggplot(df, aes(date, price)) +
  geom_line() +
  stat_smooth()

p

The cell takes today's date from the clock, as Sys.Date() does, though Date.utc_today/0 gives the date in UTC where R gives the local one, and writes out the 60 prices R draws after set.seed(1). With 60 rows the smooth is a loess fit along the dates. Giggly fits a loess curve at each of its points directly, as R's loess(surface = "direct") does, where R's default joins fits made at fewer places, so its curve sits up to 5.2 points from R's here, and its band's edges up to 5.4.

today = Date.utc_today()

df = %{
  "date" => Enum.map(0..59, fn days -> Date.add(today, -days) end),
  "price" => [
    14.373546189257668, 15.183643324222082, 14.164371387589952, 16.595280802137793,
    15.329507771815361, 14.179531615881984, 15.487429052428485, 15.738324705129218,
    15.575781351653493, 14.694611612843644, 16.51178116845085, 15.389843236411432,
    14.378759419458197, 12.785300112822501, 16.124930918143107, 14.955066390984769,
    14.983809736901055, 15.9438362106853, 15.821221195098088, 15.593901321217508,
    15.918977371608218, 15.782136300731068, 15.07456498336519, 13.010648304136627,
    15.61982574789471, 14.943871260471, 14.84420449329467, 13.529247616100726, 14.521849944891379,
    15.417941560199703, 1.1128759242594242, 0.6936033727601171, 1.0590657262597234,
    1.1323946741875262, 1.6508704670704901, 1.4580167807172986, 1.8785452482756229,
    2.3663106706459076, 1.8842469143681229, 2.875321330036968, 2.5390729378443213,
    3.2394403501879427, 2.9466834891587497, 3.1337749307975176, 3.4763512450736016,
    4.092198335845024, 4.264339470630512, 3.989989543473348, 4.577320698834956, 4.960617997217923,
    4.6346594847738745, 5.112514678714797, 4.9999943688977515, 5.125352151878179,
    5.7570871480274945, 5.402692255144939, 6.111121222469956, 5.721691921027378, 6.045488513959572,
    6.14330437942408
  ]
}

p =
  df
  |> Plot.new(%{x: :date, y: :price})
  |> Plot.geom_line()
  |> Plot.stat_smooth()

render.(p)

Limits that drop data

# Setting the limits with the scale discards all data outside the range.
p + lims(x= c(Sys.Date() - 30, NA), y = c(10, 20))

The smooth is fitted only to the rows left inside the limits. R's NA is written nil. Giggly's curve sits up to 2.6 points from R's, and its band's edges up to 3.8.

p
|> Plot.lims(x: {Date.add(today, -30), nil}, y: {10, 20})
|> render.()

Limits that zoom

# For changing x or y axis limits **without** dropping data
# observations use [coord_cartesian()]. Setting the limits on the
# coordinate system performs a visual zoom.
p + coord_cartesian(xlim =c(Sys.Date() - 30, NA), ylim = c(10, 20))

Not in Giggly yet: coord_cartesian() and its xlim and ylim options. Giggly's plots are in Cartesian coordinates, as ggplot2's are by default, but nothing sets their options.