From 80ec6f03b4198dfa2991a63fef679f88bdfd1b83 Mon Sep 17 00:00:00 2001 From: Daniel Schwabeneder Date: Sun, 5 Apr 2020 11:58:07 +0200 Subject: [PATCH] depend on RecipePipeline --- Project.toml | 1 + src/RecipePipeline/RecipePipeline.jl | 97 -------- src/RecipePipeline/api.jl | 142 ------------ src/RecipePipeline/group.jl | 122 ---------- src/RecipePipeline/plot_recipe.jl | 46 ---- src/RecipePipeline/series.jl | 170 -------------- src/RecipePipeline/series_recipe.jl | 62 ----- src/RecipePipeline/type_recipe.jl | 94 -------- src/RecipePipeline/user_recipe.jl | 330 --------------------------- src/RecipePipeline/utils.jl | 220 ------------------ src/pipeline.jl | 2 +- 11 files changed, 2 insertions(+), 1284 deletions(-) delete mode 100644 src/RecipePipeline/RecipePipeline.jl delete mode 100644 src/RecipePipeline/api.jl delete mode 100644 src/RecipePipeline/group.jl delete mode 100644 src/RecipePipeline/plot_recipe.jl delete mode 100644 src/RecipePipeline/series.jl delete mode 100644 src/RecipePipeline/series_recipe.jl delete mode 100644 src/RecipePipeline/type_recipe.jl delete mode 100644 src/RecipePipeline/user_recipe.jl delete mode 100644 src/RecipePipeline/utils.jl diff --git a/Project.toml b/Project.toml index 0f4fb1d8..f6ab3ef4 100644 --- a/Project.toml +++ b/Project.toml @@ -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" diff --git a/src/RecipePipeline/RecipePipeline.jl b/src/RecipePipeline/RecipePipeline.jl deleted file mode 100644 index 03cc24e6..00000000 --- a/src/RecipePipeline/RecipePipeline.jl +++ /dev/null @@ -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 diff --git a/src/RecipePipeline/api.jl b/src/RecipePipeline/api.jl deleted file mode 100644 index feb9350a..00000000 --- a/src/RecipePipeline/api.jl +++ /dev/null @@ -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 diff --git a/src/RecipePipeline/group.jl b/src/RecipePipeline/group.jl deleted file mode 100644 index ca9d46c5..00000000 --- a/src/RecipePipeline/group.jl +++ /dev/null @@ -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 diff --git a/src/RecipePipeline/plot_recipe.jl b/src/RecipePipeline/plot_recipe.jl deleted file mode 100644 index 6b7e05c3..00000000 --- a/src/RecipePipeline/plot_recipe.jl +++ /dev/null @@ -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 diff --git a/src/RecipePipeline/series.jl b/src/RecipePipeline/series.jl deleted file mode 100644 index e967fa4c..00000000 --- a/src/RecipePipeline/series.jl +++ /dev/null @@ -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 diff --git a/src/RecipePipeline/series_recipe.jl b/src/RecipePipeline/series_recipe.jl deleted file mode 100644 index 37bb6a4c..00000000 --- a/src/RecipePipeline/series_recipe.jl +++ /dev/null @@ -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 diff --git a/src/RecipePipeline/type_recipe.jl b/src/RecipePipeline/type_recipe.jl deleted file mode 100644 index 527fbae6..00000000 --- a/src/RecipePipeline/type_recipe.jl +++ /dev/null @@ -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 diff --git a/src/RecipePipeline/user_recipe.jl b/src/RecipePipeline/user_recipe.jl deleted file mode 100644 index 0023474f..00000000 --- a/src/RecipePipeline/user_recipe.jl +++ /dev/null @@ -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] diff --git a/src/RecipePipeline/utils.jl b/src/RecipePipeline/utils.jl deleted file mode 100644 index 54862d47..00000000 --- a/src/RecipePipeline/utils.jl +++ /dev/null @@ -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] diff --git a/src/pipeline.jl b/src/pipeline.jl index d64958b4..445075b4 100644 --- a/src/pipeline.jl +++ b/src/pipeline.jl @@ -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