Skip to content

Conversation

@daschw
Copy link
Member

@daschw daschw commented Mar 25, 2020

Temporarily store the current axis letter symbol in plotattributes, so type recipes can use this information.

An example where this could be useful:
Consider a Measurement type with value and uncertainty

struct Measurement
    val::Float64
    err::Float64
end

value(m::Measurement) = m.val
uncertainty(m::Measurement) = m.err

Now we can define a vector type recipe.

@recipe function f(::Type{T}, m::T) where T <: AbstractArray{<:Measurement}
    error_sym = Symbol(plotattributes[:letter], :error)
    plotattributes[error_sym] = uncertainty.(m)
    value.(m)
end

This is probably more composible and convenient for package authors than defining user recipes for all combinations of signatures, as it is done in Measurements.jl right now.

ms1 = Measurement.(10rand(10), rand(10))
ms2 = Measurement.(10rand(10), rand(10))
scatter(ms1)

yerror

scatter(ms1, rand(10))

xerror

scatter(ms1, ms2)

xyerror

@briochemc
Copy link
Collaborator

briochemc commented Apr 6, 2020

I think there is an issue if your type is a subtype of Number, then the recipe is ignored. MWE:

# copy-paste of issue MWE
struct Measurement1
    val::Float64
    err::Float64
end
value(m::Measurement1) = m.val
uncertainty(m::Measurement1) = m.err
@recipe function f(::Type{T}, m::T) where T <: AbstractArray{<:Measurement1}
    error_sym = Symbol(plotattributes[:letter], :error)
    plotattributes[error_sym] = uncertainty.(m)
    value.(m)
end
# same but as a subtype of Number
struct Measurement2 <: Number # is the only thing I added here
    val::Float64
    err::Float64
end
value(m::Measurement2) = m.val
uncertainty(m::Measurement2) = m.err
@recipe function f(::Type{T}, m::T) where T <: AbstractArray{<:Measurement2}
    error_sym = Symbol(plotattributes[:letter], :error)
    plotattributes[error_sym] = uncertainty.(m)
    value.(m)
end
# comparison
ms1 = Measurement1.(10rand(10), rand(10))
ms2 = Measurement2.(10rand(10), rand(10))
scatter(ms1) # works
scatter(ms2) # does not work

(FYI, I was trying to apply this new functionality to UnitfulRecipes, but it would not work because Quantity <: Number.)

@daschw
Copy link
Member Author

daschw commented Apr 6, 2020

Thanks for reporting! I will see if I can get this to work with subtypes of Number too.

@daschw
Copy link
Member Author

daschw commented Apr 6, 2020

@briochemc If I allow custom type recipes for all T<:Number we would have a significant slowdown for standard plots with Floats and Ints. Would it make sense too allow for Number but disallow for Real?

@briochemc
Copy link
Collaborator

That would fix my issue, yes!

I don't know about other packages though. Maybe an issue for packages that subtype Real? Anyway, do you mind explaining why the slowdown, just for my education?

@daschw
Copy link
Member Author

daschw commented Apr 6, 2020

If we are applying a type recipe we are doing preprocessing and postprocessing of axis arguments, like setting plotattributes[:letter] and mapping general axis arguments like guide to the respective letter.
Furthermore, if the vector type recipe did not change the type (it does not for Reals), we apply a type recipe elementwise.
For Numbers we skip all that and don't apply a type recipe currently.

@briochemc
Copy link
Collaborator

I'm not sure I got it but thanks for trying 😅

But was this (skipping for Numbers) the behavior before the v1 update? I seem to recall trying the new type recipe :letter functionality just before updating to v1 and it actually applied the recipe (and threw an error because :letter was not defined in that version).

@daschw
Copy link
Member Author

daschw commented Apr 6, 2020

That would fix my issue, yes!

Should be fixed in Plots 1.0.1

But was this (skipping for Numbers) the behavior before the v1 update?

No, apparently recipes were only skipped for vectors of Union{Integer, AbstractFloat}. I changed internally some Union{...} expressions to Plots.DataPoint and missed this unexpected effect. Thanks a lot again for reporting!

@briochemc
Copy link
Collaborator

Another thing I just noticed is that the type recipe for z is skipped in the case of contour(x,y,z). I think in this case this is because z is converted to a Surface in the pipeline before the z::AbstractArray{<:MyType} recipe can be applied, such that it is eventually skipped and throws if z is not a valid type at the end. Is there any way around this? (I have to go now but I can try to make a MWE tomorrow if this is not enough.)

