Powered by AppSignal & Oban Pro

Paths and lines: geom_path

notebooks/geom_path.livemd

Paths and lines: geom_path

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

alias Giggly.Plot

economics = Giggly.Datasets.economics()

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 geom_path(), which covers geom_line() and geom_step() 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.

These geoms join observations with lines. geom_path() joins them in the order they appear in the data, geom_line() in order of x, and geom_step() with stairs that show exactly where the value changes. The group aesthetic decides which observations make one line, and a missing value in the middle of a line breaks it. Giggly has Giggly.Plot.geom_path/3 and Giggly.Plot.geom_line/3, but no geom_step() yet. 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. Most examples use Giggly.Datasets.economics/0, ggplot2's economics: US economic time series, one row a month from 1967 to 2015, its date column dates, which the setup cell binds to economics, ggplot2's name for it. The rest make small tables of their own, as ggplot2's do.

A time series

# geom_line() is suitable for time series
ggplot(economics, aes(date, unemploy)) + geom_line()

The x axis is a date scale: its breaks fall on the decades, labelled by year, as R's pretty() puts them for dates.

economics
|> Plot.new(%{x: :date, y: :unemploy})
|> Plot.geom_line()
|> render.()

A line for each series

# separate by colour and use "timeseries" legend key glyph
ggplot(economics_long, aes(date, value01, colour = variable)) +
  geom_line(key_glyph = "timeseries")

Not in Giggly yet: geom_line()'s key_glyph option.

A vertical time series

# You can get a timeseries that run vertically by setting the orientation
ggplot(economics, aes(unemploy, date)) + geom_line(orientation = "y")

With orientation: :y the rows are put in order of y, the dates, so the line runs up the panel, the y axis now the date scale.

economics
|> Plot.new(%{x: :unemploy, y: :date})
|> Plot.geom_line(orientation: :y)
|> render.()

Steps

# geom_step() is useful when you want to highlight exactly when
# the y value changes
recent <- economics[economics$date > as.Date("2013-01-01"), ]
ggplot(recent, aes(date, unemploy)) + geom_line()
ggplot(recent, aes(date, unemploy)) + geom_step()

recent keeps the months after the start of 2013. Giggly draws the first plot; the cell keeps those rows as R's subset does, each column's values where the date is later than 1 January 2013. Over a little more than two years the date scale's breaks fall every six months, labelled by year and month.

later = Enum.map(Keyword.fetch!(economics, :date), fn date -> Date.after?(date, ~D[2013-01-01]) end)

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

    {name, kept}
  end)

recent
|> Plot.new(%{x: :date, y: :unemploy})
|> Plot.geom_line()
|> render.()

Not in Giggly yet: geom_step().

A path through two variables

# geom_path lets you explore how two variables are related over time,
# e.g. unemployment and personal savings rate
m <- ggplot(economics, aes(unemploy/pop, psavert))
m + geom_path()
m + geom_path(aes(colour = as.numeric(date)))

