Understanding recipes in Plots.jl

I am trying to define a recipe for my package that generates a plot out of an image. Consider the simplified example below:

# somewhere in my package...

using RecipesBase

@recipe function plotfoo(A::AbstractArray)
  xs = 1:length(A)
  ys = A[:]

  xs, ys
end

# someone using my package...

using MyPackage
using Plots

plotfoo(eye(3))

I don’t understand the recipes machinery yet, it is pretty mysterious. How to make this function plotfoo to just plot the flattened array?

Check this page:

https://juliaplots.github.io/recipes/

You want to use a plot recipe, not a type recipe. You used a type recipe syntax. Type recipes are converting type into generic data (arrays). A conversion of AbstractArray is too crazy. An example of a type recipe is the solution type in DifferentialEquations.jl, which takes the solution and the time series out of a more complex type and passes only that information along for the next part of the plotting timeline. This means that

plot(solution::ODESolution)

gets transformed into the data arrays for the next stage of the plots. Notice that’s only data-based, and has nothing to do with the actual plotting functionality, so it’ll let you do things like

scatter(solution::ODESolution)

since all we have done is tell plots how to interpret solution as arrays.

You’re not looking to transform data to data. Rather you’re looking you’re looking for something which makes specific transformation on data from generic data (arrays): some kind of named plot. As it’s stated in the docs, you use the syntax:

@recipe function f(::Type{Val{:myplotrecipename}}, plt::Plot; ...) 
 
end

Notice that the function name doesn’t matter: it’s the Type{Val{:Name}} that sets the name. You want plotfoo to take a matrix, and turn it into a specific kind of data (flatten the array), which you then finally plot with some series (say a line series).

I hope this explains the pipeline a bit.

3 Likes

The key thing I don’t think you get: the recipe macro does not create a
function plotfoo at all (in fact that name is ignored and thrown away). You
are adding a new definition of apply_recipe with your specific argument
types.

The end user will still call plot, scatter, or whatever else. If the types
match during the Plots processing pipe, then your method will be called to
convert data/attributes into something more low-level.

This is also why you shouldn’t define a recipe as generic as AbstractArray.
You’re likely overwriting core functionality.

Hope this helps!

2 Likes

Thank you @ChrisRackauckas, I am starting to understand the process. I tried to adapt the example following your suggestions but it didn’t work:

# somewhere in my package...

using RecipesBase

@recipe function f(::Type{Val{:plotfoo}}, A)
  xs = 1:length(A)
  ys = A[:]

  xs, ys
end

# someone using my package...

using Plots

plotfoo(eye(3))

Can you please point where the issue is in the snippet above?

  1. I agree that the plot recipes portion of Plots docs could be made clearer. Especially as some of the code in that section (e.g. @userplot) requires Plots, not just RecipesBase.
  2. The key thing is your choice of recipe type. You may not want a “plot recipe” (which you are implementing there, but I believe that requires Plots). I cannot see from your code what you want the recipe to do. Could you describe your intended use exactly?
2 Likes

@mkborregaard I believe the entire documentation of Plots.jl needs a review to be honest. What I have in mind for this package is a plotting function that takes an image as input, peforms some fancy operation on the image, and returns the coordinates xs, ys to be plotted as a time series. Any of the recipe types fit this description?

Can you give more details than that? And open and issue, or ideally a PR? The docs are really easy to change, but a vague “I don’t like it” never helps. Generally, the docs are really easy for anyone to use because you can essentially just copy/paste from the examples and tweak using the attributes page. I think the recipe docs can be improved, mostly by a better explanation of the pipelines / the differences between types of recipes, but I myself need to think of a good way to describe it before I could submit a PR.

Did you look at the docs? Your signature,

@recipe function f(::Type{Val{:plotfoo}}, A)

is for series recipes. If you scroll down to the part on series recipes, it shows that you need to create a new series if you’re doing that. But I don’t think you need a series recipe here? Could you try what I suggested instead, the plot recipe? The signature was:

@recipe function f(::Type{Val{:myplotrecipename}}, plt::Plot; ...) 
 
end

which has the plt::Plot in there. This is what you left out.

I think that last part is unnecessary. Why would you constrain “to be plotted as a time series”? plotfoo(eye(3)) would plot “as a timeseries”, but the interesting thing about it being a plot recipe instead of a series recipe is that you’re thinking about a data transformation, instead of a type of plot. This means that plotfoo!(eye(3),series=:scatter) to do the same transformation, but add new data as a scatter plot on top of a previous plot makes sense.