@daschw
Copy link
Member Author

daschw commented Apr 6, 2020

Thanks, should be fixed in Plots 1.0.3. It would be great if you could share examples if you have a new type recipe ready, so we can add some tests.

@briochemc
Copy link
Collaborator

Great!

Well I'm working on rewriting UnitfulRecipes in this branch: https://github.com/jw3126/UnitfulRecipes.jl/tree/AxisAware. Will work on it today and see if I can pass all the tests :)

@briochemc
Copy link
Collaborator

briochemc commented Apr 7, 2020

Actually, after checking, 1.0.3 did not fix it for me. Here is a MWE:

using Plots, RecipesBase
struct Measurement <: Number
    val::Float64
    err::Float64
end
value(m::Measurement) = m.val
uncertainty(m::Measurement) = m.err
# now just extract the value and print which axis is being treated
@recipe function f(::Type{T}, m::T) where T <: AbstractArray{<:Measurement}
    println(plotattributes[:letter])
    value.(m)
end
x = Measurement.(10sort(rand(10)), rand(10))
y = Measurement.(10sort(rand(10)), rand(10))
z = Measurement.(10rand(10,10), rand(10,10))
contour(x, y, z) 

where you can see that z is not captured by the recipe.

@briochemc
Copy link
Collaborator

Additional note/MWE: I could capture it via a recipe like

const AVec = AbstractVector
const AMat{T} = AbstractArray{T,2} where T
@recipe function f(x::AVec, y::AVec, z::AMat{T}) where T <: Quantity
    println(:z)
    x, y, value.(z)
end

But this seems to go against what this PR is all about, right?

@briochemc
Copy link
Collaborator

Not sure if this makes sense but I would also like to ask. Is it possible to have plotattributes[:letter] == :c for the data of series types that pertain to the color?

Maybe this could help for recipe writers? E.g., in the case of contour or heatmap, things remain 2D but with a color axis (hence :c). Then we could use :caxis, :clims, :clabel, etc., in a consistent way?

Sorry if this is completely missing some point 😅

@daschw
Copy link
Member Author

daschw commented Apr 7, 2020

Thanks for the example above, I will investigate, where the issue is.

Not sure if this makes sense but I would also like to ask. Is it possible to have plotattributes[:letter] == :c for the data of series types that pertain to the color?

Interesting idea, where would surface be here, :z or :c? Maybe we could instead provide plotattributes[l:is_surface] (or :like_surface) in type recipes, which would be true for seriestype in [:contour, :contourf, :contour3d, :heatmap, :surface, :wireframe, :image]. In these case you would not want to set xerror and yerror either, right?

@daschw
Copy link
Member Author

daschw commented Apr 7, 2020

or you could check in the type recipe for get(plotattributes, :seriestype, :path) in [:contour, :contourf, :contour3d, :heatmap, :surface, :wireframe, :image]

@daschw
Copy link
Member Author

daschw commented Apr 7, 2020

In both cases there would be an issue if someone defines e.g. a series recipe :myheatmap that sets the seriestype to :heatmap which happens after type recipe processing.

One way around this would be defining RecipesBase.like_surface(st) = false, and RecipesBase.like_surface(::Type{Val{st}) = true for the list above. Then users defining a :myheatmap series recipe could set RecipesBase.like_surface(::Type{Val{:myheatmap}) = true, and in type recipes you could check for RecipesBase.like_surface(Val{get(plotattributes, :seriestype, :path)})

@briochemc
Copy link
Collaborator

briochemc commented Apr 7, 2020

I stumble on another tiny bug FYI whereby vline! gets the wrong :letter. MWE (check the output in the REPL of the last line):

using Plots, RecipesBase, Statistics
struct Measurement <: Number
    val::Float64
    err::Float64
end
value(m::Measurement) = m.val
uncertainty(m::Measurement) = m.err
# now just extract the value and print which axis is being treated
@recipe function f(::Type{T}, m::T) where T <: AbstractArray{<:Measurement}
    println(plotattributes[:letter])
    value.(m)
end
x = Measurement.(rand(5), rand(5))
y = Measurement.(rand(5), rand(5))
plot(x, y)
hline!(y)
vline!(x) # the recipe thinks its :letter is :y instead of :x

EDIT: opened this as a separate Plots.jl issue

@daschw daschw deleted the typerecipes branch June 30, 2020 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants