depend on RecipePipeline

This commit is contained in:
Daniel Schwabeneder 2020-04-05 11:58:07 +02:00
parent 330739298c
commit 80ec6f03b4
11 changed files with 2 additions and 1284 deletions

View File

@ -21,6 +21,7 @@ PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
RecipePipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c"
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"

View File

@ -1,97 +0,0 @@
module RecipePipeline
import RecipesBase
import RecipesBase: @recipe, @series, RecipeData, is_explicit
import PlotUtils # tryrange and adapted_grid
export recipe_pipeline!
# Plots relies on these:
export SliceIt,
DefaultsDict,
Formatted,
AbstractSurface,
Surface,
Volume,
is3d,
is_surface,
needs_3d_axes,
group_as_matrix,
reset_kw!,
pop_kw!,
scale_func,
inverse_scale_func,
unzip
# API
export warn_on_recipe_aliases,
splittable_attribute,
split_attribute,
process_userrecipe!,
get_axis_limits,
is_axis_attribute,
type_alias,
plot_setup!,
slice_series_attributes!
include("api.jl")
include("utils.jl")
include("series.jl")
include("group.jl")
include("user_recipe.jl")
include("type_recipe.jl")
include("plot_recipe.jl")
include("series_recipe.jl")
"""
recipe_pipeline!(plt, plotattributes, args)
Recursively apply user recipes, type recipes, plot recipes and series recipes to build a
list of `Dict`s, each corresponding to a series. At the beginning `plotattributes`
contains only the keyword arguments passed in by the user. Add all series to the plot
bject `plt` and return it.
"""
function recipe_pipeline!(plt, plotattributes, args)
plotattributes[:plot_object] = plt
# --------------------------------
# "USER RECIPES"
# --------------------------------
# process user and type recipes
kw_list = _process_userrecipes!(plt, plotattributes, args)
# --------------------------------
# "PLOT RECIPES"
# --------------------------------
# The "Plot recipe" acts like a series type, and is processed before the plot layout
# is created, which allows for setting layouts and other plot-wide attributes.
# We get inputs which have been fully processed by "user recipes" and "type recipes",
# so we can expect standard vectors, surfaces, etc. No defaults have been set yet.
kw_list = _process_plotrecipes!(plt, kw_list)
# --------------------------------
# Plot/Subplot/Layout setup
# --------------------------------
plot_setup!(plt, plotattributes, kw_list)
# At this point, `kw_list` is fully decomposed into individual series... one KW per
# series. The next step is to recursively apply series recipes until the backend
# supports that series type.
# --------------------------------
# "SERIES RECIPES"
# --------------------------------
_process_seriesrecipes!(plt, kw_list)
# --------------------------------
# Return processed plot object
# --------------------------------
return plt
end
end

View File

