User Recipes

"""
    _process_userrecipes(plt, plotattributes, args)

Wrap input arguments in a `RecipeData' vector and recursively apply user recipes and type
recipes on the first element. Prepend the returned `RecipeData` vector. If an element with
empy `args` is returned pop it from the vector, finish up, and it to vector of `Dict`s with
processed series. When all arguments are processed return the series `Dict`.
"""
function _process_userrecipes!(plt, plotattributes, args)
    @nospecialize
    still_to_process = _recipedata_vector(plt, plotattributes, args)

For plotting recipes, we swap out the args and update the parameter dictionary. We are keeping a stack of series that still need to be processed.

On each pass through the loop, we pop one off and apply the recipe. the recipe will return a list a Series objects. The ones that are finished (no more args) get added to the kw_list, and the ones that are not are placed on top of the stack and are then processed further.

    kw_list = KW[]
    while !isempty(still_to_process)

grab the first in line to be processed and either add it to the kwlist or pass it through applyrecipe to generate a list of RecipeData objects (data + attributes) for further processing.

        next_series = popfirst!(still_to_process)

recipedata should be of type RecipeData. if it's not then the inputs must not have been fully processed by recipes

        if !(typeof(next_series) <: RecipeData)
            error("Inputs couldn't be processed... expected RecipeData but got: $next_series")
        end
        if isempty(next_series.args)
            _finish_userrecipe!(plt, kw_list, next_series)
        else
            rd_list =
                RecipesBase.apply_recipe(next_series.plotattributes, next_series.args...)
                warn_on_recipe_aliases!(plt, rd_list, :user, next_series.args)
            prepend!(still_to_process, rd_list)
        end
    end

don't allow something else to handle it

    plotattributes[:smooth] = false
    kw_list
end

TODO Move this to api.jl?

function _recipedata_vector(plt, plotattributes, args)
    @nospecialize
    still_to_process = RecipeData[]

the grouping mechanism is a recipe on a GroupBy object we simply add the GroupBy object to the front of the args list to allow the recipe to be applied

    if haskey(plotattributes, :group)
        args = (_extract_group_attributes(plotattributes[:group], args...), args...)
    end

if we were passed a vector/matrix of seriestypes and there's more than one row, we want to duplicate the inputs, once for each seriestype row.

    if !isempty(args)
        append!(still_to_process, _expand_seriestype_array(plotattributes, args))
    end

remove subplot and axis args from plotattributes... they will be passed through in the kw_list

    if !isempty(args)
        for (k, v) in plotattributes
            if is_subplot_attribute(plt, k) || is_axis_attribute(plt, k)
                reset_kw!(plotattributes, k)
            end
        end
    end

    still_to_process
end

function _expand_seriestype_array(plotattributes, args)
    @nospecialize
    sts = get(plotattributes, :seriestype, :path)
    if typeof(sts) <: AbstractArray
        reset_kw!(plotattributes, :seriestype)
        rd = Vector{RecipeData}(undef, size(sts, 1))
        for r in axes(sts, 1)
            dc = copy(plotattributes)
            dc[:seriestype] = sts[r:r, :]
            rd[r] = RecipeData(dc, args)
        end
        rd
    else
        RecipeData[RecipeData(copy(plotattributes), args)]
    end
end


function _finish_userrecipe!(plt, kw_list, recipedata)

when the arg tuple is empty, that means there's nothing left to recursively process... finish up and add to the kw_list

    kw = recipedata.plotattributes
    preprocess_attributes!(plt, kw)

if there was a grouping, filter the data here

    _filter_input_data!(kw)
    process_userrecipe!(plt, kw_list, kw)
end

Fallback user recipes

@nospecialize

These call _apply_type_recipe in type_recipe.jl and finally the SliceIt recipe in series.jl.

handle "type recipes" by converting inputs, and then either re-calling or slicing

