RecipesBase: How to define the same recipe for different packages?

I have packages A, B, C, where both B and C depend on A. In order to make some function f available to both B and C at the same time, I first define it in A and then overload it in B and C with some local methods, e.g.

module A
  function f end
  export f
end

module B
  using A
  ...
  A.f(x::SomeTypeInB) = ...
end

module C
  using A
  ...
  A.f(x::SomeTypeInC) = ...
end

So that I can then write using A, B, C and use f, dispatching without issues.

Can I use a similar approach to define recipe functions (using RecipesBase) common to both B and C? I’ve tried without success.

Additional info

For reference, atm I define recipes via

@userplot COOLPLOT
@recipe function f(h::COOLPLOT)
  if h.args[1] isa SomeTypeInPackage
      ...
    else
      error("Got $(typeof(h.args[1])) instead of SomeTypeInPackage.")
  end
end

which exports coolplot and coolplot! automatically.

You can just define a user recipeRecipes · Plots on the Union of your types (or their abstract supertype if they have one).

Although I want these recipes to share the same name, the actual codes for plotting SomeTypeInB and SomeTypeInC are quite different. I need to be able to differentiate between these types inside B and C respectively.

Do they need to have a particular name? Maybe can you be a litte more concrete about how the plots should look and what the types are?

I’d like to define a common recipe convergence (and convergence!) that shows the standard converge plot (error vs iteration) of a fixed-point problem using some custom algorithms defined in B and C.

In other words, I have SolutionUsingAlgorithmB and SolutionUsingAlgorithmC (which in practice are two different structs of common supertype <: AbstractSolution, where AbstractSolution is defined in A), defined in each respective package.

I would like to define convergence(solution::SolutionUsingAlgorithmB) and convergence(solution::SolutionUsingAlgorithmC) using RecipesBase alone. In practice, each method of convergence must be different (because SolutionUsingAlgorithmB and SolutionUsingAlgorithmC are very different), so I cannot simply make use of convergence(solution::AbstractSolution).

Ah, I see. I think. It’s a little tricky because Plots already uses dispatch to have the recipe names work.
The easiest is probably to have a function inside the recipe that generates the data to be plotted from your solution, and have that function dispatch on the type of solution. So that the recipe itself only sets up what is visible to the user plotting (which should be the same).

2 Likes

Yes, that would do it! However, using this approach, how do I go about passing different Plots’ stylistic arguments, e.g. markersize --> 4, from such a function that lives inside the recipe to the recipe itself?

EDIT: Looking at this beautiful daschw’s post, I realised that it is simply a matter of pushing to plotattributes as follows:

module A
  ...
  function _convergence end
  @userplot CONVERGENCE
  @recipe f(h::CONVERGENCE) = _convergence(h.args[1], plotattributes)
end

module B
  using A
  ...
  function A._convergence(solution::SolutionUsingAlgorithmB, plotattributes)
    push!(plotattributes, :framestyle => :box, :gridalpha => 0.2, ...)
    ...
    return solution.plottablefield
  end

and similarly for C.

For future reference, based on @mkborregaard’s comment, I found a better solution that involves wrapping SolutionUsingAlgorithmB and SolutionUsingAlgorithmC inside a constructor _Convergence defined in A:

module A
  ...
  struct _Convergence{solution_T}
    solution::solution_T
  end
  @userplot CONVERGENCE
  @recipe function f(h::CONVERGENCE)
    return _Convergence(h.args[1])
  end
end

module B
  using A
  ...
  @recipe function f(wrappedobject::NSDEBase._Convergence{<:SolutionUsingAlgorithmB})
    solution = wrappedobject.solution
    ...
  end
end

and similarly for C. This solution has the non-trivial advantage of allowing for the usage of the usual RecipesBase tricks (for loops of @series, etc) inside the relevant recipes defined in B and C, which otherwise couldn’t be used (that easily) with the previous approach based on _convergence.

My idea was just something along the lines of

using RecipesBase

create_xy(x::A_type) = ...; x, y
create_xy(x::B_type) = ...; x, y

@userplot CoolPlot

@recipe function f(h::CoolPlot)
    markersize --> 4
    seriestype := :scatter
    create_xy(h.args[1])
end

And that’s fine, but:

  1. Oftentimes, you want to use different styling options depending on A_type and B_type (which, admittedly you could do with your “function” approach by passing around plotattributes).
  2. Doing nested for loops with @series is possible if create_xy are recipes themselves.