@ -1,142 +0,0 @@
## Warnings
"""
warn_on_recipe_aliases!(plt, plotattributes, recipe_type, args...)
Warn if an alias is dedected in `plotattributes` after a recipe of type `recipe_type` is
applied to 'args'. `recipe_type` is either `:user`, `:type`, `:plot` or `:series`.
"""
function warn_on_recipe_aliases!(plt, plotattributes, recipe_type, args...) end
## Grouping
"""
splittable_attribute(plt, key, val, len)
Returns `true` if the attribute `key` with the value `val` can be split into groups with
group provided as a vector of length `len`, `false` otherwise.
"""
splittable_attribute(plt, key, val, len) = false
splittable_attribute(plt, key, val::AbstractArray, len) =
!(key in (:group, :color_palette)) && length(axes(val, 1)) == len
splittable_attribute(plt, key, val::Tuple, n) = all(splittable_attribute.(key, val, len))
"""
split_attribute(plt, key, val, indices)
Select the proper indices from `val` for attribute `key`.
"""
split_attribute(plt, key, val::AbstractArray, indices) =
val[indices, fill(Colon(), ndims(val) - 1)...]
split_attribute(plt, key, val::Tuple, indices) =
Tuple(split_attribute(key, v, indices) for v in val)
## Preprocessing attributes
"""
preprocess_attributes!(plt, plotattributes)
Any plotting package specific preprocessing of user or recipe input happens here.
For example, Plots replaces aliases and expands magic arguments.
"""
function preprocess_attributes!(plt, plotattributes) end
# TODO: should the Plots version be defined as fallback in RecipePipeline?
"""
is_subplot_attribute(plt, attr)
Returns `true` if `attr` is a subplot attribute, otherwise `false`.
"""
is_subplot_attribute(plt, attr) = false
# TODO: should the Plots version be defined as fallback in RecipePipeline?
"""
is_axis_attribute(plt, attr)
Returns `true` if `attr` is an axis attribute, i.e. it applies to `xattr`, `yattr` and
`zattr`, otherwise `false`.
"""
is_axis_attribute(plt, attr) = false
## User recipes
"""
process_userrecipe!(plt, attributes_list, attributes)
Do plotting package specific post-processing and add series attributes to attributes_list.
For example, Plots increases the number of series in `plt`, sets `:series_plotindex` in
attributes and possible adds new series attributes for errorbars or smooth.
"""
function process_userrecipe!(plt, attributes_list, attributes)
push!(attributes_list, attributes)
end
"""
get_axis_limits(plt, letter)
Get the limits for the axis specified by `letter` (`:x`, `:y` or `:z`) in `plt`. If it
errors, `tryrange` from PlotUtils is used.
"""
get_axis_limits(plt, letter) = ErrorException("Axis limits not defined.")
## Plot recipes
"""
type_alias(plt, st)
Return the seriestype alias for `st`.
"""
type_alias(plt, st) = st
## Plot setup
"""
plot_setup!(plt, plotattributes, kw_list)
Setup plot, subplots and layouts.
For example, Plots creates the backend figure, initializes subplots, expands extrema and
links subplot axes.
"""
function plot_setup!(plt, plotattributes, kw_list) end
## Series recipes
"""
slice_series_attributes!(plt, kw_list, kw)
For attributes given as vector with one element per series, only select the value for
current series.
"""
function slice_series_attributes!(plt, kw_list, kw) end
"""
series_defaults(plt)
Returns a `Dict` storing the defaults for series attributes.
"""
series_defaults(plt) = Dict{Symbol, Any}()
# TODO: Add a more sensible fallback including e.g. path, scatter, ...
"""
is_seriestype_supported(plt, st)
Check if the plotting package natively supports the seriestype `st`.
"""
is_seriestype_supported(plt, st) = false
"""
add_series!(plt, kw)
Adds the series defined by `kw` to the plot object.
For example Plots updates the current subplot arguments, expands extrema and pushes the
the series to the series_list of `plt`.
"""
function add_series!(plt, kw) end

View File