@recipe function f(x, y, z)
    wrap_surfaces!(plotattributes, x, y, z)
    did_replace = false
    newx = _apply_type_recipe(plotattributes, x, :x)
    x === newx || (did_replace = true)
    newy = _apply_type_recipe(plotattributes, y, :y)
    y === newy || (did_replace = true)
    newz = _apply_type_recipe(plotattributes, z, :z)
    z === newz || (did_replace = true)
    if did_replace
        newx, newy, newz
    else
        SliceIt, x, y, z
    end
end
@recipe function f(x, y)
    wrap_surfaces!(plotattributes, x, y)
    did_replace = false
    newx = _apply_type_recipe(plotattributes, x, :x)
    x === newx || (did_replace = true)
    newy = _apply_type_recipe(plotattributes, y, :y)
    y === newy || (did_replace = true)
    if did_replace
        newx, newy
    else
        SliceIt, x, y, nothing
    end
end
@recipe function f(y)
    wrap_surfaces!(plotattributes, y)
    newy = _apply_type_recipe(plotattributes, y, :y)
    if y !== newy
        newy
    else
        SliceIt, nothing, y, nothing
    end
end

if there's more than 3 inputs, it can't be passed directly to SliceIt so we'll applytyperecipe to all of them

@recipe function f(v1, v2, v3, v4, vrest...)
    did_replace = false
    newargs = map(
        v -> begin
            newv = _apply_type_recipe(plotattributes, v, :unknown)
            if newv !== v
                did_replace = true
            end
            newv
        end,
        (v1, v2, v3, v4, vrest...),
    )
    if !did_replace
        error("Couldn't process recipe args: $(map(typeof, (v1, v2, v3, v4, vrest...)))")
    end
    newargs
end

helper function to ensure relevant attributes are wrapped by Surface

function wrap_surfaces!(plotattributes, args...) end
wrap_surfaces!(plotattributes, x::AMat, y::AMat, z::AMat) = wrap_surfaces!(plotattributes)
wrap_surfaces!(plotattributes, x::AVec, y::AVec, z::AMat) = wrap_surfaces!(plotattributes)
function wrap_surfaces!(plotattributes, x::AVec, y::AVec, z::Surface)
    wrap_surfaces!(plotattributes)
end
function wrap_surfaces!(plotattributes)
    if haskey(plotattributes, :fill_z)
        v = plotattributes[:fill_z]
        if !isa(v, Surface)
            plotattributes[:fill_z] = Surface(v)
        end
    end
end

Special Cases


1 argument

@recipe function f(n::Integer)
    if is3d(plotattributes)
        SliceIt, n, n, n
    else
        SliceIt, n, n, nothing
    end
end

return a surface if this is a 3d plot, otherwise let it be sliced up

@recipe function f(mat::AMat)
    if is3d(plotattributes)
        n, m = axes(mat)
        m, n, Surface(mat)
    else
        nothing, mat, nothing
    end
end

if a matrix is wrapped by Formatted, do similar logic, but wrap data with Surface

@recipe function f(fmt::Formatted{<:AMat})
    if is3d(plotattributes)
        mat = fmt.data
        n, m = axes(mat)
        m, n, Formatted(Surface(mat), fmt.formatter)
    else
        nothing, fmt, nothing
    end
end

assume this is a Volume, so construct one

@recipe function f(vol::AbstractArray{<:MaybeNumber, 3}, args...)
    seriestype := :volume
    SliceIt, nothing, Volume(vol, args...), nothing
end

Dicts: each entry is a data point (x,y)=(key,value)

@recipe function f(d::AbstractDict)
    seriestype --> :line
    collect(keys(d)), collect(values(d))
end

function without range... use the current range of the x-axis

@recipe function f(f::FuncOrFuncs{F}) where {F <: Function}
    plt = plotattributes[:plot_object]
    xmin, xmax = if haskey(plotattributes, :xlims)
        plotattributes[:xlims]
    else
        try
            get_axis_limits(plt, :x)
        catch
            xinv = inverse_scale_func(get(plotattributes, :xscale, :identity))
            xm = PlotUtils.tryrange(f, xinv.([-5, -1, 0, 0.01]))
            xm, PlotUtils.tryrange(f, filter(x -> x > xm, xinv.([5, 1, 0.99, 0, -0.01])))
        end
    end
    f, xmin, xmax