geom_path() joins the months in the order of the rows, which is the order of their dates, rather than in order of x. {&Kernel.//2, [:unemploy, :pop]} divides in each row, and titles the x axis as Elixir writes it, unemploy / pop, where ggplot2 writes unemploy/pop.

m = Plot.new(economics, %{x: {&Kernel.//2, [:unemploy, :pop]}, y: :psavert})

m
|> Plot.geom_path()
|> render.()

R's as.numeric(date) is a date's days since 1970-01-01, which Date.diff/2 counts from that date, given as a constant. The colours are ggplot2's; the legend is titled by the call, Date.diff(date, ~D[1970-01-01]), where ggplot2's reads as.numeric(date), so it is wider and the panel narrower.

m
|> Plot.geom_path(%{colour: {&Date.diff/2, [:date, ~D[1970-01-01]]}})
|> render.()

A fixed colour

# Changing parameters ----------------------------------------------
ggplot(economics, aes(date, unemploy)) +
  geom_line(colour = "red")

colour: sets the colour of every line, as colour = does; "red" is the same colour in CSS as in R, #FF0000.

economics
|> Plot.new(%{x: :date, y: :unemploy})
|> Plot.geom_line(colour: "red")
|> render.()

Arrows

# Use the arrow parameter to add an arrow to the line
# See ?arrow for more details
c <- ggplot(economics, aes(x = date, y = pop))
c + geom_line(arrow = arrow())
c + geom_line(
  arrow = arrow(angle = 15, ends = "both", type = "closed")
)

arrow() comes from R's grid package and describes the arrowhead: its angle, which ends get one and whether it is open or closed.

Not in Giggly yet: geom_line()'s arrow option.

Line ends and joins

# Control line join parameters
df <- data.frame(x = 1:3, y = c(4, 1, 9))
base <- ggplot(df, aes(x, y))
base + geom_path(linewidth = 10)
base + geom_path(linewidth = 10, lineend = "round")
base + geom_path(linewidth = 10, linejoin = "mitre", lineend = "butt")

A wide line shows how its ends and its corner are drawn: cut square at the ends and rounded at the corner by default, then rounded at the ends too, then pointed at the corner. Giggly names a line's ends and corners by atoms, lineend: :round and linejoin: :mitre.

base = Plot.new(%{"x" => [1, 2, 3], "y" => [4.0, 1.0, 9.0]}, %{x: :x, y: :y})

base
|> Plot.geom_path(linewidth: 10)
|> render.()
base
|> Plot.geom_path(linewidth: 10, lineend: :round)
|> render.()
base
|> Plot.geom_path(linewidth: 10, linejoin: :mitre, lineend: :butt)
|> render.()

Missing values break a line

# You can use NAs to break the line.
df <- data.frame(x = 1:5, y = c(1, 2, NA, 4, 5))
ggplot(df, aes(x, y)) + geom_point() + geom_line()

ggplot2 draws the line in two pieces, either side of the row without a y, and warns that it removed that row's point. Giggly draws the same, without the warning, which a plot built without side effects cannot give.

%{"x" => [1, 2, 3, 4, 5], "y" => [1.0, 2.0, nil, 4.0, 5.0]}
|> Plot.new(%{x: :x, y: :y})
|> Plot.geom_point()
|> Plot.geom_line()
|> render.()

Line type and colour

# Setting line type vs colour/size
# Line type needs to be applied to a line as a whole, so it can
# not be used with colour or size that vary across a line
x <- seq(0.01, .99, length.out = 100)
df <- data.frame(
  x = rep(x, 2),
  y = c(qlogis(x), 2 * qlogis(x)),
  group = rep(c("a","b"),
  each = 100)
)
p <- ggplot(df, aes(x=x, y=y, group=group))
# These work
p + geom_line(linetype = 2)
p + geom_line(aes(colour = group), linetype = 2)
p + geom_line(aes(colour = x))

The table holds two curves, a and b, told apart by group; qlogis() is the logistic distribution's quantile function, log(x / (1 - x)), which the cell computes. The third line's colour changes along it, so it is drawn solid.

x = Enum.map(0..99, fn index -> 0.01 + index * (0.99 - 0.01) / 99 end)
logit = Enum.map(x, fn value -> :math.log(value / (1 - value)) end)

df = %{
  "x" => x ++ x,
  "y" => logit ++ Enum.map(logit, fn value -> 2 * value end),
  "group" => List.duplicate("a", 100) ++ List.duplicate("b", 100)
}

p = Plot.new(df, %{x: :x, y: :y, group: :group})

p
|> Plot.geom_line(linetype: 2)
|> render.()
p
|> Plot.geom_line(%{colour: :group}, linetype: 2)
|> render.()
p
|> Plot.geom_line(%{colour: :x})
|> render.()

A line type that cannot vary

# But this doesn't
should_stop(p + geom_line(aes(colour = x), linetype=2))

should_stop() checks that the code inside it fails, as it does here: a dashed line cannot change colour along its length, so ggplot2 stops with an error. Giggly refuses it too, raising an ArgumentError; the cell shows its message.

try do
  p
  |> Plot.geom_line(%{colour: :x}, linetype: 2)
  |> render.()
rescue
  error in ArgumentError -> Exception.message(error)
end