@ -1,122 +0,0 @@
"A special type that will break up incoming data into groups, and allow for easier creation of grouped plots"
mutable struct GroupBy
group_labels::Vector # length == numGroups
group_indices::Vector{Vector{Int}} # list of indices for each group
end
# this is when given a vector-type of values to group by
function _extract_group_attributes(v::AVec, args...; legend_entry = string)
group_labels = sort(collect(unique(v)))
n = length(group_labels)
if n > 100
@warn("You created n=$n groups... Is that intended?")
end
group_indices = Vector{Int}[filter(i -> v[i] == glab, eachindex(v)) for glab in group_labels]
GroupBy(map(legend_entry, group_labels), group_indices)
end
legend_entry_from_tuple(ns::Tuple) = join(ns, ' ')
# this is when given a tuple of vectors of values to group by
function _extract_group_attributes(vs::Tuple, args...)
isempty(vs) && return GroupBy([""], [axes(args[1],1)])
v = map(tuple, vs...)
_extract_group_attributes(v, args...; legend_entry = legend_entry_from_tuple)
end
# allow passing NamedTuples for a named legend entry
legend_entry_from_tuple(ns::NamedTuple) =
join(["$k = $v" for (k, v) in pairs(ns)], ", ")
function _extract_group_attributes(vs::NamedTuple, args...)
isempty(vs) && return GroupBy([""], [axes(args[1],1)])
v = map(NamedTuple{keys(vs)}tuple, values(vs)...)
_extract_group_attributes(v, args...; legend_entry = legend_entry_from_tuple)
end
# expecting a mapping of "group label" to "group indices"
function _extract_group_attributes(idxmap::Dict{T,V}, args...) where {T, V<:AVec{Int}}
group_labels = sortedkeys(idxmap)
group_indices = Vector{Int}[collect(idxmap[k]) for k in group_labels]
GroupBy(group_labels, group_indices)
end
filter_data(v::AVec, idxfilter::AVec{Int}) = v[idxfilter]
filter_data(v, idxfilter) = v
function filter_data!(plotattributes::AKW, idxfilter)
for s in (:x, :y, :z)
plotattributes[s] = filter_data(get(plotattributes, s, nothing), idxfilter)
end
end
function _filter_input_data!(plotattributes::AKW)
idxfilter = pop!(plotattributes, :idxfilter, nothing)
if idxfilter !== nothing
filter_data!(plotattributes, idxfilter)
end
end
function groupedvec2mat(x_ind, x, y::AbstractArray, groupby, def_val = y[1])
y_mat = Array{promote_type(eltype(y), typeof(def_val))}(
undef,
length(keys(x_ind)),
length(groupby.group_labels),
)
fill!(y_mat, def_val)
for i in eachindex(groupby.group_labels)
xi = x[groupby.group_indices[i]]
yi = y[groupby.group_indices[i]]
y_mat[getindex.(Ref(x_ind), xi), i] = yi
end
return y_mat
end
groupedvec2mat(x_ind, x, y::Tuple, groupby) =
Tuple(groupedvec2mat(x_ind, x, v, groupby) for v in y)
group_as_matrix(t) = false
# split the group into 1 series per group, and set the label and idxfilter for each
@recipe function f(groupby::GroupBy, args...)
plt = plotattributes[:plot_object]
group_length = maximum(union(groupby.group_indices...))
if !(group_as_matrix(args[1]))
for (i, glab) in enumerate(groupby.group_labels)
@series begin
label --> string(glab)
idxfilter --> groupby.group_indices[i]
for (key, val) in plotattributes
if splittable_attribute(plt, key, val, group_length)
:($key) := split_attribute(plt, key, val, groupby.group_indices[i])
end
end
args
end
end
else
g = args[1]
if length(g.args) == 1
x = zeros(Int, group_length)
for indexes in groupby.group_indices
x[indexes] = eachindex(indexes)
end
last_args = g.args
else
x = g.args[1]
last_args = g.args[2:end]
end
x_u = unique(sort(x))
x_ind = Dict(zip(x_u, eachindex(x_u)))
for (key, val) in plotattributes
if splittable_kw(key, val, group_length)
:($key) := groupedvec2mat(x_ind, x, val, groupby)
end
end
label --> reshape(groupby.group_labels, 1, :)
typeof(g)((
x_u,
(groupedvec2mat(x_ind, x, arg, groupby, NaN) for arg in last_args)...,
))
end
end

View File

@ -1,46 +0,0 @@
"""
_process_plotrecipes!(plt, kw_list)
Grab the first in line to be processed and pass it through `apply_recipe` to generate a
list of `RecipeData` objects.
If we applied a "plot recipe" without error, then add the returned datalist's KWs,
otherwise we just add the original KW.
"""
function _process_plotrecipes!(plt, kw_list)
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
return kw_list
end
function _process_plotrecipe(plt, kw, kw_list, still_to_process)
if !isa(get(kw, :seriestype, nothing), Symbol)
# seriestype was never set, or it's not a Symbol, so it can't be a plot recipe
push!(kw_list, kw)
return
end
try
st = kw[:seriestype]
st = kw[:seriestype] = type_alias(plt, st)
datalist = RecipesBase.apply_recipe(kw, Val{st}, plt)
warn_on_recipe_aliases!(plt, datalist, :plot, st)
for data in datalist
preprocess_attributes!(plt, data.plotattributes)
if data.plotattributes[:seriestype] == st
error("Plot recipe $st returned the same seriestype: $(data.plotattributes)")
end
push!(still_to_process, data.plotattributes)
end
catch err
if isa(err, MethodError)
push!(kw_list, kw)
else
rethrow()
end
end
return
end

View File