1 Like

Thank you @ChrisRackauckas, I will try add the plt::Plot argument to the argument list and see what happens, even though I don’t understand what is happening under the hood.

In terms of documentation, I feel it was written by the authors to the co-authors. As someone coming from outside, a normal user, I can’t catch up easily, which is paradoxical since one of the goals of Plots.jl is to “just work” out of the box with no need to memorize syntax nor function prototypes. I think a lot of the attention in the docs has been put into the internal machinery and less on the available customization options, makes sense?

Anyways, I wish I could understand the API more to document it myself, but this is a chicken or egg problem: I don’t understand enough to document, there is no documentation to understand.

1 Like

Oh, this is just a standard Julia design. Instead of writing two different implementations when you have a mutating function, you just make the non-mutating version pass to the mutating version.

f!(y,x) # does something mutating to x
function f(x) 
  y = similar(x)
  f!(y,x)
  y
end

This is a very common structure in Julia because mutating allows things to be faster, and sometimes allows more functionality. So for plot recipes, the implementation is really just plot!(dispatch_type,plot, args...) which adds a new dispatch_type recipe to an existing plot. This style of using dispatch on types to add extend functionality to a common function call is described here:

So then the out-of-place plot silently adds in the current plot and calls plot! and continues. So what you’re really defining is a new plot! function with a new type, where the first argument is a type which controls its dispatch. Make sense?

But this page describes all of the customization options, does it not?

https://juliaplots.github.io/attributes/

But I think what you’re looking for is more of an “Introduction to Plots”. I think a package manual should go through all of the details and explain everything it can do, which is what the docs currently do. A separate thing would be a quicker “Dive Into Plots”, and someone should write something like that.

The easiest way is to ask a question, and once it makes sense, make a PR for the docs.

3 Likes

I’d like to help you write the recipe you are after, hopefully making something about recipes clearer in the process. On rereading your quote here, though, I am sure you will agree that you are not giving me a lot of information to work with.

Generally speaking, recipes solve several different problems. (I will try to write my best understanding below, but it may be inaccurate @tbreloff may want to comment and say some of this is wrong).

The biggest advantage of recipes is the way they adress one of the trickiest issues of Julias package ecosystem architecture. Because there are many different plotting packages, anyone who writes a package that needs plotting functionality is faced with the problem of choosing the plotting package: e.g. OpenStreetMaps.jl uses Winston, PhyloNetworks uses Gadfly etc, meaning that 1) end users need all of these plotting packages, 2) package maintainers get tied to a big plotting dependency such as Gadfly, and 3) plot elements from different packages don’t communicate, e.g. it is hard to overlap other map elements on an OpenStreetMap plot because the plot is coded in Winston. The idea of Plots recipes is that any package can take a small dependency (on RecipesBase, a single macro), and then define data in a way that can be plotted by any package in the Julia ecosystem, by translating it through Plots. It requires only that someone cares enough to write a Plots ‘backend’ for the plotting package, which has been done for most high-quality plotting packages in Julia.

This function is filled by ‘Type recipes’, which are simple transformations of any custom type to data types (arrays and such) that has predefined behaviour in plots. E.g. there is a type recipe that takes a Shapefile polygon and swaps it with a standard polygon Shape as it is passed to Plots. It is also filled by ‘User recipes’, that allow more flexibility. A ‘Series recipe’ defines a particular way (e.g. a “scatter” of points, or a “:line”) of plotting something. All of these can define default values of Plots attributes as well.

Another function of recipes is to make it easy (once you know the syntax) to define more complex plots, that combines plot elements, layouts etc (see e.g. MarginalHist in the docs). These are “Plot recipes”. These recipes leverage the entire Plots machinery, and as such you will generally need to depend on Plots to use this functionality.

The key thing is to choose the right recipe type, and then follow the syntax exactly! Based on your description of the problem, I cannot say whether you need a Plot recipe (and have to depend on Plots) or could just do a user recipe.

1 Like

But for instance, let us say you have a series of satellitte images taken over time, and you need to plot the amount of greenness in the images as a function of time. You could define a specific type in your package to take the images, do the transform and hold the information:

type GreenDev
       times::Vector{DateTime}
       greens::Vector{Float64}
end

function GreenDev(imgs)
       ....
       times, greens = #fancy operation
       GreenDev(times, greens)
end

Then you can define a simple user recipe for this in your package:

using RecipesBase
@recipe f(gd::GreenDev) = gd.times, gd.greens

