Recipe Types
Overview
There are four main types of recipes which are determined by the signature of the @recipe
macro.
User Recipes
@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...)
@userplot
provides a convenient way to create a custom type to dispatch on and defines custom plotting functions.
@userplot MyPlot
@recipe function f(mp::MyPlot; ...)
...
end
Now we can plot with:
myplot(args...; kw...)
myplot!(args...; kw...)
Type Recipes
@recipe function f(::Type{T}, val::T) where T
With RecipesBase 1.0 type recipes are aware of the current axis (:x
, :y
, :z
).
@recipe function f(::Type{MyType}, val::MyType)
guide --> "My Guide"
...
end
This only sets the guide for the axes with MyType
. For more complex type recipes the current axis letter can be accessed in @recipe
with plotattributes[:letter]
.
With RecipesBase 1.0 type recipes of the form
@recipe function f(::Type{T}, val::T) where T <: AbstractArray{MyType}
for AbstractArray
s of custom types are supported too.
User recipes and type recipes must return either
- an
AbstractArray{<:V}
whereV
is a valid type, - two functions, or
- nothing
A valid type is either a Plots datapoint or a type that can be handled by another user recipe or type recipe. Plots datapoints are all subtypes of Union{AbstractString, Missing}
and Union{Number, Missing}
.
If two functions are returned the former should tell Plots how to convert from T
to a datapoint and the latter how to convert from datapoint to string for tick label formatting.
Plot Recipes
@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...)
Series Recipes
@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...)
The @shorthands
macro provides a convenient way to define plotting functions for custom plot recipes or series recipes.
@shorthands myseriestype
@recipe function f(::Type{Val{:myseriestype}}, x, y, z; ...)
...
end
This allows to plot with:
myseriestype(args...; kw...)
myseriestype!(args...; kw...)
Plot recipes and series recipes have to set the seriestype
attribute.
User Recipes
User recipes are called early in the processing pipeline and allow designing custom visualizations.
@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...)
We have already seen an example for a user recipe in the syntax section above. User recipes can also be used to define a custom visualization without necessarily wishing to plot a custom type. For this purpose we can create a type to dispatch on. The @userplot
macro is a convenient way to do this.
@userplot MyPlot
expands to
mutable struct MyPlot
args
end
export myplot, myplot!
myplot(args...; kw...) = plot(MyPlot(args); kw...)
myplot!(args...; kw...) = plot!(MyPlot(args); kw...)
myplot!(p::AbstractPlot, args...; kw...) = plot!(p, MyPlot(args); kw...)
To check args
type, define a struct with type parameters.
@userplot struct MyPlot{T<:Tuple{AbstractVector}}
args::T
end
We can use this to define a user recipe for a pie plot.
# defines mutable struct `UserPie` and sets shorthands `userpie` and `userpie!`
@userplot UserPie
@recipe function f(up::UserPie)
y = up.args[end] # extract y from the args
# if we are passed two args, we use the first as labels
labels = length(up.args) == 2 ? up.args[1] : eachindex(y)
framestyle --> :none
aspect_ratio --> true
s = sum(y)
θ = 0
# add a shape for each piece of pie
for i in 1:length(y)
# determine the angle until we stop
θ_new = θ + 2π * y[i] / s
# calculate the coordinates
coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)]
@series begin
seriestype := :shape
label --> string(labels[i])
coords
end
θ = θ_new
end
# we already added all shapes in @series so we don't want to return a series
# here. (Technically we are returning an empty series which is not added to
# the legend.)
primary := false
()
end
Now we can just use the recipe like this:
userpie('A':'D', rand(4))
Type Recipes
Type recipes define one-to-one mappings from custom types to something Plots supports
@recipe function f(::Type{T}, val::T) where T
Suppose we have a custom wrapper for vectors.
struct MyWrapper
v::Vector
end
We can tell Plots to just use the wrapped vector for plotting in a type recipe.
@recipe f(::Type{MyWrapper}, mw::MyWrapper) = mw.v
Now Plots knows what to do when it sees a MyWrapper
.
mw = MyWrapper(cumsum(rand(10)))
plot(mw)
Due to the recursive application of type recipes they even compose automatically.
struct MyOtherWrapper
w
end
@recipe f(::Type{MyOtherWrapper}, mow::MyOtherWrapper) = mow.w
mow = MyOtherWrapper(mw)
plot(mow)
If we want an element-wise conversion of custom types we can define a conversion function to a type that Plots supports (Real
, AbstractString
) and a formatter for the tick labels. Consider the following simple time type.
struct MyTime
h::Int
m::Int
end
# show e.g. `MyTime(1, 30)` as "01:30"
time_string(mt) = join((lpad(string(c), 2, "0") for c in (mt.h, mt.m)), ":")
# map a `MyTime` object to the number of minutes that have passed since midnight.
# this is the actual data Plots will use.
minutes_since_midnight(mt) = 60 * mt.h + mt.m
# convert the minutes passed since midnight to a nice string showing `MyTime`
formatter(n) = time_string(MyTime(divrem(n, 60)...))
# define the recipe (it must return two functions)
@recipe f(::Type{MyTime}, mt::MyTime) = (minutes_since_midnight, formatter)
Now we can plot vectors of MyTime
automatically with the correct tick labelling. DateTime
s and Char
s are implemented with such a type recipe in Plots for example.
times = MyTime.(0:23, rand(0:59, 24))
vals = log.(1:24)
plot(times, vals)
Again everything composes nicely.
plot(MyWrapper(vals), MyOtherWrapper(times))
Plot Recipes
Plot recipes are called after all input data is processed by type recipes but before the plot and subplots are set-up. They allow to build series with custom layouts and set plot-wide attributes.
@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...)
Plot recipes define a new series type. They are applied after type recipes. Hence, standard Plots types can be assumed for input data :x
, :y
and :z
in plotattributes
. Plot recipes can access plot and subplot attributes before they are processed, for example to build layouts. Both, plot recipes and series recipes must change the series type. Otherwise we get a warning that we would run into a StackOverflow error.
We can define a seriestype :yscaleplot
, that automatically shows data with a linear y scale in one subplot and with a logarithmic yscale in another one.
@recipe function f(::Type{Val{:yscaleplot}}, plt::AbstractPlot)
x, y = plotattributes[:x], plotattributes[:y]
layout := (1, 2)
for (i, scale) in enumerate((:linear, :log))
@series begin
title --> string(scale, " scale")
seriestype := :path
subplot := i
yscale := scale
end
end
end
We can call it with plot(...; ..., seriestype = :yscaleplot)
or we can define a shorthand with the @shorthands
macro.
@shorthands myseries
expands to
export myseries, myseries!
myseries(args...; kw...) = plot(args...; kw..., seriestype = :myseries)
myseries!(args...; kw...) = plot!(args...; kw..., seriestype = :myseries)
So let's try the yscaleplot
plot recipe.
@shorthands yscaleplot
yscaleplot((1:10).^2)
Magically the composition with type recipes works again.
yscaleplot(MyWrapper(times), MyOtherWrapper((1:24).^2))
Series Recipes
Series recipes are applied recursively until the current backend supports a series type. They are used for example to convert the input data of a bar plot to the coordinates of the shapes that define the bars.
@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...)
If we want to call the userpie
recipe with a custom type we run into errors.
userpie(MyWrapper(rand(4)))
ERROR: MethodError: no method matching keys(::MyWrapper)
Stacktrace:
[1] eachindex(::MyWrapper) at ./abstractarray.jl:209
Furthermore, if we want to show multiple pie charts in different subplots, we don't get what we expect either
userpie(rand(4, 2), layout = 2)
We could overcome these issues by implementing the required AbstractArray
methods for MyWrapper
(instead of the type recipe) and by more carefully dealing with different series in the userpie
recipe. However, the simpler approach is writing the pie recipe as a series recipe and relying on Plots' processing pipeline.
@recipe function f(::Type{Val{:seriespie}}, x, y, z)
framestyle --> :none
aspect_ratio --> true
s = sum(y)
θ = 0
for i in eachindex(y)
θ_new = θ + 2π * y[i] / s
coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)]
@series begin
seriestype := :shape
label --> string(x[i])
x := first.(coords)
y := last.(coords)
end
θ = θ_new
end
end
@shorthands seriespie
seriespie! (generic function with 1 method)
Here we use the already processed values x
and y
to calculate the shape coordinates for each pie piece, update x
and y
with these coordinates and set the series type to :shape
.
seriespie(rand(4))
This automatically works together with type recipes ...
seriespie(MyWrapper(rand(4)))
... or with layouts
seriespie(rand(4, 2), layout = 2)
Remarks
Plot recipes and series recipes are actually very similar. In fact, a pie recipe could be also implemented as a plot recipe by acessing the data through plotattributes
.
@recipe function f(::Type{Val{:plotpie}}, plt::AbstractPlot)
y = plotattributes[:y]
labels = plotattributes[:x]
framestyle --> :none
aspect_ratio --> true
s = sum(y)
θ = 0
for i in 1:length(y)
θ_new = θ + 2π * y[i] / s
coords = [(0.0, 0.0); Plots.partialcircle(θ, θ_new, 50)]
@series begin
seriestype := :shape
label --> string(labels[i])
x := first.(coords)
y := last.(coords)
end
θ = θ_new
end
end
@shorthands plotpie
plotpie(rand(4, 2), layout = (1, 2))
The series recipe syntax is just a little nicer in this case.
Here's subtle difference between these recipe types: Plot recipes are applied in any case while series are only applied if the backend does not support the series type natively.
Let's try it the other way around and implement our yscaleplot
recipe as a series recipe.
@recipe function f(::Type{Val{:yscaleseries}}, x, y, z)
layout := (1, 2)
for (i, scale) in enumerate((:linear, :log))
@series begin
title --> string(scale, " scale")
seriestype := :path
subplot := i
yscale := scale
end
end
end
@shorthands yscaleseries
yscaleseries! (generic function with 1 method)
That looks a little nicer than the plot recipe version as well. Let's try to plot.
yscaleseries((1:10).^2)
MethodError: Cannot `convert` an object of type Int64 to an object of type Plots.Subplot{Plots.GRBackend}
Closest candidates are:
convert(::Type{T}, !Matched::T) where T at essentials.jl:168
Plots.Subplot{Plots.GRBackend}(::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any, !Matched::Any) where T<:RecipesBase.AbstractBackend at /home/daniel/.julia/packages/Plots/rNwM4/src/types.jl:88
That is because the plot and subplots have already been built before the series recipe is applied.
For everything that modifies plot-wide attributes plot recipes have to be used, otherwise series recipes are recommended.