@ -1,170 +0,0 @@
const FuncOrFuncs{F} = Union{F, Vector{F}, Matrix{F}}
const MaybeNumber = Union{Number, Missing}
const MaybeString = Union{AbstractString, Missing}
const DataPoint = Union{MaybeNumber, MaybeString}
_prepare_series_data(x) = error("Cannot convert $(typeof(x)) to series data for plotting")
_prepare_series_data(::Nothing) = nothing
_prepare_series_data(t::Tuple{T, T}) where {T <: Number} = t
_prepare_series_data(f::Function) = f
_prepare_series_data(ar::AbstractRange{<:Number}) = ar
function _prepare_series_data(a::AbstractArray{<:MaybeNumber})
f = isimmutable(a) ? replace : replace!
a = f(x -> ismissing(x) || isinf(x) ? NaN : x, map(float, a))
end
_prepare_series_data(a::AbstractArray{<:Missing}) = fill(NaN, axes(a))
_prepare_series_data(a::AbstractArray{<:MaybeString}) =
replace(x -> ismissing(x) ? "" : x, a)
_prepare_series_data(s::Surface{<:AMat{<:MaybeNumber}}) =
Surface(_prepare_series_data(s.surf))
_prepare_series_data(s::Surface) = s # non-numeric Surface, such as an image
_prepare_series_data(v::Volume) =
Volume(_prepare_series_data(v.v), v.x_extents, v.y_extents, v.z_extents)
# default: assume x represents a single series
_series_data_vector(x, plotattributes) = [_prepare_series_data(x)]
# fixed number of blank series
_series_data_vector(n::Integer, plotattributes) = [zeros(0) for i in 1:n]
# vector of data points is a single series
_series_data_vector(v::AVec{<:DataPoint}, plotattributes) = [_prepare_series_data(v)]
# list of things (maybe other vectors, functions, or something else)
function _series_data_vector(v::AVec, plotattributes)
if all(x -> x isa MaybeNumber, v)
_series_data_vector(Vector{MaybeNumber}(v), plotattributes)
elseif all(x -> x isa MaybeString, v)
_series_data_vector(Vector{MaybeString}(v), plotattributes)
else
vcat((_series_data_vector(vi, plotattributes) for vi in v)...)
end
end
# Matrix is split into columns
function _series_data_vector(v::AMat{<:DataPoint}, plotattributes)
if is3d(plotattributes)
[_prepare_series_data(Surface(v))]
else
[_prepare_series_data(v[:, i]) for i in axes(v, 2)]
end
end
# --------------------------------------------------------------------
# Fillranges & ribbons
_process_fillrange(range::Number, plotattributes) = [range]
_process_fillrange(range, plotattributes) = _series_data_vector(range, plotattributes)
_process_ribbon(ribbon::Number, plotattributes) = [ribbon]
_process_ribbon(ribbon, plotattributes) = _series_data_vector(ribbon, plotattributes)
# ribbon as a tuple: (lower_ribbons, upper_ribbons)
_process_ribbon(ribbon::Tuple{S, T}, plotattributes) where {S, T} = collect(zip(
_series_data_vector(ribbon[1], plotattributes),
_series_data_vector(ribbon[2], plotattributes),
))
# --------------------------------------------------------------------
_compute_x(x::Nothing, y::Nothing, z) = axes(z, 1)
_compute_x(x::Nothing, y, z) = axes(y, 1)
_compute_x(x::Function, y, z) = map(x, y)
_compute_x(x, y, z) = x
_compute_y(x::Nothing, y::Nothing, z) = axes(z, 2)
_compute_y(x, y::Function, z) = map(y, x)
_compute_y(x, y, z) = y
_compute_z(x, y, z::Function) = map(z, x, y)
_compute_z(x, y, z::AbstractMatrix) = Surface(z)
_compute_z(x, y, z::Nothing) = nothing
_compute_z(x, y, z) = z
_nobigs(v::AVec{BigFloat}) = map(Float64, v)
_nobigs(v::AVec{BigInt}) = map(Int64, v)
_nobigs(v) = v
@noinline function _compute_xyz(x, y, z)
x = _compute_x(x, y, z)
y = _compute_y(x, y, z)
z = _compute_z(x, y, z)
_nobigs(x), _nobigs(y), _nobigs(z)
end
# not allowed
_compute_xyz(x::Nothing, y::FuncOrFuncs{F}, z) where {F <: Function} =
error("If you want to plot the function `$y`, you need to define the x values!")
_compute_xyz(x::Nothing, y::Nothing, z::FuncOrFuncs{F}) where {F <: Function} =
error("If you want to plot the function `$z`, you need to define x and y values!")
_compute_xyz(x::Nothing, y::Nothing, z::Nothing) = error("x/y/z are all nothing!")
# --------------------------------------------------------------------
# we are going to build recipes to do the processing and splitting of the args
# --------------------------------------------------------------------
# The catch-all SliceIt recipe
# --------------------------------------------------------------------
# ensure we dispatch to the slicer
struct SliceIt end
# TODO: Should ribbon and fillrange be handled by the plotting package?
# The `SliceIt` recipe finishes user and type recipe processing.
# It splits processed data into individual series data, stores in copied `plotattributes`
# for each series and returns no arguments.
@recipe function f(::Type{SliceIt}, x, y, z)
# handle data with formatting attached
if typeof(x) <: Formatted
xformatter := x.formatter
x = x.data
end
if typeof(y) <: Formatted
yformatter := y.formatter
y = y.data
end
if typeof(z) <: Formatted
zformatter := z.formatter
z = z.data
end
xs = _series_data_vector(x, plotattributes)
ys = _series_data_vector(y, plotattributes)
zs = _series_data_vector(z, plotattributes)
fr = pop!(plotattributes, :fillrange, nothing)
fillranges = _process_fillrange(fr, plotattributes)
mf = length(fillranges)
rib = pop!(plotattributes, :ribbon, nothing)
ribbons = _process_ribbon(rib, plotattributes)
mr = length(ribbons)
mx = length(xs)
my = length(ys)
mz = length(zs)
if mx > 0 && my > 0 && mz > 0
for i in 1:max(mx, my, mz)
# add a new series
di = copy(plotattributes)
xi, yi, zi = xs[mod1(i, mx)], ys[mod1(i, my)], zs[mod1(i, mz)]
di[:x], di[:y], di[:z] = _compute_xyz(xi, yi, zi)
# handle fillrange
fr = fillranges[mod1(i, mf)]
di[:fillrange] = isa(fr, Function) ? map(fr, di[:x]) : fr
# handle ribbons
rib = ribbons[mod1(i, mr)]
di[:ribbon] = isa(rib, Function) ? map(rib, di[:x]) : rib
push!(series_list, RecipeData(di, ()))
end
end
nothing # don't add a series for the main block
end

