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