RecipesBase: Reuse a @series and use/work with provided limits

I am starting to really appreciate how RecipesBase works, now that I slowly understand the scheme. Maybe for now this is limited to user recipes, but nevertheless, really neat!

Currently I have one point left, where I am reproducing code, that I would like to only do once. Consider the following example, which I tried to get minimal

using Plots, RecipesBase, Colors
gr()

struct myType
d::Int
s::Int
end

@recipe function f(m::myType)
    @series begin
        seriestype := surface
        fillcolor := RGBA(0.0, 0.5, 1.0, 1.0)
        legend := :none
        return [-m.d,-m.d,m.d,m.d,-m.d], [-m.d,m.d,m.d,-m.d,-m.d], [0,0,0,0,0]
    end
end

@recipe function f(m::myType, pts::AbstractVector{T}) where {T}
    @series begin
        seriestype := surface
        fillcolor := RGBA(0.0, 0.5, 1.0, 1.0)
        legend := :none
        return [-m.d,-m.d,m.d,m.d,-m.d], [-m.d,m.d,m.d,-m.d,-m.d], [0,0,0,0,0]
    end
    return [p[1] for p ∈ pts], [p[2] for p ∈ pts], [p[3] for p ∈ pts]
end

D = myType(3,2)
scene1plot(D)
scene2 = plot(D, [[1,1,1],[2,2,2]])

Then scene 2 looks like
a

Which is (up to my laziness to remove the colormap) what I would like to have.

  1. As you can see the inner @series appears twice in the code, or in other words I would like to just use the first recipe as part of the second. Could that be done?

  2. I would like to avoid redrawing this first recipe in plot! cases, i.e. only draw the surface once for the first appearance of such a user recipe. Is that possible?

  3. I would like to make the inner @series depend on xlim/ylim (i.e. make the surface similarly large) can I somehow check what the user provided for these values within the recipe?

For context: The first type introduces a context for the data pts in my application see for example Hyperbolic space · Manifolds.jl for a first sketch of what I aim to do.

If you want to avoid code duplication you could write the above as one recipe like this:

using Plots, RecipesBase, Colors
gr()

struct myType
d::Int
s::Int
end

@recipe function f(m::myType, pts::Union{AbstractVector{T}, Nothing} = nothing) where {T}
    @series begin
        seriestype := surface
        fillcolor := RGBA(0.0, 0.5, 1.0, 1.0)
        legend := :none
        return [-m.d,-m.d,m.d,m.d,-m.d], [-m.d,m.d,m.d,-m.d,-m.d], [0,0,0,0,0]
    end
    if pts !== nothing
        return [p[1] for p ∈ pts], [p[2] for p ∈ pts], [p[3] for p ∈ pts]
    end
end

D = myType(3,2)
scene1plot(D)
scene2 = plot(D, [[1,1,1],[2,2,2]])

It is also possible to chain recipes via dispatch, like: create a recipe for MyType write a recipe for MyType2 which returns a MyType object. I don’t quite see, if it helps in this case though.

1 Like

That’s a nice way, though my concrete example even does that one step further (having pts and vecs as possibly-nothing-vallues. But sure, that is a neat way that should work.

Edit: Reusing the recipe for myType within the myType, pts signature would still be neat I think, i.e. like with @series to trigger another user recipe within a recipe. Not with two types but this two signatures.
This is because I just noticed that my pts change their meaning (appeatance to be precise) depending on whether vecs are provided with the recipes for
myType,pts (which plots points) and myType,pts,vecs (which more like quivers, but in a quite specific way).

Edit2: I got it to work :slight_smile: So my first point is solved. Thanks!