View File

@ -1,62 +0,0 @@
"""
_process_seriesrecipes!(plt, kw_list)
Recursively apply series recipes until the backend supports the seriestype
"""
function _process_seriesrecipes!(plt, kw_list)
for kw in kw_list
# in series attributes given as vector with one element per series,
# select the value for current series
slice_series_attributes!(plt, kw_list, kw)
series_attr = DefaultsDict(kw, series_defaults(plt))
# now we have a fully specified series, with colors chosen. we must recursively
# handle series recipes, which dispatch on seriestype. If a backend does not
# natively support a seriestype, we check for a recipe that will convert that
# series type into one made up of lower-level components.
# For example, a histogram is just a bar plot with binned data, a bar plot is
# really a filled step plot, and a step plot is really just a path. So any backend
# that supports drawing a path will implicitly be able to support step, bar, and
# histogram plots (and any recipes that use those components).
_process_seriesrecipe(plt, series_attr)
end
end
# this method recursively applies series recipes when the seriestype is not supported
# natively by the backend
function _process_seriesrecipe(plt, plotattributes)
# replace seriestype aliases
st = Symbol(plotattributes[:seriestype])
st = plotattributes[:seriestype] = type_alias(plt, st)
# shapes shouldn't have fillrange set
if plotattributes[:seriestype] == :shape
plotattributes[:fillrange] = nothing
end
# if it's natively supported, finalize processing and pass along to the backend,
# otherwise recurse
if is_seriestype_supported(plt, st)
add_series!(plt, plotattributes)
else
# get a sub list of series for this seriestype
x, y, z = plotattributes[:x], plotattributes[:y], plotattributes[:z]
datalist = RecipesBase.apply_recipe(plotattributes, Val{st}, x, y, z)
warn_on_recipe_aliases!(plt, datalist, :series, st)
# assuming there was no error, recursively apply the series recipes
for data in datalist
if isa(data, RecipeData)
preprocess_attributes!(plt, data.plotattributes)
if data.plotattributes[:seriestype] == st
error("The seriestype didn't change in series recipe $st. This will cause a StackOverflow.")
end
_process_seriesrecipe(plt, data.plotattributes)
else
@warn("Unhandled recipe: $(data)")
break
end
end
end
nothing
end

