diff --git a/docs/make.jl b/docs/make.jl index cd1b0a8462b..6d9c70fe893 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -55,15 +55,16 @@ unique!(galleries_assets) ################## # `UnitfulRecipes` -unitfulrecipes_src = joinpath(@__DIR__, "src/unitfulrecipes") -notebooks = joinpath(unitfulrecipes_src, "notebooks") +src_unitfulrecipes = "src/UnitfulRecipes" +unitfulrecipes = joinpath(@__DIR__, src_unitfulrecipes) +notebooks = joinpath(unitfulrecipes, "notebooks") execute = true # set to true for executing notebooks and documenter! nb = false # set to true to generate the notebooks -for (root, _, files) in walkdir(unitfulrecipes_src), file in files +for (root, _, files) in walkdir(unitfulrecipes), file in files last(splitext(file)) == ".jl" || continue ipath = joinpath(root, file) - opath = replace(ipath, "src/unitfulrecipes" => "src/generated") |> splitdir |> first + opath = replace(ipath, src_unitfulrecipes => "src/generated") |> splitdir |> first Literate.markdown(ipath, opath, documenter = execute) nb && Literate.notebook(ipath, notebooks, execute = execute) end @@ -85,7 +86,37 @@ const PAGES = Any[ "Subplot Attributes" => "generated/attributes_subplot.md", "Axis Attributes" => "generated/attributes_axis.md", "Layouts" => "layouts.md", - "Recipes" => "recipes.md", + "Recipes" => [ + "Overview" => "recipes.md", + "RecipesBase" => [ + "Home" => "RecipesBase/index.md", + "Recipes Syntax" => "RecipesBase/syntax.md", + "Recipes Types" => "RecipesBase/types.md", + "Internals" => "RecipesBase/internals.md", + "Public API" => "RecipesBase/api.md", + ], + "RecipesPipeline" => [ + "Home" => "RecipesPipeline/index.md", + "Developer manual" => [ + "Public API" => "RecipesPipeline/api.md", + "Recipes" => "RecipesPipeline/recipes.md", + ], + "Reference" => "RecipesPipeline/reference.md", + # "Source code" => joinpath.("generated", [ + # "RecipesPipeline.md", + # "api.md", + # "user_recipe.md", + # "plot_recipe.md", + # "type_recipe.md", + # "series_recipe.md", + # "group.md", + # "recipes.md", + # "series.md", + # "group.md", + # "utils.md" + # ]) + ], + ], "Colors" => "colors.md", "ColorSchemes" => "generated/colorschemes.md", "Animations" => "animations.md", @@ -98,12 +129,12 @@ const PAGES = Any[ "Ecosystem" => [ "StatsPlots" => "generated/statsplots.md", "GraphRecipes" => [ - "Introduction" => "graphrecipes/introduction.md", - "Examples" => "graphrecipes/examples.md", + "Introduction" => "GraphRecipes/introduction.md", + "Examples" => "GraphRecipes/examples.md", "Attributes" => "generated/graph_attributes.md", ], "UnitfulRecipes" => [ - "Introduction" => "unitfulrecipes/unitfulrecipes.md", + "Introduction" => "UnitfulRecipes/unitfulrecipes.md", "Examples" => [ "Simple" => "generated/unitfulrecipes_examples.md", "Plots" => "generated/unitfulrecipes_plots.md", diff --git a/docs/src/graphrecipes/examples.md b/docs/src/GraphRecipes/examples.md similarity index 100% rename from docs/src/graphrecipes/examples.md rename to docs/src/GraphRecipes/examples.md diff --git a/docs/src/graphrecipes/introduction.md b/docs/src/GraphRecipes/introduction.md similarity index 100% rename from docs/src/graphrecipes/introduction.md rename to docs/src/GraphRecipes/introduction.md diff --git a/docs/src/RecipesBase/api.md b/docs/src/RecipesBase/api.md new file mode 100644 index 00000000000..8d25af3066b --- /dev/null +++ b/docs/src/RecipesBase/api.md @@ -0,0 +1,3 @@ +```@autodocs +Modules = [RecipesBase] +``` diff --git a/docs/src/RecipesBase/index.md b/docs/src/RecipesBase/index.md new file mode 100644 index 00000000000..97f1320aca2 --- /dev/null +++ b/docs/src/RecipesBase/index.md @@ -0,0 +1,15 @@ +# RecipesBase.jl + +**Author: Thomas Breloff (@tbreloff)** + +RecipesBase is a lightweight Package without dependencies that allows to define custom visualizations with the [`@recipe`](@ref) macro. + +Package developers and users can define recipes to tell [Plots.jl](https://github.com/JuliaPlots/Plots.jl) how to plot custom types without depending on it. +Furthermore, recipes can be used for complex visualizations and new series types. +Plots, for example, uses recipes internally to define histograms or bar plots. +[StatsPlots.jl](https://github.com/JuliaPlots/StatsPlots.jl) and [GraphRecipes.jl](https://github.com/JuliaPlots/GraphRecipes.jl) extend Plots functionality for statistical plotting and visualization of graphs. + +RecipesBase exports the [`@recipe`](@ref) macro which provides a nice syntax for defining plot recipes. +Under the hood [`@recipe`](@ref) defines a new method for `RecipesBase.apply_recipe` which is called recursively in Plots at different stages of the argument processing pipeline. +This way other packages can communicate with Plots, i.e. define custom plotting recipes, only depending on RecipesBase. +Furthermore, the convenience macros [`@series`](@ref), [`@userplot`](@ref) and [`@shorthands`](@ref) are exported by RecipesBase. diff --git a/docs/src/RecipesBase/internals.md b/docs/src/RecipesBase/internals.md new file mode 100644 index 00000000000..057d3660738 --- /dev/null +++ b/docs/src/RecipesBase/internals.md @@ -0,0 +1,149 @@ +## RecipesBase + +The [`@recipe`](@ref) macro defines a new method for `RecipesBase.apply_recipe`. +```julia +@recipe function f(args...; kwargs...) +``` +defines +```julia +RecipesBase.apply_recipe(plotattributes, args...; kwargs...) +``` +returning a `Vector{RecipeData}` where `RecipeData` holds the `plotattributes` Dict and the arguments returned in [`@recipe`](@ref) or in [`@series`](@ref). +```julia +struct RecipeData + plotattributes::AbstractDict{Symbol,Any} + args::Tuple +end +``` +This function sets and overwrites entries in `plotattributes` and possibly adds new series. +- `attr --> val` translates to `haskey(plotattributes, :attr) || plotattributes[:attr] = val` +- `attr := val` sets `plotattributes[:attr] = val`. +- [`@series`](@ref) allows to add new series within [`@recipe`](@ref). It copies `plotattributes` from [`@recipe`](@ref), applies the replacements defined in its code block and returns corresponding new `RecipeData` object. + !!! info + [`@series`](@ref) have to be defined as a code block with `begin` and `end` statements. + ```julia + @series begin + ... + end + ``` + +So `RecipesBase.apply_recipe(plotattributes, args...; kwargs...)` returns a `Vector{RecipeData}`. +Plots can then recursively apply it again on the `plotattributes` and `args` of the elements of this vector, dispatching on a different signature. + + +## Plots + +The standard plotting commands +```julia +plot(args...; plotattributes...) +plot!(args...; plotattributes...) +``` +and shorthands like `scatter` or `bar` call the core internal plotting function `Plots._plot!`. +```julia +Plots._plot!(plt::Plot, plotattributes::AbstractDict{Symbol, Any}, args::Tuple) +``` + +In the following we will go through the major steps of the preprocessing pipeline implemented in `Plots._plot!`. + +#### Preprocess `plotattributes` +Before `Plots._plot!` is called and after each recipe is applied, `preprocessArgs!` preprocesses the `plotattributes` Dict. +It replaces aliases, expands magic arguments, and converts some attribute types. +- `lc = nothing` is replaced by `linecolor = RGBA(0, 0, 0, 0)`. +- `marker = (:red, :circle, 8)` expands to `markercolor = :red`, `markershape = :circle` and `markersize = 8`. + +#### Process User Recipes + +In the first step, `_process_userrecipe` is called. + +```julia +kw_list = _process_userrecipes(plt, plotattributes, args) +``` +It converts the user-provided `plotattributes` to a vector of `RecipeData`. +It recursively applies `RecipesBase.apply_recipe` on the fields of the first element of the `RecipeData` vector and prepends the resulting `RecipeData` vector to it. +If the `args` of an element are empty, it extracts `plotattributes` and adds it to a Vector of Dicts `kw_list`. +When all `RecipeData` elements are fully processed, `kw_list` is returned. + +#### Process Type Recipes + +After user recipes are processed, at some point in the recursion above args is of the form `(y, )`, `(x, y)` or `(x, y, z)`. +Plots defines recipes for these signatures. +The two argument version, for example, looks like this. + +```julia +@recipe function f(x, y) + did_replace = false + newx = _apply_type_recipe(plotattributes, x) + x === newx || (did_replace = true) + newy = _apply_type_recipe(plotattributes, y) + y === newy || (did_replace = true) + if did_replace + newx, newy + else + SliceIt, x, y, nothing + end +end +``` + +It recursively calls `_apply_type_recipe` on each argument until none of the arguments is replaced. +`_apply_type_recipe` applies the type recipe with the corresponding signature and for vectors it tries to apply the recipe element-wise. +When no argument is changed by `_apply_type_recipe`, the fallback `SliceIt` recipe is applied, which adds the data to `plotattributes` and returns `RecipeData` with empty args. + +#### Process Plot Recipes + +At this stage all arguments have been processed to something Plots supports. +In `_plot!` we have a `Vector{Dict}` `kw_list` with an entry for each series and already populated `:x`, `:y` and `:z` keys. +Now `_process_plotrecipe` is called until all plot recipes are processed. + +```julia +still_to_process = kw_list +kw_list = KW[] +while !isempty(still_to_process) + next_kw = popfirst!(still_to_process) + _process_plotrecipe(plt, next_kw, kw_list, still_to_process) +end +``` + +If no series type is set in the Dict, `_process_plotrecipe` pushes it to `kw_list` and returns. +Otherwise it tries to call `RecipesBase.apply_recipe` with the plot recipe signature. +If there is a method for this signature and the seriestype has changed by applying the recipe, the new `plotattributes` are appended to `still_to_process`. +If there is no method for the current plot recipe signature, we append the current Dict to `kw_list` and rely on series recipe processing. + +After all plot recipes have been applied, the plot and subplots are set-up. +```julia +_plot_setup(plt, plotattributes, kw_list) +_subplot_setup(plt, plotattributes, kw_list) +``` + +#### Process Series Recipes + +We are almost finished. +Now the series defaults are populated and `_process_seriesrecipe` is called for each series . + +```julia +for kw in kw_list + # merge defaults + series_attr = Attr(kw, _series_defaults) + _process_seriesrecipe(plt, series_attr) +end +``` + +If the series type is natively supported by the backend, we finalize processing and pass the series along to the backend. +Otherwise, the series recipe for the current series type is applied and `_process_seriesrecipe` is called again for the `plotattributes` in each returned `RecipeData` object. +Here we have to check again that the series type changed. +Due to this recursive processing, complex series types can be built up by simple blocks. +For example if we add an `@show st` in `_process_seriesrecipe` and plot a histogram, we go through the following series types: + +```julia +plot(histogram(randn(1000))) +``` +```julia +st = :histogram +st = :barhist +st = :barbins +st = :bar +st = :shape +``` +```@example +using Plots # hide +plot(histogram(randn(1000))) #hide +``` diff --git a/docs/src/RecipesBase/syntax.md b/docs/src/RecipesBase/syntax.md new file mode 100644 index 00000000000..21c439534b6 --- /dev/null +++ b/docs/src/RecipesBase/syntax.md @@ -0,0 +1,121 @@ +```@setup syntax +using Plots, Random +Random.seed!(100) +default(legend = :topleft, markerstrokecolor = :auto, markersize = 6) +``` + +# Recipes Syntax + +The syntax in the [`@recipe`](@ref) macro is best explained using an example. +Suppose, we have a custom type storing the results of a simulation `x` and `y` and a measure `ε` for the maximum error in `y`. + +```@example syntax +struct Result + x::Vector{Float64} + y::Vector{Float64} + ε::Vector{Float64} +end +``` + +If we want to plot the `x` and `y` values of such a result with an error band given by `ε`, we could run something like +```@example syntax +res = Result(1:10, cumsum(rand(10)), cumsum(rand(10)) / 5) + +using Plots + +# plot the error band as invisible line with fillrange +plot( + res.x, + res.y .+ res.ε, + xlabel = "x", + ylabel = "y", + fill = (res.y .- res.ε, :lightgray, 0.5), + linecolor = nothing, + primary = false, # no legend entry +) + +# add the data to the plots +plot!(res.x, res.y, marker = :diamond) +``` + +Instead of typing this plot command over and over for different results we can define a **user recipe** to tell Plots what to do with input of the type `Result`. +Here is an example for such a user recipe with the additional feature to highlight datapoints with a maximal error above a certain threshold `ε_max`. + +```@example syntax +@recipe function f(r::Result; ε_max = 0.5) + # set a default value for an attribute with `-->` + xlabel --> "x" + yguide --> "y" + markershape --> :diamond + # add a series for an error band + @series begin + # force an argument with `:=` + seriestype := :path + # ignore series in legend and color cycling + primary := false + linecolor := nothing + fillcolor := :lightgray + fillalpha := 0.5 + fillrange := r.y .- r.ε + # ensure no markers are shown for the error band + markershape := :none + # return series data + r.x, r.y .+ r.ε + end + # get the seriescolor passed by the user + c = get(plotattributes, :seriescolor, :auto) + # highlight big errors, otherwise use the user-defined color + markercolor := ifelse.(r.ε .> ε_max, :red, c) + # return data + r.x, r.y +end +``` + +Let's walk through this recipe step by step. +First, the function signature in the recipe definition determines the recipe type, in this case a user recipe. +The function name `f` in is irrelevant and can be replaced by any other function name. +[`@recipe`](@ref) does not use it. +In the recipe body we can set default values for [Plots attributes](http://docs.juliaplots.org/latest/attributes/). +``` +attr --> val +``` +This will set `attr` to `val` unless it is specified otherwise by the user in the plot command. +``` +plot(args...; kw..., attr = otherval) +``` +Similarly we can force an attribute value with `:=`. +``` +attr := val +``` +This overwrites whatever the user passed to `plot` for `attr` and sets it to `val`. +!!! tip + It is strongly recommended to avoid using attribute aliases in recipes as this might lead to unexpected behavior in some cases. + In the recipe above `xlabel` is used as aliases for `xguide`. + When the recipe is used Plots will show a warning and hint to the default attribute name. + They can also be found in the attribute tables under http://docs.juliaplots.org/latest/attributes/. + +We use the [`@series`](@ref) macro to add a new series for the error band to the plot. +Within an [`@series`](@ref) block we can use the same syntax as above to force or set default values for attributes. + +In [`@recipe`](@ref) we have access to `plotattributes`. This is an `AbstractDict` storing the attributes that have been already processed at the current stage in the Plots pipeline. +For user recipes, which are called early in the pipeline, this mostly contains the keyword arguments provided by the user in the `plot` command. +In our example we want to highlight data points with an error above a certain threshold by changing the marker color. +For all other data points we set the marker color to whatever is the default or has been provided as keyword argument. +We can do this by getting the `seriescolor` from `plotattributes` and defaulting to `auto` if it has not been specified by the user. + +Finally, in both, [`@recipe`](@ref)s and [`@series`](@ref) blocks we return the data we wish to pass on to Plots (or the next recipe). + +!!! compat + With RecipesBase 1.0 the `return` statement is allowed in [`@recipe`](@ref) and [`@series`](@ref). + +With the recipe above we can now plot `Result`s with just + +```@example syntax +plot(res) +``` + +or + +```@example syntax +scatter(res, ε_max = 0.7, color = :green, marker = :star) +``` diff --git a/docs/src/RecipesBase/types.md b/docs/src/RecipesBase/types.md new file mode 100644 index 00000000000..a69ed9b197d --- /dev/null +++ b/docs/src/RecipesBase/types.md @@ -0,0 +1,401 @@ +```@setup types +using Plots, Random +Random.seed!(100) +default(legend = :topleft, markerstrokecolor = :auto, markersize = 6) +``` + +# Recipe Types + +## Overview + +There are four main types of recipes which are determined by the signature of the [`@recipe`](@ref) macro. + +### User Recipes + +```julia +@recipe function f(custom_arg_1::T, custom_arg_2::S, ...; ...) +``` + +!!! tip + [`@userplot`](@ref) provides a convenient way to create a custom type to dispatch on and defines custom plotting functions. + ```julia + @userplot MyPlot + @recipe function f(mp::MyPlot; ...) + ... + end + ``` + Now we can plot with: + ```julia + myplot(args...; kw...) + myplot!(args...; kw...) + ``` + +### Type Recipes + +```julia +@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`). + ```julia + @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`](@ref) with `plotattributes[:letter]`. + +!!! compat + With RecipesBase 1.0 type recipes of the form + ```julia + @recipe function f(::Type{T}, val::T) where T <: AbstractArray{MyType} + ``` + for `AbstractArray`s 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 + +```julia +@recipe function f(::Type{Val{:myplotrecipename}}, plt::AbstractPlot; ...) +``` + +### Series Recipes + +```julia +@recipe function f(::Type{Val{:myseriesrecipename}}, x, y, z; ...) +``` + +!!! tip + The [`@shorthands`](@ref) macro provides a convenient way to define plotting functions for custom plot recipes or series recipes. + ```julia + @shorthands myseriestype + @recipe function f(::Type{Val{:myseriestype}}, x, y, z; ...) + ... + end + ``` + This allows to plot with: + ```julia + 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. +```julia +@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`](@ref) macro is a convenient way to do this. +```julia +@userplot MyPlot +``` +expands to +```julia +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. + +```julia +@userplot struct MyPlot{T<:Tuple{AbstractVector}} + args::T +end +``` + +We can use this to define a user recipe for a pie plot. +```@example types +# 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: + +```@example types +userpie('A':'D', rand(4)) +``` + +## Type Recipes +Type recipes define one-to-one mappings from custom types to something Plots supports +```julia +@recipe function f(::Type{T}, val::T) where T +``` + +Suppose we have a custom wrapper for vectors. + +```@example types +struct MyWrapper + v::Vector +end +``` +We can tell Plots to just use the wrapped vector for plotting in a type recipe. +```@example types +@recipe f(::Type{MyWrapper}, mw::MyWrapper) = mw.v +``` +Now Plots knows what to do when it sees a `MyWrapper`. +```@example types +mw = MyWrapper(cumsum(rand(10))) +plot(mw) +``` +Due to the recursive application of type recipes they even compose automatically. +```@example types +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. +```@example types +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. +`DateTime`s and `Char`s are implemented with such a type recipe in Plots for example. + +```@example types +times = MyTime.(0:23, rand(0:59, 24)) +vals = log.(1:24) + +plot(times, vals) +``` +Again everything composes nicely. +```@example types +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. +```julia +@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. +```@example types +@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`](@ref) macro. +```julia +@shorthands myseries +``` +expands to +```julia +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. +```@example types +@shorthands yscaleplot + +yscaleplot((1:10).^2) +``` +Magically the composition with type recipes works again. +```@example types +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. +```julia +@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. +```julia +userpie(MyWrapper(rand(4))) +``` +```julia +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 +```@example types +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. +```@example types +@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 +``` +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`. +```@example types +seriespie(rand(4)) +``` +This automatically works together with type recipes ... +```@example types +seriespie(MyWrapper(rand(4))) +``` +... or with layouts +```@example types +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`. + +```@example types +@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. + +```@example types +@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 +``` +That looks a little nicer than the plot recipe version as well. +Let's try to plot. +```julia +yscaleseries((1:10).^2) +``` +```julia +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. diff --git a/docs/src/RecipesPipeline/api.md b/docs/src/RecipesPipeline/api.md new file mode 100644 index 00000000000..2896860ed7e --- /dev/null +++ b/docs/src/RecipesPipeline/api.md @@ -0,0 +1,18 @@ +### API + +```@autodocs +Modules = [RecipesPipeline] +Pages = [ + "api.jl" +] +Private = false +``` + +### Utility functions + +```@autodocs +Modules = [RecipesPipeline] +Pages = [ + "utils.jl" +] +``` diff --git a/docs/src/RecipesPipeline/index.md b/docs/src/RecipesPipeline/index.md new file mode 100644 index 00000000000..289969c77eb --- /dev/null +++ b/docs/src/RecipesPipeline/index.md @@ -0,0 +1,7 @@ +# RecipesPipeline.jl + +Documentation for RecipesPipeline.jl + +```@docs +recipe_pipeline!(plt, plotattributes, args) +``` diff --git a/docs/src/RecipesPipeline/recipes.md b/docs/src/RecipesPipeline/recipes.md new file mode 100644 index 00000000000..381c648a07f --- /dev/null +++ b/docs/src/RecipesPipeline/recipes.md @@ -0,0 +1,10 @@ +## Recipes processing + +```@autodocs +Modules = [RecipesPipeline] +Pages = [ + "user_recipe.jl", + "plot_recipe.jl", + "series_recipe.jl" +] +``` diff --git a/docs/src/RecipesPipeline/reference.md b/docs/src/RecipesPipeline/reference.md new file mode 100644 index 00000000000..d6855079181 --- /dev/null +++ b/docs/src/RecipesPipeline/reference.md @@ -0,0 +1,4 @@ +## Reference + +```@index +``` diff --git a/docs/src/unitfulrecipes/unitfulrecipes.md b/docs/src/UnitfulRecipes/unitfulrecipes.md similarity index 100% rename from docs/src/unitfulrecipes/unitfulrecipes.md rename to docs/src/UnitfulRecipes/unitfulrecipes.md diff --git a/docs/src/unitfulrecipes/unitfulrecipes_examples.jl b/docs/src/UnitfulRecipes/unitfulrecipes_examples.jl similarity index 100% rename from docs/src/unitfulrecipes/unitfulrecipes_examples.jl rename to docs/src/UnitfulRecipes/unitfulrecipes_examples.jl diff --git a/docs/src/unitfulrecipes/unitfulrecipes_plots.jl b/docs/src/UnitfulRecipes/unitfulrecipes_plots.jl similarity index 100% rename from docs/src/unitfulrecipes/unitfulrecipes_plots.jl rename to docs/src/UnitfulRecipes/unitfulrecipes_plots.jl