end

2 arguments

if functions come first, just swap the order (not to be confused with parametric functions... as there would be more than one function passed in)

@recipe function f(f::FuncOrFuncs{F}, x) where {F <: Function}
    F2 = typeof(x)
    @assert !(F2 <: Function || (F2 <: AbstractArray && F2.parameters[1] <: Function))

otherwise we'd hit infinite recursion here

    x, f
end

3 arguments

surface-like... function

@recipe function f(x::AVec, y::AVec, zf::Function)
    x, y, Surface(zf, x, y)  # TODO: replace with SurfaceFunction when supported
end

surface-like... matrix grid

@recipe function f(x::AVec, y::AVec, z::AMat)
    if !is_surface(plotattributes)
        plotattributes[:seriestype] = :contour
    end
    x, y, Surface(z)
end

parametric functions special handling... xmin/xmax with parametric function(s)

@recipe function f(f::Function, xmin::Number, xmax::Number)
    xscale, yscale = [get(plotattributes, sym, :identity) for sym in (:xscale, :yscale)]
    _scaled_adapted_grid(f, xscale, yscale, xmin, xmax)
end
@recipe function f(fs::AbstractArray{F}, xmin::Number, xmax::Number) where {F <: Function}
    xscale, yscale = [get(plotattributes, sym, :identity) for sym in (:xscale, :yscale)]
    unzip(_scaled_adapted_grid.(vec(fs), xscale, yscale, xmin, xmax))
end
@recipe f(
    fx::FuncOrFuncs{F},
    fy::FuncOrFuncs{G},
    u::AVec,
) where {F <: Function, G <: Function} = _map_funcs(fx, u), _map_funcs(fy, u)
@recipe f(
    fx::FuncOrFuncs{F},
    fy::FuncOrFuncs{G},
    umin::Number,
    umax::Number,
    n = 200,
) where {F <: Function, G <: Function} = fx, fy, range(umin, stop = umax, length = n)

special handling... 3D parametric function(s)

@recipe function f(
    fx::FuncOrFuncs{F},
    fy::FuncOrFuncs{G},
    fz::FuncOrFuncs{H},
    u::AVec,
) where {F <: Function, G <: Function, H <: Function}
    _map_funcs(fx, u), _map_funcs(fy, u), _map_funcs(fz, u)
end
@recipe function f(
    fx::FuncOrFuncs{F},
    fy::FuncOrFuncs{G},
    fz::FuncOrFuncs{H},
    umin::Number,
    umax::Number,
    numPoints = 200,
) where {F <: Function, G <: Function, H <: Function}
    fx, fy, fz, range(umin, stop = umax, length = numPoints)
end

list of tuples

@recipe f(v::AVec{<:Tuple}) = unzip(v)
@recipe f(tup::Tuple) = [tup]

list of NamedTuples

@recipe function f(ntv::AVec{<:NamedTuple{K, Tuple{S, T}}}) where {K, S, T}
    xguide --> string(K[1])
    yguide --> string(K[2])
    return Tuple.(ntv)
end
@recipe function f(ntv::AVec{<:NamedTuple{K, Tuple{R, S, T}}}) where {K, R, S, T}
    xguide --> string(K[1])
    yguide --> string(K[2])
    zguide --> string(K[3])
    return Tuple.(ntv)
end

@specialize

function _scaled_adapted_grid(f, xscale, yscale, xmin, xmax)
    (xf, xinv), (yf, yinv) = ((scale_func(s), inverse_scale_func(s)) for s in (xscale, yscale))
    xs, ys = PlotUtils.adapted_grid(yf ∘ f ∘ xinv, xf.((xmin, xmax)))
    xinv.(xs), yinv.(ys)
end

This page was generated using Literate.jl.