View File

@ -1,94 +0,0 @@
# this is the default "type recipe"... just pass the object through
@recipe f(::Type{T}, v::T) where {T} = v
# this should catch unhandled "series recipes" and error with a nice message
@recipe f(::Type{V}, x, y, z) where {V <: Val} =
error("The backend must not support the series type $V, and there isn't a series recipe defined.")
"""
_apply_type_recipe(plotattributes, v::T, letter)
Apply the type recipe with signature `(::Type{T}, ::T)`.
"""
function _apply_type_recipe(plotattributes, v, letter)
_preprocess_axis_args!(plotattributes, letter)
rdvec = RecipesBase.apply_recipe(plotattributes, typeof(v), v)
warn_on_recipe_aliases!(plotattributes[:plot_object], plotattributes, :type, typeof(v))
_postprocess_axis_args!(plotattributes, letter)
return rdvec[1].args[1]
end
# Handle type recipes when the recipe is defined on the elements.
# This sort of recipe should return a pair of functions... one to convert to number,
# and one to format tick values.
function _apply_type_recipe(plotattributes, v::AbstractArray, letter)
plt = plotattributes[:plot_object]
_preprocess_axis_args!(plotattributes, letter)
# First we try to apply an array type recipe.
w = RecipesBase.apply_recipe(plotattributes, typeof(v), v)[1].args[1]
warn_on_recipe_aliases!(plt, plotattributes, :type, typeof(v))
# If the type did not change try it element-wise
if typeof(v) == typeof(w)
isempty(skipmissing(v)) && return Float64[]
x = first(skipmissing(v))
args = RecipesBase.apply_recipe(plotattributes, typeof(x), x)[1].args
warn_on_recipe_aliases!(plt, plotattributes, :type, typeof(x))
_postprocess_axis_args!(plotattributes, letter)
if length(args) == 2 && all(arg -> arg isa Function, args)
numfunc, formatter = args
return Formatted(map(numfunc, v), formatter)
else
return v
end
end
_postprocess_axis_args!(plotattributes, letter)
return w
end
# special handling for Surface... need to properly unwrap and re-wrap
_apply_type_recipe(plotattributes, v::Surface{<:AMat{<:DataPoint}}) = v
function _apply_type_recipe(plotattributes, v::Surface)
ret = _apply_type_recipe(plotattributes, v.surf)
if typeof(ret) <: Formatted
Formatted(Surface(ret.data), ret.formatter)
else
Surface(ret.data)
end
end
# don't do anything for datapoints or nothing
_apply_type_recipe(plotattributes, v::AbstractArray{<:DataPoint}, letter) = v
_apply_type_recipe(plotattributes, v::Nothing, letter) = v
# axis args before type recipes should still be mapped to all axes
function _preprocess_axis_args!(plotattributes)
plt = plotattributes[:plot_object]
for (k, v) in plotattributes
if is_axis_attribute(plt, k)
pop!(plotattributes, k)
for l in (:x, :y, :z)
lk = Symbol(l, k)
haskey(plotattributes, lk) || (plotattributes[lk] = v)
end
end
end
end
function _preprocess_axis_args!(plotattributes, letter)
plotattributes[:letter] = letter
_preprocess_axis_args!(plotattributes)
end
# axis args in type recipes should only be applied to the current axis
function _postprocess_axis_args!(plotattributes, letter)
plt = plotattributes[:plot_object]
pop!(plotattributes, :letter)
if letter in (:x, :y, :z)
for (k, v) in plotattributes
if is_axis_attribute(plt, k)
pop!(plotattributes, k)
lk = Symbol(letter, k)
haskey(plotattributes, lk) || (plotattributes[lk] = v)
end
end
end
end

View File