simply telling the plotting package to plot it as a plot where gd.times is the x axis (DateTime objects are handled automatically by Plots), gd.greens the y.
Your user might expect the plot to be a line graph with a green color, so you could set default values for this, instead definining your recipe as

using RecipesBase
@recipe function f(gd::GreenDev)
    seriestype --> :path
    seriescolor --> :green 
    gd.times, gd.greens
end 
6 Likes

Creating your own recipe definitely falls into the “advanced topics” category. You need to either 1) have a good understanding of macros, dispatch, and more, or 2) “trust in the magic”. In this case, I feel like you don’t fall into either category, so you’re frustrated. Using a recipe is simple… just call plot(something).

Understanding the Plots machinery/internals should be daunting… it’s incredibly intricate and complicated. But for most purposes you shouldn’t need to understand how it works, just the rules that get you from point A to B.

The best way to fix the documentation is to ask in the gitter about specific things you don’t understand, and then write up your understanding afterwards and submit a PR (to PlotDocs.jl). I can’t possibly know what you don’t understand, and I don’t have the time to spend on rewriting documentation anyways.

1 Like

I don’t think writing recipes is necessarily very complex (although of course it can be). In most cases it should be well within the ability of julia package authors, and I’d encourage all package authors to do so.

Frankly I don’t see any better solution to the issue of providing plotting capabilities in the context of many different plotting packages, than providing a succinct data format for passing plot information. I am sure @tbreloff (as the inventor of this) must agree :slight_smile:

With regards to the recipes documentation, I would hope to find the time to make a small PR on PlotDocs with some suggestions.

3 Likes

I have a recipes question and decided not to open a new topic. Basically, I have a datatype that just wraps quadrature nodes and weights, and would like a plotting recipe (for teaching). This is what I have so far:

using RecipesBase
using ValidatedNumerics # for Interval

immutable Quadrature{T <: Real}
    domain::Interval{T}
    nodes::Vector{T}
    weights::Vector{T}
end

@recipe function f{T <: Quadrature}(q::T)
    seriestype --> :scatter
    # ylim --> (0, maximum(q.weights))
    q.nodes, q.weights
end

Questions:

  1. Note the commented out line for ylim: what I would like is to have the axis from 0 to the largest weight, but a bit of a margin. If I don’t specify, Plots adds a bit of space, if I specify, the largest marker is cut in half. How can/should I hook into whatever mechanism Plots uses to expand automatically determined ranges? I found expand_extrema in the source, unsure how to use it.
  2. I would like to draw a semi-transparent bar or something along the x axis on the range
    a. (q.domain.lo, q.domain.hi) if isfinite(q.domain),
    b. from edge to edge (the whole x range) if !isfinite(q.domain).

Untested, but I would expect something like this to work:

@recipe function f(q::Quadrature)
    # add a box?
    @series begin
        xmin, xmax = isfinite(q.domain) ? (q.domain.lo, q.domain.hi) : extrema(q.nodes)
        ymin, ymax = 0, 0.1maximum(q.weights)
        seriestype := :shape
        fillalpha --> 0.3
        primary --> false
        [xmin, xmax, xmax, xmin], [ymin, ymin, ymax, ymax] 
    end

    seriestype --> :scatter
    ylim --> (0, maximum(q.weights)*1.05)
    q.nodes, q.weights
end
2 Likes

@tbreloff is there any way to fix the color of the :shape series to be the same color of the :scatter series? I am almost done with my recipe. RecipesBase is a super cool idea, thanks.

I added primary --> false to the series block, which should “fuse” the 2 series into one for the purposes of assigning colors, etc. To override both colors, you should be able to just do: plot(q, color=:blue)

2 Likes

@tbreloff one more question before I push the recipe to GitHub, how can we query the automatic margin (let’s say ymin) in the plot when we don’t specify it explicitly?

After the plot is built, you can query a Plot/Subplot like:

ylims()
ylims(plt)
ylims(sp)

Defined here: https://github.com/JuliaPlots/Plots.jl/blob/47ee0a4c9778568a433b46f66508f0b49a17c7f2/src/utils.jl#L487-L498

If you want to do this in the recipe, it’s not ideal because you need a Plots dependency, and you probably also need to make this into a “plot recipe” instead. So try to avoid it if you can just compute it yourself.

1 Like

Is there a way to specify a custom layout in a recipe using only RecipesBase (i.e, not creating a dependency on Plots)?

More specifically, I would like to do this in a @recipe without relying on Plots:

layout := @layout [a{0.1h}; grid(2,2)]