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, ...; ...)
Tip

@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
Compat

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].

Compat

With RecipesBase 1.0 type recipes of the form

@recipe function f(::Type{T}, val::T) where T <: AbstractArray{MyType}

for AbstractArrays of custom types are supported too.

Info

User recipes and type recipes must return either

  • an AbstractArray{<:V} where V 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; ...)
Tip

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...)
Warning

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...)

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. DateTimes and Chars 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.

Info

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.

Tip

For everything that modifies plot-wide attributes plot recipes have to be used, otherwise series recipes are recommended.