@ -1,330 +0,0 @@
"""
_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)
still_to_process = _recipedata_vector(plt, plotattributes, args)
# for plotting recipes, swap out the args and update the parameter dictionary
# we are keeping a stack of series that still need to be processed.
# 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, 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 kw_list or
# pass it through apply_recipe 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)
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)
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
# --------------------------------
# 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 apply_type_recipe 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 f(d::AbstractDict) = collect(keys(d)), collect(values(d))
# 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.(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)
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
# 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]

View File

@ -1,220 +0,0 @@
const AVec = AbstractVector
const AMat = AbstractMatrix
const KW = Dict{Symbol, Any}
const AKW = AbstractDict{Symbol, Any}
# --------------------------------
# DefaultsDict
# --------------------------------
struct DefaultsDict <: AbstractDict{Symbol, Any}
explicit::KW
defaults::KW
end
function Base.getindex(dd::DefaultsDict, k)
return haskey(dd.explicit, k) ? dd.explicit[k] : dd.defaults[k]
end
Base.haskey(dd::DefaultsDict, k) = haskey(dd.explicit, k) || haskey(dd.defaults, k)
Base.get(dd::DefaultsDict, k, default) = haskey(dd, k) ? dd[k] : default
function Base.get!(dd::DefaultsDict, k, default)
v = if haskey(dd, k)
dd[k]
else
dd.defaults[k] = default
end
return v
end
function Base.delete!(dd::DefaultsDict, k)
haskey(dd.explicit, k) && delete!(dd.explicit, k)
haskey(dd.defaults, k) && delete!(dd.defaults, k)
end
Base.length(dd::DefaultsDict) = length(union(keys(dd.explicit), keys(dd.defaults)))
function Base.iterate(dd::DefaultsDict)
exp_keys = keys(dd.explicit)
def_keys = setdiff(keys(dd.defaults), exp_keys)
key_list = collect(Iterators.flatten((exp_keys, def_keys)))
iterate(dd, (key_list, 1))
end
function Base.iterate(dd::DefaultsDict, (key_list, i))
i > length(key_list) && return nothing
k = key_list[i]
(k => dd[k], (key_list, i + 1))
end
Base.copy(dd::DefaultsDict) = DefaultsDict(copy(dd.explicit), dd.defaults)
RecipesBase.is_explicit(dd::DefaultsDict, k) = haskey(dd.explicit, k)
isdefault(dd::DefaultsDict, k) = !is_explicit(dd, k) && haskey(dd.defaults, k)
Base.setindex!(dd::DefaultsDict, v, k) = dd.explicit[k] = v
# Reset to default value and return dict
reset_kw!(dd::DefaultsDict, k) = is_explicit(dd, k) ? delete!(dd.explicit, k) : dd
# Reset to default value and return old value
pop_kw!(dd::DefaultsDict, k) = is_explicit(dd, k) ? pop!(dd.explicit, k) : dd.defaults[k]
pop_kw!(dd::DefaultsDict, k, default) =
is_explicit(dd, k) ? pop!(dd.explicit, k) : get(dd.defaults, k, default)
# Fallbacks for dicts without defaults
reset_kw!(d::AKW, k) = delete!(d, k)
pop_kw!(d::AKW, k) = pop!(d, k)
pop_kw!(d::AKW, k, default) = pop!(d, k, default)
# --------------------------------
# 3D types
# --------------------------------
abstract type AbstractSurface end
"represents a contour or surface mesh"
struct Surface{M <: AMat} <: AbstractSurface
surf::M
end
Surface(f::Function, x, y) = Surface(Float64[f(xi, yi) for yi in y, xi in x])
Base.Array(surf::Surface) = surf.surf
for f in (:length, :size, :axes)
@eval Base.$f(surf::Surface, args...) = $f(surf.surf, args...)
end
Base.copy(surf::Surface) = Surface(copy(surf.surf))
Base.eltype(surf::Surface{T}) where {T} = eltype(T)
struct Volume{T}
v::Array{T, 3}
x_extents::Tuple{T, T}
y_extents::Tuple{T, T}
z_extents::Tuple{T, T}
end
default_extents(::Type{T}) where {T} = (zero(T), one(T))
function Volume(
v::Array{T, 3},
x_extents = default_extents(T),
y_extents = default_extents(T),
z_extents = default_extents(T),
) where {T}
Volume(v, x_extents, y_extents, z_extents)
end
Base.Array(vol::Volume) = vol.v
for f in (:length, :size)
@eval Base.$f(vol::Volume, args...) = $f(vol.v, args...)
end
Base.copy(vol::Volume{T}) where {T} =
Volume{T}(copy(vol.v), vol.x_extents, vol.y_extents, vol.z_extents)
Base.eltype(vol::Volume{T}) where {T} = T
# --------------------------------
# Formatting
# --------------------------------
"Represents data values with formatting that should apply to the tick labels."
struct Formatted{T}
data::T
formatter::Function
end
# -------------------------------
# 3D seriestypes
# -------------------------------
# TODO: Move to RecipesBase?
"""
is3d(::Type{Val{:myseriestype}})
Returns `true` if `myseriestype` represents a 3D series, `false` otherwise.
"""
is3d(st) = false
for st in (
:contour,
:contourf,
:contour3d,
:heatmap,
:image,
:path3d,
:scatter3d,
:surface,
:volume,
:wireframe,
)
@eval is3d(::Type{Val{Symbol($(string(st)))}}) = true
end
is3d(st::Symbol) = is3d(Val{st})
is3d(plt, stv::AbstractArray) = all(st -> is3d(plt, st), stv)
is3d(plotattributes::AbstractDict) = is3d(get(plotattributes, :seriestype, :path))
"""
is_surface(::Type{Val{:myseriestype}})
Returns `true` if `myseriestype` represents a surface series, `false` otherwise.
"""
is_surface(st) = false
for st in (:contour, :contourf, :contour3d, :image, :heatmap, :surface, :wireframe)
@eval is_surface(::Type{Val{Symbol($(string(st)))}}) = true
end
is_surface(st::Symbol) = is_surface(Val{st})
is_surface(plt, stv::AbstractArray) = all(st -> is_surface(plt, st), stv)
is_surface(plotattributes::AbstractDict) =
is_surface(get(plotattributes, :seriestype, :path))
"""
needs_3d_axes(::Type{Val{:myseriestype}})
Returns `true` if `myseriestype` needs 3d axes, `false` otherwise.
"""
needs_3d_axes(st) = false
for st in (
:contour3d,
:path3d,
:scatter3d,
:surface,
:volume,
:wireframe,
)
@eval needs_3d_axes(::Type{Val{Symbol($(string(st)))}}) = true
end
needs_3d_axes(st::Symbol) = needs_3d_axes(Val{st})
needs_3d_axes(plt, stv::AbstractArray) = all(st -> needs_3d_axes(plt, st), stv)
needs_3d_axes(plotattributes::AbstractDict) =
needs_3d_axes(get(plotattributes, :seriestype, :path))
# --------------------------------
# Scales
# --------------------------------
const SCALE_FUNCTIONS = Dict{Symbol, Function}(:log10 => log10, :log2 => log2, :ln => log)
const INVERSE_SCALE_FUNCTIONS =
Dict{Symbol, Function}(:log10 => exp10, :log2 => exp2, :ln => exp)
scale_func(scale::Symbol) = x -> get(SCALE_FUNCTIONS, scale, identity)(Float64(x))
inverse_scale_func(scale::Symbol) =
x -> get(INVERSE_SCALE_FUNCTIONS, scale, identity)(Float64(x))
# --------------------------------
# Unzip
# --------------------------------
for i in 2:4
@eval begin
unzip(v::AVec{<:Tuple{Vararg{T, $i} where T}}) =
$(Expr(:tuple, (:([t[$j] for t in v]) for j in 1:i)...))
end
end
# --------------------------------
# Map functions on vectors
# --------------------------------
_map_funcs(f::Function, u::AVec) = map(f, u)
_map_funcs(fs::AVec{F}, u::AVec) where {F <: Function} = [map(f, u) for f in fs]

View File

@ -142,7 +142,7 @@ function _add_smooth_kw(kw_list::Vector{KW}, kw::AKW)
end
RecipePipeline.get_axis_limits(plt::Plot, f, letter) = axis_limits(plt[1], :x)
RecipePipeline.get_axis_limits(plt::Plot, f, letter) = axis_limits(plt[1], letter)
## Plot recipes