Compare commits
4 Commits
master
...
ds/pipelin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9df1ef0d4d | ||
|
|
e851dcded2 | ||
|
|
79f8402483 | ||
|
|
f86b324200 |
@ -44,7 +44,7 @@ PlotThemes = "1"
|
|||||||
PlotUtils = "0.6.5"
|
PlotUtils = "0.6.5"
|
||||||
RecipesBase = "0.8"
|
RecipesBase = "0.8"
|
||||||
Reexport = "0.2"
|
Reexport = "0.2"
|
||||||
Requires = "0.5, 1.0"
|
Requires = "0.5, 1"
|
||||||
Showoff = "0.3.1"
|
Showoff = "0.3.1"
|
||||||
StatsBase = "0.32"
|
StatsBase = "0.32"
|
||||||
julia = "1"
|
julia = "1"
|
||||||
@ -53,12 +53,12 @@ julia = "1"
|
|||||||
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
|
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
|
||||||
GeometryTypes = "4d00f742-c7ba-57c2-abde-4428a4b178cb"
|
GeometryTypes = "4d00f742-c7ba-57c2-abde-4428a4b178cb"
|
||||||
Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44"
|
Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44"
|
||||||
|
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
|
||||||
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
|
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
|
||||||
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
|
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
|
||||||
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
|
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
|
||||||
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
|
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
|
||||||
PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925"
|
PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925"
|
||||||
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
|
|
||||||
RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b"
|
RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b"
|
||||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||||
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
|
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
|
||||||
|
|||||||
19
src/Plots.jl
19
src/Plots.jl
@ -163,6 +163,24 @@ using .PlotMeasures
|
|||||||
import .PlotMeasures: Length, AbsoluteLength, Measure, width, height
|
import .PlotMeasures: Length, AbsoluteLength, Measure, width, height
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
include("RecipePipeline/RecipePipeline.jl")
|
||||||
|
import .RecipePipeline
|
||||||
|
import .RecipePipeline: 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
|
||||||
|
|
||||||
include("types.jl")
|
include("types.jl")
|
||||||
include("utils.jl")
|
include("utils.jl")
|
||||||
include("components.jl")
|
include("components.jl")
|
||||||
@ -171,7 +189,6 @@ include("args.jl")
|
|||||||
include("themes.jl")
|
include("themes.jl")
|
||||||
include("plot.jl")
|
include("plot.jl")
|
||||||
include("pipeline.jl")
|
include("pipeline.jl")
|
||||||
include("series.jl")
|
|
||||||
include("layouts.jl")
|
include("layouts.jl")
|
||||||
include("subplots.jl")
|
include("subplots.jl")
|
||||||
include("recipes.jl")
|
include("recipes.jl")
|
||||||
|
|||||||
97
src/RecipePipeline/RecipePipeline.jl
Normal file
97
src/RecipePipeline/RecipePipeline.jl
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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
|
||||||
142
src/RecipePipeline/api.jl
Normal file
142
src/RecipePipeline/api.jl
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
## 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
|
||||||
122
src/RecipePipeline/group.jl
Normal file
122
src/RecipePipeline/group.jl
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"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
|
||||||
46
src/RecipePipeline/plot_recipe.jl
Normal file
46
src/RecipePipeline/plot_recipe.jl
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
_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
|
||||||
170
src/RecipePipeline/series.jl
Normal file
170
src/RecipePipeline/series.jl
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
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
|
||||||
62
src/RecipePipeline/series_recipe.jl
Normal file
62
src/RecipePipeline/series_recipe.jl
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
_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
|
||||||
94
src/RecipePipeline/type_recipe.jl
Normal file
94
src/RecipePipeline/type_recipe.jl
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 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
|
||||||
330
src/RecipePipeline/user_recipe.jl
Normal file
330
src/RecipePipeline/user_recipe.jl
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
"""
|
||||||
|
_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]
|
||||||
220
src/RecipePipeline/utils.jl
Normal file
220
src/RecipePipeline/utils.jl
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
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]
|
||||||
77
src/args.jl
77
src/args.jl
@ -86,12 +86,9 @@ const _surface_like = [:contour, :contourf, :contour3d, :heatmap, :surface, :wir
|
|||||||
|
|
||||||
like_histogram(seriestype::Symbol) = seriestype in _histogram_like
|
like_histogram(seriestype::Symbol) = seriestype in _histogram_like
|
||||||
like_line(seriestype::Symbol) = seriestype in _line_like
|
like_line(seriestype::Symbol) = seriestype in _line_like
|
||||||
like_surface(seriestype::Symbol) = seriestype in _surface_like
|
like_surface(seriestype::Symbol) = is_surface(seriestype)
|
||||||
|
|
||||||
is3d(seriestype::Symbol) = seriestype in _3dTypes
|
|
||||||
is3d(series::Series) = is3d(series.plotattributes)
|
is3d(series::Series) = is3d(series.plotattributes)
|
||||||
is3d(plotattributes::AKW) = trueOrAllTrue(is3d, Symbol(plotattributes[:seriestype]))
|
|
||||||
|
|
||||||
is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d"
|
is3d(sp::Subplot) = string(sp.attr[:projection]) == "3d"
|
||||||
ispolar(sp::Subplot) = string(sp.attr[:projection]) == "polar"
|
ispolar(sp::Subplot) = string(sp.attr[:projection]) == "polar"
|
||||||
ispolar(series::Series) = ispolar(series.plotattributes[:subplot])
|
ispolar(series::Series) = ispolar(series.plotattributes[:subplot])
|
||||||
@ -682,7 +679,7 @@ end
|
|||||||
|
|
||||||
function default(; kw...)
|
function default(; kw...)
|
||||||
kw = KW(kw)
|
kw = KW(kw)
|
||||||
preprocessArgs!(kw)
|
preprocess_attributes!(kw)
|
||||||
for (k,v) in kw
|
for (k,v) in kw
|
||||||
default(k, v)
|
default(k, v)
|
||||||
end
|
end
|
||||||
@ -935,7 +932,7 @@ function _add_markershape(plotattributes::AKW)
|
|||||||
end
|
end
|
||||||
|
|
||||||
"Handle all preprocessing of args... break out colors/sizes/etc and replace aliases."
|
"Handle all preprocessing of args... break out colors/sizes/etc and replace aliases."
|
||||||
function preprocessArgs!(plotattributes::AKW)
|
function preprocess_attributes!(plotattributes::AKW)
|
||||||
replaceAliases!(plotattributes, _keyAliases)
|
replaceAliases!(plotattributes, _keyAliases)
|
||||||
|
|
||||||
# handle axis args common to all axis
|
# handle axis args common to all axis
|
||||||
@ -1112,75 +1109,13 @@ function preprocessArgs!(plotattributes::AKW)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
"A special type that will break up incoming data into groups, and allow for easier creation of grouped plots"
|
|
||||||
mutable struct GroupBy
|
|
||||||
groupLabels::Vector # length == numGroups
|
|
||||||
groupIds::Vector{Vector{Int}} # list of indices for each group
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# this is when given a vector-type of values to group by
|
|
||||||
function extractGroupArgs(v::AVec, args...; legendEntry = string)
|
|
||||||
groupLabels = sort(collect(unique(v)))
|
|
||||||
n = length(groupLabels)
|
|
||||||
if n > 100
|
|
||||||
@warn("You created n=$n groups... Is that intended?")
|
|
||||||
end
|
|
||||||
groupIds = Vector{Int}[filter(i -> v[i] == glab, eachindex(v)) for glab in groupLabels]
|
|
||||||
GroupBy(map(legendEntry, groupLabels), groupIds)
|
|
||||||
end
|
|
||||||
|
|
||||||
legendEntryFromTuple(ns::Tuple) = join(ns, ' ')
|
|
||||||
|
|
||||||
# this is when given a tuple of vectors of values to group by
|
|
||||||
function extractGroupArgs(vs::Tuple, args...)
|
|
||||||
isempty(vs) && return GroupBy([""], [axes(args[1],1)])
|
|
||||||
v = map(tuple, vs...)
|
|
||||||
extractGroupArgs(v, args...; legendEntry = legendEntryFromTuple)
|
|
||||||
end
|
|
||||||
|
|
||||||
# allow passing NamedTuples for a named legend entry
|
|
||||||
legendEntryFromTuple(ns::NamedTuple) =
|
|
||||||
join(["$k = $v" for (k, v) in pairs(ns)], ", ")
|
|
||||||
|
|
||||||
function extractGroupArgs(vs::NamedTuple, args...)
|
|
||||||
isempty(vs) && return GroupBy([""], [axes(args[1],1)])
|
|
||||||
v = map(NamedTuple{keys(vs)}∘tuple, values(vs)...)
|
|
||||||
extractGroupArgs(v, args...; legendEntry = legendEntryFromTuple)
|
|
||||||
end
|
|
||||||
|
|
||||||
# expecting a mapping of "group label" to "group indices"
|
|
||||||
function extractGroupArgs(idxmap::Dict{T,V}, args...) where {T, V<:AVec{Int}}
|
|
||||||
groupLabels = sortedkeys(idxmap)
|
|
||||||
groupIds = Vector{Int}[collect(idxmap[k]) for k in groupLabels]
|
|
||||||
GroupBy(groupLabels, groupIds)
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
const _already_warned = Dict{Symbol,Set{Symbol}}()
|
const _already_warned = Dict{Symbol,Set{Symbol}}()
|
||||||
const _to_warn = Set{Symbol}()
|
const _to_warn = Set{Symbol}()
|
||||||
|
|
||||||
function warnOnUnsupported_args(pkg::AbstractBackend, plotattributes)
|
function warn_on_unsupported_args(pkg::AbstractBackend, plotattributes)
|
||||||
empty!(_to_warn)
|
empty!(_to_warn)
|
||||||
bend = backend_name(pkg)
|
bend = backend_name(pkg)
|
||||||
already_warned = get!(_already_warned, bend, Set{Symbol}())
|
already_warned = get!(_already_warned, bend, Set{Symbol}())
|
||||||
@ -1204,7 +1139,7 @@ end
|
|||||||
# _markershape_supported(pkg::AbstractBackend, shape::Shape) = Shape in supported_markers(pkg)
|
# _markershape_supported(pkg::AbstractBackend, shape::Shape) = Shape in supported_markers(pkg)
|
||||||
# _markershape_supported(pkg::AbstractBackend, shapes::AVec) = all([_markershape_supported(pkg, shape) for shape in shapes])
|
# _markershape_supported(pkg::AbstractBackend, shapes::AVec) = all([_markershape_supported(pkg, shape) for shape in shapes])
|
||||||
|
|
||||||
function warnOnUnsupported(pkg::AbstractBackend, plotattributes)
|
function warn_on_unsupported(pkg::AbstractBackend, plotattributes)
|
||||||
if !is_seriestype_supported(pkg, plotattributes[:seriestype])
|
if !is_seriestype_supported(pkg, plotattributes[:seriestype])
|
||||||
@warn("seriestype $(plotattributes[:seriestype]) is unsupported with $pkg. Choose from: $(supported_seriestypes(pkg))")
|
@warn("seriestype $(plotattributes[:seriestype]) is unsupported with $pkg. Choose from: $(supported_seriestypes(pkg))")
|
||||||
end
|
end
|
||||||
@ -1216,7 +1151,7 @@ function warnOnUnsupported(pkg::AbstractBackend, plotattributes)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function warnOnUnsupported_scales(pkg::AbstractBackend, plotattributes::AKW)
|
function warn_on_unsupported_scales(pkg::AbstractBackend, plotattributes::AKW)
|
||||||
for k in (:xscale, :yscale, :zscale, :scale)
|
for k in (:xscale, :yscale, :zscale, :scale)
|
||||||
if haskey(plotattributes, k)
|
if haskey(plotattributes, k)
|
||||||
v = plotattributes[k]
|
v = plotattributes[k]
|
||||||
|
|||||||
56
src/axes.jl
56
src/axes.jl
@ -17,7 +17,7 @@ function Axis(sp::Subplot, letter::Symbol, args...; kw...)
|
|||||||
:show => true, # show or hide the axis? (useful for linked subplots)
|
:show => true, # show or hide the axis? (useful for linked subplots)
|
||||||
)
|
)
|
||||||
|
|
||||||
attr = Attr(explicit, _axis_defaults_byletter[letter])
|
attr = DefaultsDict(explicit, _axis_defaults_byletter[letter])
|
||||||
|
|
||||||
# update the defaults
|
# update the defaults
|
||||||
attr!(Axis([sp], attr), args...; kw...)
|
attr!(Axis([sp], attr), args...; kw...)
|
||||||
@ -85,7 +85,7 @@ function attr!(axis::Axis, args...; kw...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# then preprocess keyword arguments
|
# then preprocess keyword arguments
|
||||||
preprocessArgs!(KW(kw))
|
preprocess_attributes!(KW(kw))
|
||||||
|
|
||||||
# then override for any keywords... only those keywords that already exists in plotattributes
|
# then override for any keywords... only those keywords that already exists in plotattributes
|
||||||
for (k,v) in kw
|
for (k,v) in kw
|
||||||
@ -117,33 +117,11 @@ Base.setindex!(axis::Axis, v, ks::Symbol...) = setindex!(axis.plotattributes, v,
|
|||||||
Base.haskey(axis::Axis, k::Symbol) = haskey(axis.plotattributes, k)
|
Base.haskey(axis::Axis, k::Symbol) = haskey(axis.plotattributes, k)
|
||||||
ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax))
|
ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax))
|
||||||
|
|
||||||
|
|
||||||
const _scale_funcs = Dict{Symbol,Function}(
|
|
||||||
:log10 => log10,
|
|
||||||
:log2 => log2,
|
|
||||||
:ln => log,
|
|
||||||
)
|
|
||||||
const _inv_scale_funcs = Dict{Symbol,Function}(
|
|
||||||
:log10 => exp10,
|
|
||||||
:log2 => exp2,
|
|
||||||
:ln => exp,
|
|
||||||
)
|
|
||||||
|
|
||||||
# const _label_func = Dict{Symbol,Function}(
|
|
||||||
# :log10 => x -> "10^$x",
|
|
||||||
# :log2 => x -> "2^$x",
|
|
||||||
# :ln => x -> "e^$x",
|
|
||||||
# )
|
|
||||||
|
|
||||||
const _label_func = Dict{Symbol,Function}(
|
const _label_func = Dict{Symbol,Function}(
|
||||||
:log10 => x -> "10^$x",
|
:log10 => x -> "10^$x",
|
||||||
:log2 => x -> "2^$x",
|
:log2 => x -> "2^$x",
|
||||||
:ln => x -> "e^$x",
|
:ln => x -> "e^$x",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
scalefunc(scale::Symbol) = x -> get(_scale_funcs, scale, identity)(Float64(x))
|
|
||||||
invscalefunc(scale::Symbol) = x -> get(_inv_scale_funcs, scale, identity)(Float64(x))
|
|
||||||
labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string)
|
labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string)
|
||||||
|
|
||||||
function optimal_ticks_and_labels(sp::Subplot, axis::Axis, ticks = nothing)
|
function optimal_ticks_and_labels(sp::Subplot, axis::Axis, ticks = nothing)
|
||||||
@ -151,7 +129,7 @@ function optimal_ticks_and_labels(sp::Subplot, axis::Axis, ticks = nothing)
|
|||||||
|
|
||||||
# scale the limits
|
# scale the limits
|
||||||
scale = axis[:scale]
|
scale = axis[:scale]
|
||||||
sf = scalefunc(scale)
|
sf = scale_func(scale)
|
||||||
|
|
||||||
# If the axis input was a Date or DateTime use a special logic to find
|
# If the axis input was a Date or DateTime use a special logic to find
|
||||||
# "round" Date(Time)s as ticks
|
# "round" Date(Time)s as ticks
|
||||||
@ -196,11 +174,11 @@ function optimal_ticks_and_labels(sp::Subplot, axis::Axis, ticks = nothing)
|
|||||||
# chosen ticks is not too much bigger than amin - amax:
|
# chosen ticks is not too much bigger than amin - amax:
|
||||||
strict_span = false,
|
strict_span = false,
|
||||||
)
|
)
|
||||||
axis[:lims] = map(invscalefunc(scale), (viewmin, viewmax))
|
axis[:lims] = map(inverse_scale_func(scale), (viewmin, viewmax))
|
||||||
else
|
else
|
||||||
scaled_ticks = map(sf, (filter(t -> amin <= t <= amax, ticks)))
|
scaled_ticks = map(sf, (filter(t -> amin <= t <= amax, ticks)))
|
||||||
end
|
end
|
||||||
unscaled_ticks = map(invscalefunc(scale), scaled_ticks)
|
unscaled_ticks = map(inverse_scale_func(scale), scaled_ticks)
|
||||||
|
|
||||||
labels = if any(isfinite, unscaled_ticks)
|
labels = if any(isfinite, unscaled_ticks)
|
||||||
formatter = axis[:formatter]
|
formatter = axis[:formatter]
|
||||||
@ -400,7 +378,7 @@ function expand_extrema!(sp::Subplot, plotattributes::AKW)
|
|||||||
if fr === nothing && plotattributes[:seriestype] == :bar
|
if fr === nothing && plotattributes[:seriestype] == :bar
|
||||||
fr = 0.0
|
fr = 0.0
|
||||||
end
|
end
|
||||||
if fr !== nothing && !all3D(plotattributes)
|
if fr !== nothing && !is3d(plotattributes)
|
||||||
axis = sp.attr[vert ? :yaxis : :xaxis]
|
axis = sp.attr[vert ? :yaxis : :xaxis]
|
||||||
if typeof(fr) <: Tuple
|
if typeof(fr) <: Tuple
|
||||||
for fri in fr
|
for fri in fr
|
||||||
@ -445,7 +423,7 @@ end
|
|||||||
|
|
||||||
# push the limits out slightly
|
# push the limits out slightly
|
||||||
function widen(lmin, lmax, scale = :identity)
|
function widen(lmin, lmax, scale = :identity)
|
||||||
f, invf = scalefunc(scale), invscalefunc(scale)
|
f, invf = scale_func(scale), inverse_scale_func(scale)
|
||||||
span = f(lmax) - f(lmin)
|
span = f(lmax) - f(lmin)
|
||||||
# eps = NaNMath.max(1e-16, min(1e-2span, 1e-10))
|
# eps = NaNMath.max(1e-16, min(1e-2span, 1e-10))
|
||||||
eps = NaNMath.max(1e-16, 0.03span)
|
eps = NaNMath.max(1e-16, 0.03span)
|
||||||
@ -648,8 +626,8 @@ function axis_drawing_info(sp::Subplot)
|
|||||||
sp[:framestyle] in (:semi, :box) && push!(xborder_segs, (xmin, y2), (xmax, y2)) # top spine
|
sp[:framestyle] in (:semi, :box) && push!(xborder_segs, (xmin, y2), (xmax, y2)) # top spine
|
||||||
end
|
end
|
||||||
if !(xaxis[:ticks] in (:none, nothing, false))
|
if !(xaxis[:ticks] in (:none, nothing, false))
|
||||||
f = scalefunc(yaxis[:scale])
|
f = scale_func(yaxis[:scale])
|
||||||
invf = invscalefunc(yaxis[:scale])
|
invf = inverse_scale_func(yaxis[:scale])
|
||||||
tick_start, tick_stop = if sp[:framestyle] == :origin
|
tick_start, tick_stop = if sp[:framestyle] == :origin
|
||||||
t = invf(f(0) + 0.012 * (f(ymax) - f(ymin)))
|
t = invf(f(0) + 0.012 * (f(ymax) - f(ymin)))
|
||||||
(-t, t)
|
(-t, t)
|
||||||
@ -702,8 +680,8 @@ function axis_drawing_info(sp::Subplot)
|
|||||||
sp[:framestyle] in (:semi, :box) && push!(yborder_segs, (x2, ymin), (x2, ymax)) # right spine
|
sp[:framestyle] in (:semi, :box) && push!(yborder_segs, (x2, ymin), (x2, ymax)) # right spine
|
||||||
end
|
end
|
||||||
if !(yaxis[:ticks] in (:none, nothing, false))
|
if !(yaxis[:ticks] in (:none, nothing, false))
|
||||||
f = scalefunc(xaxis[:scale])
|
f = scale_func(xaxis[:scale])
|
||||||
invf = invscalefunc(xaxis[:scale])
|
invf = inverse_scale_func(xaxis[:scale])
|
||||||
tick_start, tick_stop = if sp[:framestyle] == :origin
|
tick_start, tick_stop = if sp[:framestyle] == :origin
|
||||||
t = invf(f(0) + 0.012 * (f(xmax) - f(xmin)))
|
t = invf(f(0) + 0.012 * (f(xmax) - f(xmin)))
|
||||||
(-t, t)
|
(-t, t)
|
||||||
@ -794,8 +772,8 @@ function axis_drawing_info_3d(sp::Subplot)
|
|||||||
sp[:framestyle] in (:semi, :box) && push!(xborder_segs, (xmin, y2, z2), (xmax, y2, z2)) # top spine
|
sp[:framestyle] in (:semi, :box) && push!(xborder_segs, (xmin, y2, z2), (xmax, y2, z2)) # top spine
|
||||||
end
|
end
|
||||||
if !(xaxis[:ticks] in (:none, nothing, false))
|
if !(xaxis[:ticks] in (:none, nothing, false))
|
||||||
f = scalefunc(yaxis[:scale])
|
f = scale_func(yaxis[:scale])
|
||||||
invf = invscalefunc(yaxis[:scale])
|
invf = inverse_scale_func(yaxis[:scale])
|
||||||
tick_start, tick_stop = if sp[:framestyle] == :origin
|
tick_start, tick_stop = if sp[:framestyle] == :origin
|
||||||
t = invf(f(0) + 0.012 * (f(ymax) - f(ymin)))
|
t = invf(f(0) + 0.012 * (f(ymax) - f(ymin)))
|
||||||
(-t, t)
|
(-t, t)
|
||||||
@ -869,8 +847,8 @@ function axis_drawing_info_3d(sp::Subplot)
|
|||||||
sp[:framestyle] in (:semi, :box) && push!(yborder_segs, (x2, ymin, z2), (x2, ymax, z2)) # right spine
|
sp[:framestyle] in (:semi, :box) && push!(yborder_segs, (x2, ymin, z2), (x2, ymax, z2)) # right spine
|
||||||
end
|
end
|
||||||
if !(yaxis[:ticks] in (:none, nothing, false))
|
if !(yaxis[:ticks] in (:none, nothing, false))
|
||||||
f = scalefunc(xaxis[:scale])
|
f = scale_func(xaxis[:scale])
|
||||||
invf = invscalefunc(xaxis[:scale])
|
invf = inverse_scale_func(xaxis[:scale])
|
||||||
tick_start, tick_stop = if sp[:framestyle] == :origin
|
tick_start, tick_stop = if sp[:framestyle] == :origin
|
||||||
t = invf(f(0) + 0.012 * (f(xmax) - f(xmin)))
|
t = invf(f(0) + 0.012 * (f(xmax) - f(xmin)))
|
||||||
(-t, t)
|
(-t, t)
|
||||||
@ -944,8 +922,8 @@ function axis_drawing_info_3d(sp::Subplot)
|
|||||||
sp[:framestyle] in (:semi, :box) && push!(zborder_segs, (x2, y2, zmin), (x2, y2, zmax))
|
sp[:framestyle] in (:semi, :box) && push!(zborder_segs, (x2, y2, zmin), (x2, y2, zmax))
|
||||||
end
|
end
|
||||||
if !(zaxis[:ticks] in (:none, nothing, false))
|
if !(zaxis[:ticks] in (:none, nothing, false))
|
||||||
f = scalefunc(xaxis[:scale])
|
f = scale_func(xaxis[:scale])
|
||||||
invf = invscalefunc(xaxis[:scale])
|
invf = inverse_scale_func(xaxis[:scale])
|
||||||
tick_start, tick_stop = if sp[:framestyle] == :origin
|
tick_start, tick_stop = if sp[:framestyle] == :origin
|
||||||
t = invf(f(0) + 0.012 * (f(ymax) - f(ymin)))
|
t = invf(f(0) + 0.012 * (f(ymax) - f(ymin)))
|
||||||
(-t, t)
|
(-t, t)
|
||||||
|
|||||||
@ -83,7 +83,7 @@ if length(HDF5PLOT_MAP_TELEM2STR) < 1
|
|||||||
"ARRAY" => Array, #Dict won't allow Array to be key in HDF5PLOT_MAP_TELEM2STR
|
"ARRAY" => Array, #Dict won't allow Array to be key in HDF5PLOT_MAP_TELEM2STR
|
||||||
|
|
||||||
#Sub-structure types:
|
#Sub-structure types:
|
||||||
"ATTR" => Attr,
|
"DEFAULTSDICT" => DefaultsDict,
|
||||||
"FONT" => Font,
|
"FONT" => Font,
|
||||||
"BOUNDINGBOX" => BoundingBox,
|
"BOUNDINGBOX" => BoundingBox,
|
||||||
"GRIDLAYOUT" => GridLayout,
|
"GRIDLAYOUT" => GridLayout,
|
||||||
@ -395,12 +395,12 @@ function _hdf5plot_write(grp, plotattributes::KW)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
function _hdf5plot_write(grp, plotattributes::Attr)
|
function _hdf5plot_write(grp, plotattributes::DefaultsDict)
|
||||||
for (k, v) in plotattributes
|
for (k, v) in plotattributes
|
||||||
kstr = string(k)
|
kstr = string(k)
|
||||||
_hdf5plot_gwrite(grp, kstr, v)
|
_hdf5plot_gwrite(grp, kstr, v)
|
||||||
end
|
end
|
||||||
_hdf5plot_writetype(grp, Attr)
|
_hdf5plot_writetype(grp, DefaultsDict)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@ -558,14 +558,14 @@ parent = RootLayout()
|
|||||||
|
|
||||||
return GridLayout(parent, minpad, bbox, grid, widths, heights, attr)
|
return GridLayout(parent, minpad, bbox, grid, widths, heights, attr)
|
||||||
end
|
end
|
||||||
function _hdf5plot_read(grp, T::Type{Attr})
|
function _hdf5plot_read(grp, T::Type{DefaultsDict})
|
||||||
attr = Attr(KW(), _plot_defaults)
|
attr = DefaultsDict(KW(), _plot_defaults)
|
||||||
v = _hdf5plot_read(grp, attr)
|
v = _hdf5plot_read(grp, attr)
|
||||||
return attr
|
return attr
|
||||||
end
|
end
|
||||||
function _hdf5plot_read(grp, k::String, T::Type{Axis})
|
function _hdf5plot_read(grp, k::String, T::Type{Axis})
|
||||||
grp = HDF5.g_open(grp, k)
|
grp = HDF5.g_open(grp, k)
|
||||||
plotattributes = Attr(KW(), _plot_defaults)
|
plotattributes = DefaultsDict(KW(), _plot_defaults)
|
||||||
_hdf5plot_read(grp, plotattributes)
|
_hdf5plot_read(grp, plotattributes)
|
||||||
return Axis([], plotattributes)
|
return Axis([], plotattributes)
|
||||||
end
|
end
|
||||||
@ -610,7 +610,7 @@ function _hdf5plot_readattr(grp, plotattributes::AbstractDict)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
_hdf5plot_read(grp, plotattributes::KW) = _hdf5plot_readattr(grp, plotattributes)
|
_hdf5plot_read(grp, plotattributes::KW) = _hdf5plot_readattr(grp, plotattributes)
|
||||||
_hdf5plot_read(grp, plotattributes::Attr) = _hdf5plot_readattr(grp, plotattributes)
|
_hdf5plot_read(grp, plotattributes::DefaultsDict) = _hdf5plot_readattr(grp, plotattributes)
|
||||||
|
|
||||||
# Read main plot structures:
|
# Read main plot structures:
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
@ -623,7 +623,7 @@ function _hdf5plot_read(sp::Subplot, subpath::String, f)
|
|||||||
|
|
||||||
for i in 1:nseries
|
for i in 1:nseries
|
||||||
grp = HDF5.g_open(f, _hdf5_plotelempath("$subpath/series_list/series$i"))
|
grp = HDF5.g_open(f, _hdf5_plotelempath("$subpath/series_list/series$i"))
|
||||||
seriesinfo = Attr(KW(), _plot_defaults)
|
seriesinfo = DefaultsDict(KW(), _plot_defaults)
|
||||||
_hdf5plot_read(grp, seriesinfo)
|
_hdf5plot_read(grp, seriesinfo)
|
||||||
plot!(sp, seriesinfo[:x], seriesinfo[:y]) #Add data & create data structures
|
plot!(sp, seriesinfo[:x], seriesinfo[:y]) #Add data & create data structures
|
||||||
_hdf5_merge!(sp.series_list[end].plotattributes, seriesinfo)
|
_hdf5_merge!(sp.series_list[end].plotattributes, seriesinfo)
|
||||||
@ -631,7 +631,7 @@ function _hdf5plot_read(sp::Subplot, subpath::String, f)
|
|||||||
|
|
||||||
#Perform after adding series... otherwise values get overwritten:
|
#Perform after adding series... otherwise values get overwritten:
|
||||||
grp = HDF5.g_open(f, _hdf5_plotelempath("$subpath/attr"))
|
grp = HDF5.g_open(f, _hdf5_plotelempath("$subpath/attr"))
|
||||||
attr = Attr(KW(), _plot_defaults)
|
attr = DefaultsDict(KW(), _plot_defaults)
|
||||||
_hdf5plot_read(grp, attr)
|
_hdf5plot_read(grp, attr)
|
||||||
_hdf5_merge!(sp.attr, attr)
|
_hdf5_merge!(sp.attr, attr)
|
||||||
|
|
||||||
|
|||||||
@ -652,23 +652,6 @@ end
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
function expand_extrema!(a::Axis, surf::Surface)
|
function expand_extrema!(a::Axis, surf::Surface)
|
||||||
ex = a[:extrema]
|
ex = a[:extrema]
|
||||||
for vi in surf.surf
|
for vi in surf.surf
|
||||||
@ -688,28 +671,7 @@ end
|
|||||||
# # I don't want to clash with ValidatedNumerics, but this would be nice:
|
# # I don't want to clash with ValidatedNumerics, but this would be nice:
|
||||||
# ..(a::T, b::T) = (a,b)
|
# ..(a::T, b::T) = (a,b)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
@ -773,14 +735,6 @@ function add_arrows(func::Function, x::AVec, y::AVec)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
"Represents data values with formatting that should apply to the tick labels."
|
|
||||||
struct Formatted{T}
|
|
||||||
data::T
|
|
||||||
formatter::Function
|
|
||||||
end
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
"create a BezierCurve for plotting"
|
"create a BezierCurve for plotting"
|
||||||
mutable struct BezierCurve{T <: GeometryTypes.Point}
|
mutable struct BezierCurve{T <: GeometryTypes.Point}
|
||||||
|
|||||||
359
src/pipeline.jl
359
src/pipeline.jl
@ -1,127 +1,74 @@
|
|||||||
# Error for aliases used in recipes
|
# RecipePipeline API
|
||||||
function warn_on_recipe_aliases!(plotattributes, recipe_type, args...)
|
|
||||||
|
## Warnings
|
||||||
|
|
||||||
|
function RecipePipeline.warn_on_recipe_aliases!(
|
||||||
|
plt::Plot,
|
||||||
|
plotattributes,
|
||||||
|
recipe_type,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
for k in keys(plotattributes)
|
for k in keys(plotattributes)
|
||||||
if !is_default_attribute(k)
|
if !is_default_attribute(k)
|
||||||
dk = get(_keyAliases, k, k)
|
dk = get(_keyAliases, k, k)
|
||||||
if k !== dk
|
if k !== dk
|
||||||
@warn "Attribute alias `$k` detected in the $recipe_type recipe defined for the signature $(signature_string(Val{recipe_type}, args...)). To ensure expected behavior it is recommended to use the default attribute `$dk`."
|
@warn "Attribute alias `$k` detected in the $recipe_type recipe defined for the signature $(_signature_string(Val{recipe_type}, args...)). To ensure expected behavior it is recommended to use the default attribute `$dk`."
|
||||||
end
|
end
|
||||||
plotattributes[dk] = pop_kw!(plotattributes, k)
|
plotattributes[dk] = pop_kw!(plotattributes, k)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
function warn_on_recipe_aliases!(v::AbstractVector, recipe_type, args...)
|
function RecipePipeline.warn_on_recipe_aliases!(
|
||||||
foreach(x -> warn_on_recipe_aliases!(x, recipe_type, args...), v)
|
plt::Plot,
|
||||||
|
v::AbstractVector,
|
||||||
|
recipe_type,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
foreach(x -> RecipePipeline.warn_on_recipe_aliases!(plt, x, recipe_type, args...), v)
|
||||||
end
|
end
|
||||||
function warn_on_recipe_aliases!(rd::RecipeData, recipe_type, args...)
|
function RecipePipeline.warn_on_recipe_aliases!(
|
||||||
warn_on_recipe_aliases!(rd.plotattributes, recipe_type, args...)
|
plt::Plot,
|
||||||
|
rd::RecipeData,
|
||||||
|
recipe_type,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
RecipePipeline.warn_on_recipe_aliases!(plt, rd.plotattributes, recipe_type, args...)
|
||||||
end
|
end
|
||||||
|
|
||||||
function signature_string(::Type{Val{:user}}, args...)
|
function _signature_string(::Type{Val{:user}}, args...)
|
||||||
return string("(::", join(string.(typeof.(args)), ", ::"), ")")
|
return string("(::", join(string.(typeof.(args)), ", ::"), ")")
|
||||||
end
|
end
|
||||||
signature_string(::Type{Val{:type}}, T) = "(::Type{$T}, ::$T)"
|
_signature_string(::Type{Val{:type}}, T) = "(::Type{$T}, ::$T)"
|
||||||
signature_string(::Type{Val{:plot}}, st) = "(::Type{Val{:$st}}, ::AbstractPlot)"
|
_signature_string(::Type{Val{:plot}}, st) = "(::Type{Val{:$st}}, ::AbstractPlot)"
|
||||||
signature_string(::Type{Val{:series}}, st) = "(::Type{Val{:$st}}, x, y, z)"
|
_signature_string(::Type{Val{:series}}, st) = "(::Type{Val{:$st}}, x, y, z)"
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# preprocessing
|
|
||||||
|
|
||||||
function series_idx(kw_list::AVec{KW}, kw::AKW)
|
## Grouping
|
||||||
Int(kw[:series_plotindex]) - Int(kw_list[1][:series_plotindex]) + 1
|
|
||||||
|
RecipePipeline.splittable_attribute(plt::Plot, key, val::SeriesAnnotations, len) =
|
||||||
|
RecipePipeline.splittable_attribute(plt, key, val.strs, len)
|
||||||
|
|
||||||
|
function RecipePipeline.split_attribute(plt::Plot, key, val::SeriesAnnotations, indices)
|
||||||
|
split_strs = _RecipePipeline.split_attribute(key, val.strs, indices)
|
||||||
|
return SeriesAnnotations(split_strs, val.font, val.baseshape, val.scalefactor)
|
||||||
end
|
end
|
||||||
|
|
||||||
function _expand_seriestype_array(plotattributes::AKW, 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 _preprocess_args(plotattributes::AKW, args, still_to_process::Vector{RecipeData})
|
## Preprocessing attributes
|
||||||
# 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 = (extractGroupArgs(plotattributes[:group], args...), args...)
|
|
||||||
end
|
|
||||||
|
|
||||||
# if we were passed a vector/matrix of seriestypes and there's more than one row,
|
RecipePipeline.preprocess_attributes!(plt::Plot, plotattributes) =
|
||||||
# we want to duplicate the inputs, once for each seriestype row.
|
preprocess_attributes!(plotattributes) # in src/args.jl
|
||||||
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
|
RecipePipeline.is_axis_attribute(plt::Plot, attr) = is_axis_attr_noletter(attr) # in src/args.jl
|
||||||
if !isempty(args)
|
|
||||||
for (k,v) in plotattributes
|
|
||||||
if k in _all_subplot_args || k in _all_axis_args
|
|
||||||
reset_kw!(plotattributes, k)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
args
|
RecipePipeline.is_subplot_attribute(plt::Plot, attr) = is_subplot_attr(attr) # in src/args.jl
|
||||||
end
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# user recipes
|
|
||||||
|
|
||||||
|
|
||||||
function _process_userrecipes(plt::Plot, plotattributes::AKW, args)
|
## User recipes
|
||||||
still_to_process = RecipeData[]
|
|
||||||
args = _preprocess_args(plotattributes, args, still_to_process)
|
|
||||||
|
|
||||||
# for plotting recipes, swap out the args and update the parameter dictionary
|
function RecipePipeline.process_userrecipe!(plt::Plot, kw_list, kw)
|
||||||
# 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)
|
|
||||||
_process_userrecipe(plt, kw_list, next_series)
|
|
||||||
else
|
|
||||||
rd_list = RecipesBase.apply_recipe(
|
|
||||||
next_series.plotattributes,
|
|
||||||
next_series.args...
|
|
||||||
)
|
|
||||||
warn_on_recipe_aliases!(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
|
|
||||||
|
|
||||||
function _process_userrecipe(plt::Plot, kw_list::Vector{KW}, recipedata::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
|
|
||||||
preprocessArgs!(kw)
|
|
||||||
_preprocess_userrecipe(kw)
|
_preprocess_userrecipe(kw)
|
||||||
warnOnUnsupported_scales(plt.backend, kw)
|
warn_on_unsupported_scales(plt.backend, kw)
|
||||||
|
|
||||||
# add the plot index
|
# add the plot index
|
||||||
plt.n += 1
|
plt.n += 1
|
||||||
kw[:series_plotindex] = plt.n
|
kw[:series_plotindex] = plt.n
|
||||||
@ -135,18 +82,17 @@ end
|
|||||||
function _preprocess_userrecipe(kw::AKW)
|
function _preprocess_userrecipe(kw::AKW)
|
||||||
_add_markershape(kw)
|
_add_markershape(kw)
|
||||||
|
|
||||||
# if there was a grouping, filter the data here
|
|
||||||
_filter_input_data!(kw)
|
|
||||||
|
|
||||||
# map marker_z if it's a Function
|
# map marker_z if it's a Function
|
||||||
if isa(get(kw, :marker_z, nothing), Function)
|
if isa(get(kw, :marker_z, nothing), Function)
|
||||||
# TODO: should this take y and/or z as arguments?
|
# TODO: should this take y and/or z as arguments?
|
||||||
kw[:marker_z] = isa(kw[:z], Nothing) ? map(kw[:marker_z], kw[:x], kw[:y]) : map(kw[:marker_z], kw[:x], kw[:y], kw[:z])
|
kw[:marker_z] = isa(kw[:z], Nothing) ? map(kw[:marker_z], kw[:x], kw[:y]) :
|
||||||
|
map(kw[:marker_z], kw[:x], kw[:y], kw[:z])
|
||||||
end
|
end
|
||||||
|
|
||||||
# map line_z if it's a Function
|
# map line_z if it's a Function
|
||||||
if isa(get(kw, :line_z, nothing), Function)
|
if isa(get(kw, :line_z, nothing), Function)
|
||||||
kw[:line_z] = isa(kw[:z], Nothing) ? map(kw[:line_z], kw[:x], kw[:y]) : map(kw[:line_z], kw[:x], kw[:y], kw[:z])
|
kw[:line_z] = isa(kw[:z], Nothing) ? map(kw[:line_z], kw[:x], kw[:y]) :
|
||||||
|
map(kw[:line_z], kw[:x], kw[:y], kw[:z])
|
||||||
end
|
end
|
||||||
|
|
||||||
# convert a ribbon into a fillrange
|
# convert a ribbon into a fillrange
|
||||||
@ -178,59 +124,43 @@ function _add_smooth_kw(kw_list::Vector{KW}, kw::AKW)
|
|||||||
β, α = convert(Matrix{Float64}, [x ones(length(x))]) \ convert(Vector{Float64}, y)
|
β, α = convert(Matrix{Float64}, [x ones(length(x))]) \ convert(Vector{Float64}, y)
|
||||||
sx = [ignorenan_minimum(x), ignorenan_maximum(x)]
|
sx = [ignorenan_minimum(x), ignorenan_maximum(x)]
|
||||||
sy = β .* sx .+ α
|
sy = β .* sx .+ α
|
||||||
push!(kw_list, merge(copy(kw), KW(
|
push!(
|
||||||
:seriestype => :path,
|
kw_list,
|
||||||
:x => sx,
|
merge(
|
||||||
:y => sy,
|
copy(kw),
|
||||||
:fillrange => nothing,
|
KW(
|
||||||
:label => "",
|
:seriestype => :path,
|
||||||
:primary => false,
|
:x => sx,
|
||||||
)))
|
:y => sy,
|
||||||
|
:fillrange => nothing,
|
||||||
|
:label => "",
|
||||||
|
:primary => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# plot recipes
|
|
||||||
|
|
||||||
# Grab the first in line to be processed and pass it through apply_recipe
|
RecipePipeline.get_axis_limits(plt::Plot, f, letter) = axis_limits(plt[1], :x)
|
||||||
# to generate a list of RecipeData objects (data + attributes).
|
|
||||||
# If we applied a "plot recipe" without error, then add the returned datalist's KWs,
|
|
||||||
# otherwise we just add the original KW.
|
## Plot recipes
|
||||||
function _process_plotrecipe(plt::Plot, kw::AKW, kw_list::Vector{KW}, still_to_process::Vector{KW})
|
|
||||||
if !isa(get(kw, :seriestype, nothing), Symbol)
|
RecipePipeline.type_alias(plt::Plot) = get(_typeAliases, st, st)
|
||||||
# seriestype was never set, or it's not a Symbol, so it can't be a plot recipe
|
|
||||||
push!(kw_list, kw)
|
|
||||||
return
|
## Plot setup
|
||||||
end
|
|
||||||
try
|
function RecipePipeline.plot_setup!(plt::Plot, plotattributes, kw_list)
|
||||||
st = kw[:seriestype]
|
_plot_setup(plt, plotattributes, kw_list)
|
||||||
st = kw[:seriestype] = get(_typeAliases, st, st)
|
_subplot_setup(plt, plotattributes, kw_list)
|
||||||
datalist = RecipesBase.apply_recipe(kw, Val{st}, plt)
|
|
||||||
warn_on_recipe_aliases!(datalist, :plot, st)
|
|
||||||
for data in datalist
|
|
||||||
preprocessArgs!(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
|
end
|
||||||
|
|
||||||
|
# TODO: Should some of this logic be moved to RecipePipeline?
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# setup plot and subplot
|
|
||||||
|
|
||||||
function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
|
function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
|
||||||
# merge in anything meant for the Plot
|
# merge in anything meant for the Plot
|
||||||
for kw in kw_list, (k,v) in kw
|
for kw in kw_list, (k, v) in kw
|
||||||
haskey(_plot_defaults, k) && (plotattributes[k] = pop!(kw, k))
|
haskey(_plot_defaults, k) && (plotattributes[k] = pop!(kw, k))
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -241,7 +171,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
|
|||||||
|
|
||||||
# create the layout and subplots from the inputs
|
# create the layout and subplots from the inputs
|
||||||
plt.layout, plt.subplots, plt.spmap = build_layout(plt.attr)
|
plt.layout, plt.subplots, plt.spmap = build_layout(plt.attr)
|
||||||
for (idx,sp) in enumerate(plt.subplots)
|
for (idx, sp) in enumerate(plt.subplots)
|
||||||
sp.plt = plt
|
sp.plt = plt
|
||||||
sp.attr[:subplot_index] = idx
|
sp.attr[:subplot_index] = idx
|
||||||
end
|
end
|
||||||
@ -266,7 +196,7 @@ function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
|
|||||||
else
|
else
|
||||||
parent = plt.layout
|
parent = plt.layout
|
||||||
end
|
end
|
||||||
sp = Subplot(backend(), parent=parent)
|
sp = Subplot(backend(), parent = parent)
|
||||||
sp.plt = plt
|
sp.plt = plt
|
||||||
push!(plt.subplots, sp)
|
push!(plt.subplots, sp)
|
||||||
push!(plt.inset_subplots, sp)
|
push!(plt.inset_subplots, sp)
|
||||||
@ -282,28 +212,34 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
|
|||||||
# Subplot/Axis attributes set by a user/series recipe apply only to the
|
# Subplot/Axis attributes set by a user/series recipe apply only to the
|
||||||
# Subplot object which they belong to.
|
# Subplot object which they belong to.
|
||||||
# TODO: allow matrices to still apply to all subplots
|
# TODO: allow matrices to still apply to all subplots
|
||||||
sp_attrs = Dict{Subplot,Any}()
|
sp_attrs = Dict{Subplot, Any}()
|
||||||
for kw in kw_list
|
for kw in kw_list
|
||||||
# get the Subplot object to which the series belongs.
|
# get the Subplot object to which the series belongs.
|
||||||
sps = get(kw, :subplot, :auto)
|
sps = get(kw, :subplot, :auto)
|
||||||
sp = get_subplot(plt, _cycle(sps == :auto ? plt.subplots : plt.subplots[sps], series_idx(kw_list,kw)))
|
sp = get_subplot(
|
||||||
|
plt,
|
||||||
|
_cycle(
|
||||||
|
sps == :auto ? plt.subplots : plt.subplots[sps],
|
||||||
|
series_idx(kw_list, kw),
|
||||||
|
),
|
||||||
|
)
|
||||||
kw[:subplot] = sp
|
kw[:subplot] = sp
|
||||||
|
|
||||||
# extract subplot/axis attributes from kw and add to sp_attr
|
# extract subplot/axis attributes from kw and add to sp_attr
|
||||||
attr = KW()
|
attr = KW()
|
||||||
for (k,v) in collect(kw)
|
for (k, v) in collect(kw)
|
||||||
if is_subplot_attr(k) || is_axis_attr(k)
|
if is_subplot_attr(k) || is_axis_attr(k)
|
||||||
attr[k] = pop!(kw, k)
|
attr[k] = pop!(kw, k)
|
||||||
end
|
end
|
||||||
if is_axis_attr_noletter(k)
|
if is_axis_attr_noletter(k)
|
||||||
v = pop!(kw, k)
|
v = pop!(kw, k)
|
||||||
for letter in (:x,:y,:z)
|
for letter in (:x, :y, :z)
|
||||||
attr[Symbol(letter,k)] = v
|
attr[Symbol(letter, k)] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for k in (:scale,), letter in (:x,:y,:z)
|
for k in (:scale,), letter in (:x, :y, :z)
|
||||||
# Series recipes may need access to this information
|
# Series recipes may need access to this information
|
||||||
lk = Symbol(letter,k)
|
lk = Symbol(letter, k)
|
||||||
if haskey(attr, lk)
|
if haskey(attr, lk)
|
||||||
kw[lk] = attr[lk]
|
kw[lk] = attr[lk]
|
||||||
end
|
end
|
||||||
@ -313,7 +249,7 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
|
|||||||
end
|
end
|
||||||
|
|
||||||
# override subplot/axis args. `sp_attrs` take precendence
|
# override subplot/axis args. `sp_attrs` take precendence
|
||||||
for (idx,sp) in enumerate(plt.subplots)
|
for (idx, sp) in enumerate(plt.subplots)
|
||||||
attr = if !haskey(plotattributes, :subplot) || plotattributes[:subplot] == idx
|
attr = if !haskey(plotattributes, :subplot) || plotattributes[:subplot] == idx
|
||||||
merge(plotattributes, get(sp_attrs, sp, KW()))
|
merge(plotattributes, get(sp_attrs, sp, KW()))
|
||||||
else
|
else
|
||||||
@ -326,9 +262,34 @@ function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
|
|||||||
link_axes!(plt.layout, plt[:link])
|
link_axes!(plt.layout, plt[:link])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function series_idx(kw_list::AVec{KW}, kw::AKW)
|
||||||
|
Int(kw[:series_plotindex]) - Int(kw_list[1][:series_plotindex]) + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
## Series recipes
|
||||||
|
|
||||||
|
function RecipePipeline.slice_series_attributes!(plt::Plot, kw_list, kw)
|
||||||
|
sp::Subplot = kw[:subplot]
|
||||||
|
# in series attributes given as vector with one element per series,
|
||||||
|
# select the value for current series
|
||||||
|
_slice_series_args!(kw, plt, sp, series_idx(kw_list, kw))
|
||||||
|
end
|
||||||
|
|
||||||
|
RecipePipeline.series_defaults(plt::Plot) = _series_defaults # in args.jl
|
||||||
|
|
||||||
|
RecipePipeline.is_seriestype_supported(plt::Plot, st) = is_seriestype_supported(st)
|
||||||
|
|
||||||
|
function RecipePipeline.add_series!(plt::Plot, plotattributes)
|
||||||
|
sp = _prepare_subplot(plt, plotattributes)
|
||||||
|
_expand_subplot_extrema(sp, plotattributes, plotattributes[:seriestype])
|
||||||
|
_update_series_attributes!(plotattributes, plt, sp)
|
||||||
|
_add_the_series(plt, sp, plotattributes)
|
||||||
|
end
|
||||||
|
|
||||||
# getting ready to add the series... last update to subplot from anything
|
# getting ready to add the series... last update to subplot from anything
|
||||||
# that might have been added during series recipes
|
# that might have been added during series recipes
|
||||||
function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where T
|
function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where {T}
|
||||||
st::Symbol = plotattributes[:seriestype]
|
st::Symbol = plotattributes[:seriestype]
|
||||||
sp::Subplot{T} = plotattributes[:subplot]
|
sp::Subplot{T} = plotattributes[:subplot]
|
||||||
sp_idx = get_subplot_index(plt, sp)
|
sp_idx = get_subplot_index(plt, sp)
|
||||||
@ -337,7 +298,7 @@ function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where T
|
|||||||
st = _override_seriestype_check(plotattributes, st)
|
st = _override_seriestype_check(plotattributes, st)
|
||||||
|
|
||||||
# change to a 3d projection for this subplot?
|
# change to a 3d projection for this subplot?
|
||||||
if is3d(st)
|
if needs_3d_axes(st)
|
||||||
sp.attr[:projection] = "3d"
|
sp.attr[:projection] = "3d"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -349,14 +310,12 @@ function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where T
|
|||||||
sp
|
sp
|
||||||
end
|
end
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# series types
|
|
||||||
|
|
||||||
function _override_seriestype_check(plotattributes::AKW, st::Symbol)
|
function _override_seriestype_check(plotattributes::AKW, st::Symbol)
|
||||||
# do we want to override the series type?
|
# do we want to override the series type?
|
||||||
if !is3d(st) && !(st in (:contour,:contour3d))
|
if !is3d(st) && !(st in (:contour, :contour3d))
|
||||||
z = plotattributes[:z]
|
z = plotattributes[:z]
|
||||||
if !isa(z, Nothing) && (size(plotattributes[:x]) == size(plotattributes[:y]) == size(z))
|
if !isa(z, Nothing) &&
|
||||||
|
(size(plotattributes[:x]) == size(plotattributes[:y]) == size(z))
|
||||||
st = (st == :scatter ? :scatter3d : :path3d)
|
st = (st == :scatter ? :scatter3d : :path3d)
|
||||||
plotattributes[:seriestype] = st
|
plotattributes[:seriestype] = st
|
||||||
end
|
end
|
||||||
@ -364,27 +323,11 @@ function _override_seriestype_check(plotattributes::AKW, st::Symbol)
|
|||||||
st
|
st
|
||||||
end
|
end
|
||||||
|
|
||||||
function _prepare_annotations(sp::Subplot, plotattributes::AKW)
|
|
||||||
# strip out series annotations (those which are based on series x/y coords)
|
|
||||||
# and add them to the subplot attr
|
|
||||||
sp_anns = annotations(sp[:annotations])
|
|
||||||
# series_anns = annotations(pop!(plotattributes, :series_annotations, []))
|
|
||||||
# if isa(series_anns, SeriesAnnotations)
|
|
||||||
# series_anns.x = plotattributes[:x]
|
|
||||||
# series_anns.y = plotattributes[:y]
|
|
||||||
# elseif length(series_anns) > 0
|
|
||||||
# x, y = plotattributes[:x], plotattributes[:y]
|
|
||||||
# nx, ny, na = map(length, (x,y,series_anns))
|
|
||||||
# n = max(nx, ny, na)
|
|
||||||
# series_anns = [(x[mod1(i,nx)], y[mod1(i,ny)], text(series_anns[mod1(i,na)])) for i=1:n]
|
|
||||||
# end
|
|
||||||
# sp.attr[:annotations] = vcat(sp_anns, series_anns)
|
|
||||||
end
|
|
||||||
|
|
||||||
function _expand_subplot_extrema(sp::Subplot, plotattributes::AKW, st::Symbol)
|
function _expand_subplot_extrema(sp::Subplot, plotattributes::AKW, st::Symbol)
|
||||||
# adjust extrema and discrete info
|
# adjust extrema and discrete info
|
||||||
if st == :image
|
if st == :image
|
||||||
xmin, xmax = ignorenan_extrema(plotattributes[:x]); ymin, ymax = ignorenan_extrema(plotattributes[:y])
|
xmin, xmax = ignorenan_extrema(plotattributes[:x])
|
||||||
|
ymin, ymax = ignorenan_extrema(plotattributes[:y])
|
||||||
expand_extrema!(sp[:xaxis], (xmin, xmax))
|
expand_extrema!(sp[:xaxis], (xmin, xmax))
|
||||||
expand_extrema!(sp[:yaxis], (ymin, ymax))
|
expand_extrema!(sp[:yaxis], (ymin, ymax))
|
||||||
elseif !(st in (:pie, :histogram, :bins2d, :histogram2d))
|
elseif !(st in (:pie, :histogram, :bins2d, :histogram2d))
|
||||||
@ -398,56 +341,10 @@ function _expand_subplot_extrema(sp::Subplot, plotattributes::AKW, st::Symbol)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function _add_the_series(plt, sp, plotattributes)
|
function _add_the_series(plt, sp, plotattributes)
|
||||||
warnOnUnsupported_args(plt.backend, plotattributes)
|
warn_on_unsupported_args(plt.backend, plotattributes)
|
||||||
warnOnUnsupported(plt.backend, plotattributes)
|
warn_on_unsupported(plt.backend, plotattributes)
|
||||||
series = Series(plotattributes)
|
series = Series(plotattributes)
|
||||||
push!(plt.series_list, series)
|
push!(plt.series_list, series)
|
||||||
push!(sp.series_list, series)
|
push!(sp.series_list, series)
|
||||||
_series_added(plt, series)
|
_series_added(plt, series)
|
||||||
end
|
end
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# this method recursively applies series recipes when the seriestype is not supported
|
|
||||||
# natively by the backend
|
|
||||||
function _process_seriesrecipe(plt::Plot, plotattributes::AKW)
|
|
||||||
#println("process $(typeof(plotattributes))")
|
|
||||||
# replace seriestype aliases
|
|
||||||
st = Symbol(plotattributes[:seriestype])
|
|
||||||
st = plotattributes[:seriestype] = get(_typeAliases, st, 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(st)
|
|
||||||
sp = _prepare_subplot(plt, plotattributes)
|
|
||||||
_prepare_annotations(sp, plotattributes)
|
|
||||||
_expand_subplot_extrema(sp, plotattributes, st)
|
|
||||||
_update_series_attributes!(plotattributes, plt, sp)
|
|
||||||
_add_the_series(plt, sp, 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!(datalist, :series, st)
|
|
||||||
|
|
||||||
# assuming there was no error, recursively apply the series recipes
|
|
||||||
for data in datalist
|
|
||||||
if isa(data, RecipeData)
|
|
||||||
preprocessArgs!(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
|
|
||||||
|
|||||||
88
src/plot.jl
88
src/plot.jl
@ -49,7 +49,7 @@ as a String to look up its docstring; e.g. `plotattr("seriestype")`.
|
|||||||
function plot(args...; kw...)
|
function plot(args...; kw...)
|
||||||
# this creates a new plot with args/kw and sets it to be the current plot
|
# this creates a new plot with args/kw and sets it to be the current plot
|
||||||
plotattributes = KW(kw)
|
plotattributes = KW(kw)
|
||||||
preprocessArgs!(plotattributes)
|
preprocess_attributes!(plotattributes)
|
||||||
|
|
||||||
# create an empty Plot then process
|
# create an empty Plot then process
|
||||||
plt = Plot()
|
plt = Plot()
|
||||||
@ -61,7 +61,7 @@ end
|
|||||||
# note: we split into plt1 and plts_tail so we can dispatch correctly
|
# note: we split into plt1 and plts_tail so we can dispatch correctly
|
||||||
function plot(plt1::Plot, plts_tail::Plot...; kw...)
|
function plot(plt1::Plot, plts_tail::Plot...; kw...)
|
||||||
plotattributes = KW(kw)
|
plotattributes = KW(kw)
|
||||||
preprocessArgs!(plotattributes)
|
preprocess_attributes!(plotattributes)
|
||||||
|
|
||||||
# build our plot vector from the args
|
# build our plot vector from the args
|
||||||
n = length(plts_tail) + 1
|
n = length(plts_tail) + 1
|
||||||
@ -153,7 +153,7 @@ end
|
|||||||
# this adds to a specific plot... most plot commands will flow through here
|
# this adds to a specific plot... most plot commands will flow through here
|
||||||
function plot!(plt::Plot, args...; kw...)
|
function plot!(plt::Plot, args...; kw...)
|
||||||
plotattributes = KW(kw)
|
plotattributes = KW(kw)
|
||||||
preprocessArgs!(plotattributes)
|
preprocess_attributes!(plotattributes)
|
||||||
# merge!(plt.user_attr, plotattributes)
|
# merge!(plt.user_attr, plotattributes)
|
||||||
_plot!(plt, plotattributes, args)
|
_plot!(plt, plotattributes, args)
|
||||||
end
|
end
|
||||||
@ -163,87 +163,11 @@ end
|
|||||||
# this is the core plotting function. recursively apply recipes to build
|
# this is the core plotting function. recursively apply recipes to build
|
||||||
# a list of series KW dicts.
|
# a list of series KW dicts.
|
||||||
# note: at entry, we only have those preprocessed args which were passed in... no default values yet
|
# note: at entry, we only have those preprocessed args which were passed in... no default values yet
|
||||||
function _plot!(plt::Plot, plotattributes::AKW, args::Tuple)
|
function _plot!(plt::Plot, plotattributes, args)
|
||||||
plotattributes[:plot_object] = plt
|
RecipePipeline.recipe_pipeline!(plt, plotattributes, args)
|
||||||
|
|
||||||
if !isempty(args) && !isdefined(Main, :StatsPlots) &&
|
|
||||||
first(split(string(typeof(args[1])), ".")) == "DataFrames"
|
|
||||||
@warn("You're trying to plot a DataFrame, but this functionality is provided by StatsPlots")
|
|
||||||
end
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
# "USER RECIPES"
|
|
||||||
# --------------------------------
|
|
||||||
|
|
||||||
kw_list = _process_userrecipes(plt, plotattributes, args)
|
|
||||||
|
|
||||||
# @info(1)
|
|
||||||
# map(DD, kw_list)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
# "PLOT RECIPES"
|
|
||||||
# --------------------------------
|
|
||||||
|
|
||||||
# "plot recipe", which 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.
|
|
||||||
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
|
|
||||||
|
|
||||||
# @info(2)
|
|
||||||
# map(DD, kw_list)
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
# Plot/Subplot/Layout setup
|
|
||||||
# --------------------------------
|
|
||||||
_plot_setup(plt, plotattributes, kw_list)
|
|
||||||
_subplot_setup(plt, plotattributes, kw_list)
|
|
||||||
|
|
||||||
# !!! note: 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"
|
|
||||||
# --------------------------------
|
|
||||||
|
|
||||||
# @info(3)
|
|
||||||
# map(DD, kw_list)
|
|
||||||
|
|
||||||
for kw in kw_list
|
|
||||||
sp::Subplot = kw[:subplot]
|
|
||||||
|
|
||||||
# in series attributes given as vector with one element per series,
|
|
||||||
# select the value for current series
|
|
||||||
_slice_series_args!(kw, plt, sp, series_idx(kw_list,kw))
|
|
||||||
|
|
||||||
|
|
||||||
series_attr = Attr(kw, _series_defaults)
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
|
|
||||||
current(plt)
|
current(plt)
|
||||||
|
|
||||||
# do we want to force display?
|
|
||||||
# if plt[:show]
|
|
||||||
# gui(plt)
|
|
||||||
# end
|
|
||||||
_do_plot_show(plt, plt[:show])
|
_do_plot_show(plt, plt[:show])
|
||||||
|
return plt
|
||||||
plt
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
114
src/recipes.jl
114
src/recipes.jl
@ -457,6 +457,8 @@ end
|
|||||||
()
|
()
|
||||||
end
|
end
|
||||||
@deps plots_heatmap shape
|
@deps plots_heatmap shape
|
||||||
|
is_3d(::Type{Val{:plots_heatmap}}) = true
|
||||||
|
is_surface(::Type{Val{:plots_heatmap}}) = true
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Histograms
|
# Histograms
|
||||||
@ -1165,6 +1167,118 @@ end
|
|||||||
@deps quiver shape path
|
@deps quiver shape path
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# 1 argument
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
# images - grays
|
||||||
|
function clamp_greys!(mat::AMat{<:Gray})
|
||||||
|
for i in eachindex(mat)
|
||||||
|
mat[i].val < 0 && (mat[i] = Gray(0))
|
||||||
|
mat[i].val > 1 && (mat[i] = Gray(1))
|
||||||
|
end
|
||||||
|
mat
|
||||||
|
end
|
||||||
|
|
||||||
|
@recipe function f(mat::AMat{<:Gray})
|
||||||
|
n, m = axes(mat)
|
||||||
|
if is_seriestype_supported(:image)
|
||||||
|
seriestype := :image
|
||||||
|
yflip --> true
|
||||||
|
SliceIt, m, n, Surface(clamp_greys!(mat))
|
||||||
|
else
|
||||||
|
seriestype := :heatmap
|
||||||
|
yflip --> true
|
||||||
|
cbar --> false
|
||||||
|
fillcolor --> ColorGradient([:black, :white])
|
||||||
|
SliceIt, m, n, Surface(clamp!(convert(Matrix{Float64}, mat), 0.0, 1.0))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# images - colors
|
||||||
|
@recipe function f(mat::AMat{T}) where {T <: Colorant}
|
||||||
|
n, m = axes(mat)
|
||||||
|
|
||||||
|
if is_seriestype_supported(:image)
|
||||||
|
seriestype := :image
|
||||||
|
yflip --> true
|
||||||
|
SliceIt, m, n, Surface(mat)
|
||||||
|
else
|
||||||
|
seriestype := :heatmap
|
||||||
|
yflip --> true
|
||||||
|
cbar --> false
|
||||||
|
aspect_ratio --> :equal
|
||||||
|
z, plotattributes[:fillcolor] = replace_image_with_heatmap(mat)
|
||||||
|
SliceIt, m, n, Surface(z)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# plotting arbitrary shapes/polygons
|
||||||
|
|
||||||
|
@recipe function f(shape::Shape)
|
||||||
|
seriestype --> :shape
|
||||||
|
coords(shape)
|
||||||
|
end
|
||||||
|
|
||||||
|
@recipe function f(shapes::AVec{Shape})
|
||||||
|
seriestype --> :shape
|
||||||
|
coords(shapes)
|
||||||
|
end
|
||||||
|
|
||||||
|
@recipe function f(shapes::AMat{Shape})
|
||||||
|
seriestype --> :shape
|
||||||
|
for j in axes(shapes, 2)
|
||||||
|
@series coords(vec(shapes[:, j]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# 3 arguments
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
|
# images - grays
|
||||||
|
@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where {T <: Gray}
|
||||||
|
if is_seriestype_supported(:image)
|
||||||
|
seriestype := :image
|
||||||
|
yflip --> true
|
||||||
|
SliceIt, x, y, Surface(mat)
|
||||||
|
else
|
||||||
|
seriestype := :heatmap
|
||||||
|
yflip --> true
|
||||||
|
cbar --> false
|
||||||
|
fillcolor --> ColorGradient([:black, :white])
|
||||||
|
SliceIt, x, y, Surface(convert(Matrix{Float64}, mat))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# images - colors
|
||||||
|
@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where {T <: Colorant}
|
||||||
|
if is_seriestype_supported(:image)
|
||||||
|
seriestype := :image
|
||||||
|
yflip --> true
|
||||||
|
SliceIt, x, y, Surface(mat)
|
||||||
|
else
|
||||||
|
seriestype := :heatmap
|
||||||
|
yflip --> true
|
||||||
|
cbar --> false
|
||||||
|
z, plotattributes[:fillcolor] = replace_image_with_heatmap(mat)
|
||||||
|
SliceIt, x, y, Surface(z)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Lists of tuples and GeometryTypes.Points
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
@recipe f(v::AVec{<:GeometryTypes.Point}) = unzip(v)
|
||||||
|
@recipe f(p::GeometryTypes.Point) = [p]
|
||||||
|
|
||||||
|
# Special case for 4-tuples in :ohlc series
|
||||||
|
@recipe f(xyuv::AVec{<:Tuple{R1, R2, R3, R4}}) where {R1, R2, R3, R4} =
|
||||||
|
get(plotattributes, :seriestype, :path) == :ohlc ? OHLC[OHLC(t...) for t in xyuv] :
|
||||||
|
unzip(xyuv)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
# TODO: move OHLC to PlotRecipes finance.jl
|
# TODO: move OHLC to PlotRecipes finance.jl
|
||||||
|
|||||||
651
src/series.jl
651
src/series.jl
@ -1,651 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# create a new "build_series_args" which converts all inputs into xs = Any[xitems], ys = Any[yitems].
|
|
||||||
# Special handling for: no args, xmin/xmax, parametric, dataframes
|
|
||||||
# Then once inputs have been converted, build the series args, map functions, etc.
|
|
||||||
# This should cut down on boilerplate code and allow more focused dispatch on type
|
|
||||||
# note: returns meta information... mainly for use with automatic labeling from DataFrames for now
|
|
||||||
|
|
||||||
const FuncOrFuncs{F} = Union{F, Vector{F}, Matrix{F}}
|
|
||||||
const MaybeNumber = Union{Number, Missing}
|
|
||||||
const MaybeString = Union{AbstractString, Missing}
|
|
||||||
const DataPoint = Union{MaybeNumber, MaybeString}
|
|
||||||
|
|
||||||
prepareSeriesData(x) = error("Cannot convert $(typeof(x)) to series data for plotting")
|
|
||||||
prepareSeriesData(::Nothing) = nothing
|
|
||||||
prepareSeriesData(t::Tuple{T, T}) where {T<:Number} = t
|
|
||||||
prepareSeriesData(f::Function) = f
|
|
||||||
prepareSeriesData(ar::AbstractRange{<:Number}) = ar
|
|
||||||
function prepareSeriesData(a::AbstractArray{<:MaybeNumber})
|
|
||||||
f = isimmutable(a) ? replace : replace!
|
|
||||||
a = f(x -> ismissing(x) || isinf(x) ? NaN : x, map(float, a))
|
|
||||||
end
|
|
||||||
prepareSeriesData(a::AbstractArray{<:Missing}) = fill(NaN, axes(a))
|
|
||||||
prepareSeriesData(a::AbstractArray{<:MaybeString}) = replace(x -> ismissing(x) ? "" : x, a)
|
|
||||||
prepareSeriesData(s::Surface{<:AMat{<:MaybeNumber}}) = Surface(prepareSeriesData(s.surf))
|
|
||||||
prepareSeriesData(s::Surface) = s # non-numeric Surface, such as an image
|
|
||||||
prepareSeriesData(v::Volume) = Volume(prepareSeriesData(v.v), v.x_extents, v.y_extents, v.z_extents)
|
|
||||||
|
|
||||||
# default: assume x represents a single series
|
|
||||||
series_vector(x, plotattributes) = [prepareSeriesData(x)]
|
|
||||||
|
|
||||||
# fixed number of blank series
|
|
||||||
series_vector(n::Integer, plotattributes) = [zeros(0) for i in 1:n]
|
|
||||||
|
|
||||||
# vector of data points is a single series
|
|
||||||
series_vector(v::AVec{<:DataPoint}, plotattributes) = [prepareSeriesData(v)]
|
|
||||||
|
|
||||||
# list of things (maybe other vectors, functions, or something else)
|
|
||||||
function series_vector(v::AVec, plotattributes)
|
|
||||||
if all(x -> x isa MaybeNumber, v)
|
|
||||||
series_vector(Vector{MaybeNumber}(v), plotattributes)
|
|
||||||
elseif all(x -> x isa MaybeString, v)
|
|
||||||
series_vector(Vector{MaybeString}(v), plotattributes)
|
|
||||||
else
|
|
||||||
vcat((series_vector(vi, plotattributes) for vi in v)...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Matrix is split into columns
|
|
||||||
function series_vector(v::AMat{<:DataPoint}, plotattributes)
|
|
||||||
if all3D(plotattributes)
|
|
||||||
[prepareSeriesData(Surface(v))]
|
|
||||||
else
|
|
||||||
[prepareSeriesData(v[:, i]) for i in axes(v, 2)]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# Fillranges & ribbons
|
|
||||||
|
|
||||||
|
|
||||||
process_fillrange(range::Number, plotattributes) = [range]
|
|
||||||
process_fillrange(range, plotattributes) = series_vector(range, plotattributes)
|
|
||||||
|
|
||||||
process_ribbon(ribbon::Number, plotattributes) = [ribbon]
|
|
||||||
process_ribbon(ribbon, plotattributes) = series_vector(ribbon, plotattributes)
|
|
||||||
# ribbon as a tuple: (lower_ribbons, upper_ribbons)
|
|
||||||
process_ribbon(ribbon::Tuple{S, T}, plotattributes) where {S, T} = collect(zip(
|
|
||||||
series_vector(ribbon[1], plotattributes),
|
|
||||||
series_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
|
|
||||||
|
|
||||||
# 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_vector(x, plotattributes)
|
|
||||||
ys = series_vector(y, plotattributes)
|
|
||||||
zs = series_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
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# Apply type recipes
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 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.")
|
|
||||||
|
|
||||||
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, :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)
|
|
||||||
_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!(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!(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)
|
|
||||||
for (k, v) in plotattributes
|
|
||||||
if is_axis_attr_noletter(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)
|
|
||||||
pop!(plotattributes, :letter)
|
|
||||||
if letter in (:x, :y, :z)
|
|
||||||
for (k, v) in plotattributes
|
|
||||||
if is_axis_attr_noletter(k)
|
|
||||||
pop!(plotattributes, k)
|
|
||||||
lk = Symbol(letter, k)
|
|
||||||
haskey(plotattributes, lk) || (plotattributes[lk] = v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# Fallback user recipes calling type recipes
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# 1 argument
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
|
|
||||||
@recipe f(n::Integer) = is3d(get(plotattributes,:seriestype,:path)) ? (SliceIt, n, n, n) : (SliceIt, n, n, nothing)
|
|
||||||
|
|
||||||
all3D(plotattributes) = trueOrAllTrue(
|
|
||||||
st -> st in (
|
|
||||||
:contour,
|
|
||||||
:contourf,
|
|
||||||
:heatmap,
|
|
||||||
:surface,
|
|
||||||
:wireframe,
|
|
||||||
:contour3d,
|
|
||||||
:image,
|
|
||||||
:plots_heatmap,
|
|
||||||
),
|
|
||||||
get(plotattributes, :seriestype, :none),
|
|
||||||
)
|
|
||||||
|
|
||||||
# return a surface if this is a 3d plot, otherwise let it be sliced up
|
|
||||||
@recipe function f(mat::AMat)
|
|
||||||
if all3D(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 all3D(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
|
|
||||||
|
|
||||||
|
|
||||||
# images - grays
|
|
||||||
function clamp_greys!(mat::AMat{<:Gray})
|
|
||||||
for i in eachindex(mat)
|
|
||||||
mat[i].val < 0 && (mat[i] = Gray(0))
|
|
||||||
mat[i].val > 1 && (mat[i] = Gray(1))
|
|
||||||
end
|
|
||||||
mat
|
|
||||||
end
|
|
||||||
|
|
||||||
@recipe function f(mat::AMat{<:Gray})
|
|
||||||
n, m = axes(mat)
|
|
||||||
if is_seriestype_supported(:image)
|
|
||||||
seriestype := :image
|
|
||||||
yflip --> true
|
|
||||||
SliceIt, m, n, Surface(clamp_greys!(mat))
|
|
||||||
else
|
|
||||||
seriestype := :heatmap
|
|
||||||
yflip --> true
|
|
||||||
cbar --> false
|
|
||||||
fillcolor --> ColorGradient([:black, :white])
|
|
||||||
SliceIt, m, n, Surface(clamp!(convert(Matrix{Float64}, mat), 0., 1.))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# images - colors
|
|
||||||
@recipe function f(mat::AMat{T}) where T<:Colorant
|
|
||||||
n, m = axes(mat)
|
|
||||||
|
|
||||||
if is_seriestype_supported(:image)
|
|
||||||
seriestype := :image
|
|
||||||
yflip --> true
|
|
||||||
SliceIt, m, n, Surface(mat)
|
|
||||||
else
|
|
||||||
seriestype := :heatmap
|
|
||||||
yflip --> true
|
|
||||||
cbar --> false
|
|
||||||
aspect_ratio --> :equal
|
|
||||||
z, plotattributes[:fillcolor] = replace_image_with_heatmap(mat)
|
|
||||||
SliceIt, m, n, Surface(z)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# plotting arbitrary shapes/polygons
|
|
||||||
|
|
||||||
@recipe function f(shape::Shape)
|
|
||||||
seriestype --> :shape
|
|
||||||
coords(shape)
|
|
||||||
end
|
|
||||||
|
|
||||||
@recipe function f(shapes::AVec{Shape})
|
|
||||||
seriestype --> :shape
|
|
||||||
coords(shapes)
|
|
||||||
end
|
|
||||||
|
|
||||||
@recipe function f(shapes::AMat{Shape})
|
|
||||||
seriestype --> :shape
|
|
||||||
for j in axes(shapes,2)
|
|
||||||
@series coords(vec(shapes[:,j]))
|
|
||||||
end
|
|
||||||
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
|
|
||||||
axis_limits(plt[1], :x)
|
|
||||||
catch
|
|
||||||
xinv = invscalefunc(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 !like_surface(get(plotattributes, :seriestype, :none))
|
|
||||||
plotattributes[:seriestype] = :contour
|
|
||||||
end
|
|
||||||
x, y, Surface(z)
|
|
||||||
end
|
|
||||||
|
|
||||||
# images - grays
|
|
||||||
@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where T<:Gray
|
|
||||||
if is_seriestype_supported(:image)
|
|
||||||
seriestype := :image
|
|
||||||
yflip --> true
|
|
||||||
SliceIt, x, y, Surface(mat)
|
|
||||||
else
|
|
||||||
seriestype := :heatmap
|
|
||||||
yflip --> true
|
|
||||||
cbar --> false
|
|
||||||
fillcolor --> ColorGradient([:black, :white])
|
|
||||||
SliceIt, x, y, Surface(convert(Matrix{Float64}, mat))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# images - colors
|
|
||||||
@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where T<:Colorant
|
|
||||||
if is_seriestype_supported(:image)
|
|
||||||
seriestype := :image
|
|
||||||
yflip --> true
|
|
||||||
SliceIt, x, y, Surface(mat)
|
|
||||||
else
|
|
||||||
seriestype := :heatmap
|
|
||||||
yflip --> true
|
|
||||||
cbar --> false
|
|
||||||
z, plotattributes[:fillcolor] = replace_image_with_heatmap(mat)
|
|
||||||
SliceIt, x, y, Surface(z)
|
|
||||||
end
|
|
||||||
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=(: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=(: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} = mapFuncOrFuncs(fx, u), mapFuncOrFuncs(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) = ((scalefunc(s),invscalefunc(s)) for s in (xscale,yscale))
|
|
||||||
xs, ys = 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}
|
|
||||||
mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u), mapFuncOrFuncs(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
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# Lists of tuples and GeometryTypes.Points
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
|
|
||||||
@recipe f(v::AVec{<:Tuple}) = unzip(v)
|
|
||||||
@recipe f(v::AVec{<:GeometryTypes.Point}) = unzip(v)
|
|
||||||
@recipe f(tup::Tuple) = [tup]
|
|
||||||
@recipe f(p::GeometryTypes.Point) = [p]
|
|
||||||
|
|
||||||
# Special case for 4-tuples in :ohlc series
|
|
||||||
@recipe f(xyuv::AVec{<:Tuple{R1,R2,R3,R4}}) where {R1,R2,R3,R4} = get(plotattributes,:seriestype,:path)==:ohlc ? OHLC[OHLC(t...) for t in xyuv] : unzip(xyuv)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# handle grouping
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
|
|
||||||
splittable_kw(key, val, lengthGroup) = false
|
|
||||||
splittable_kw(key, val::AbstractArray, lengthGroup) = !(key in (:group, :color_palette)) && length(axes(val,1)) == lengthGroup
|
|
||||||
splittable_kw(key, val::Tuple, lengthGroup) = all(splittable_kw.(key, val, lengthGroup))
|
|
||||||
splittable_kw(key, val::SeriesAnnotations, lengthGroup) = splittable_kw(key, val.strs, lengthGroup)
|
|
||||||
|
|
||||||
split_kw(key, val::AbstractArray, indices) = val[indices, fill(Colon(), ndims(val)-1)...]
|
|
||||||
split_kw(key, val::Tuple, indices) = Tuple(split_kw(key, v, indices) for v in val)
|
|
||||||
function split_kw(key, val::SeriesAnnotations, indices)
|
|
||||||
split_strs = split_kw(key, val.strs, indices)
|
|
||||||
return SeriesAnnotations(split_strs, val.font, val.baseshape, val.scalefactor)
|
|
||||||
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.groupLabels))
|
|
||||||
fill!(y_mat, def_val)
|
|
||||||
for i in eachindex(groupby.groupLabels)
|
|
||||||
xi = x[groupby.groupIds[i]]
|
|
||||||
yi = y[groupby.groupIds[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...)
|
|
||||||
lengthGroup = maximum(union(groupby.groupIds...))
|
|
||||||
if !(group_as_matrix(args[1]))
|
|
||||||
for (i,glab) in enumerate(groupby.groupLabels)
|
|
||||||
@series begin
|
|
||||||
label --> string(glab)
|
|
||||||
idxfilter --> groupby.groupIds[i]
|
|
||||||
for (key,val) in plotattributes
|
|
||||||
if splittable_kw(key, val, lengthGroup)
|
|
||||||
:($key) := split_kw(key, val, groupby.groupIds[i])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
args
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
g = args[1]
|
|
||||||
if length(g.args) == 1
|
|
||||||
x = zeros(Int, lengthGroup)
|
|
||||||
for indexes in groupby.groupIds
|
|
||||||
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, lengthGroup)
|
|
||||||
:($key) := groupedvec2mat(x_ind, x, val, groupby)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
label --> reshape(groupby.groupLabels, 1, :)
|
|
||||||
typeof(g)((x_u, (groupedvec2mat(x_ind, x, arg, groupby, NaN) for arg in last_args)...))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -7,7 +7,7 @@ function Subplot(::T; parent = RootLayout()) where T<:AbstractBackend
|
|||||||
(20mm, 5mm, 2mm, 10mm),
|
(20mm, 5mm, 2mm, 10mm),
|
||||||
defaultbox,
|
defaultbox,
|
||||||
defaultbox,
|
defaultbox,
|
||||||
Attr(KW(), _subplot_defaults),
|
DefaultsDict(KW(), _subplot_defaults),
|
||||||
nothing,
|
nothing,
|
||||||
nothing
|
nothing
|
||||||
)
|
)
|
||||||
|
|||||||
65
src/types.jl
65
src/types.jl
@ -18,65 +18,10 @@ end
|
|||||||
wrap(obj::T) where {T} = InputWrapper{T}(obj)
|
wrap(obj::T) where {T} = InputWrapper{T}(obj)
|
||||||
Base.isempty(wrapper::InputWrapper) = false
|
Base.isempty(wrapper::InputWrapper) = false
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------
|
|
||||||
|
|
||||||
struct Attr <: AbstractDict{Symbol,Any}
|
|
||||||
explicit::KW
|
|
||||||
defaults::KW
|
|
||||||
end
|
|
||||||
|
|
||||||
function Base.getindex(attr::Attr, k)
|
|
||||||
return haskey(attr.explicit, k) ? attr.explicit[k] : attr.defaults[k]
|
|
||||||
end
|
|
||||||
Base.haskey(attr::Attr, k) = haskey(attr.explicit,k) || haskey(attr.defaults,k)
|
|
||||||
Base.get(attr::Attr, k, default) = haskey(attr, k) ? attr[k] : default
|
|
||||||
function Base.get!(attr::Attr, k, default)
|
|
||||||
v = if haskey(attr, k)
|
|
||||||
attr[k]
|
|
||||||
else
|
|
||||||
attr.defaults[k] = default
|
|
||||||
end
|
|
||||||
return v
|
|
||||||
end
|
|
||||||
function Base.delete!(attr::Attr, k)
|
|
||||||
haskey(attr.explicit, k) && delete!(attr.explicit, k)
|
|
||||||
haskey(attr.defaults, k) && delete!(attr.defaults, k)
|
|
||||||
end
|
|
||||||
Base.length(attr::Attr) = length(union(keys(attr.explicit), keys(attr.defaults)))
|
|
||||||
function Base.iterate(attr::Attr)
|
|
||||||
exp_keys = keys(attr.explicit)
|
|
||||||
def_keys = setdiff(keys(attr.defaults), exp_keys)
|
|
||||||
key_list = collect(Iterators.flatten((exp_keys, def_keys)))
|
|
||||||
iterate(attr, (key_list, 1))
|
|
||||||
end
|
|
||||||
function Base.iterate(attr::Attr, (key_list, i))
|
|
||||||
i > length(key_list) && return nothing
|
|
||||||
k = key_list[i]
|
|
||||||
(k=>attr[k], (key_list, i+1))
|
|
||||||
end
|
|
||||||
|
|
||||||
Base.copy(attr::Attr) = Attr(copy(attr.explicit), attr.defaults)
|
|
||||||
|
|
||||||
RecipesBase.is_explicit(attr::Attr, k) = haskey(attr.explicit,k)
|
|
||||||
isdefault(attr::Attr, k) = !is_explicit(attr,k) && haskey(attr.defaults,k)
|
|
||||||
|
|
||||||
Base.setindex!(attr::Attr, v, k) = attr.explicit[k] = v
|
|
||||||
|
|
||||||
# Reset to default value and return dict
|
|
||||||
reset_kw!(attr::Attr, k) = is_explicit(attr, k) ? delete!(attr.explicit, k) : attr
|
|
||||||
# Reset to default value and return old value
|
|
||||||
pop_kw!(attr::Attr, k) = is_explicit(attr, k) ? pop!(attr.explicit, k) : attr.defaults[k]
|
|
||||||
pop_kw!(attr::Attr, k, default) = is_explicit(attr, k) ? pop!(attr.explicit, k) : get(attr.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)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------
|
# -----------------------------------------------------------
|
||||||
|
|
||||||
mutable struct Series
|
mutable struct Series
|
||||||
plotattributes::Attr
|
plotattributes::DefaultsDict
|
||||||
end
|
end
|
||||||
|
|
||||||
attr(series::Series, k::Symbol) = series.plotattributes[k]
|
attr(series::Series, k::Symbol) = series.plotattributes[k]
|
||||||
@ -91,7 +36,7 @@ mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout
|
|||||||
minpad::Tuple # leftpad, toppad, rightpad, bottompad
|
minpad::Tuple # leftpad, toppad, rightpad, bottompad
|
||||||
bbox::BoundingBox # the canvas area which is available to this subplot
|
bbox::BoundingBox # the canvas area which is available to this subplot
|
||||||
plotarea::BoundingBox # the part where the data goes
|
plotarea::BoundingBox # the part where the data goes
|
||||||
attr::Attr # args specific to this subplot
|
attr::DefaultsDict # args specific to this subplot
|
||||||
o # can store backend-specific data... like a pyplot ax
|
o # can store backend-specific data... like a pyplot ax
|
||||||
plt # the enclosing Plot object (can't give it a type because of no forward declarations)
|
plt # the enclosing Plot object (can't give it a type because of no forward declarations)
|
||||||
end
|
end
|
||||||
@ -103,7 +48,7 @@ Base.show(io::IO, sp::Subplot) = print(io, "Subplot{$(sp[:subplot_index])}")
|
|||||||
# simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place
|
# simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place
|
||||||
mutable struct Axis
|
mutable struct Axis
|
||||||
sps::Vector{Subplot}
|
sps::Vector{Subplot}
|
||||||
plotattributes::Attr
|
plotattributes::DefaultsDict
|
||||||
end
|
end
|
||||||
|
|
||||||
mutable struct Extrema
|
mutable struct Extrema
|
||||||
@ -122,7 +67,7 @@ const SubplotMap = Dict{Any, Subplot}
|
|||||||
mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T}
|
mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T}
|
||||||
backend::T # the backend type
|
backend::T # the backend type
|
||||||
n::Int # number of series
|
n::Int # number of series
|
||||||
attr::Attr # arguments for the whole plot
|
attr::DefaultsDict # arguments for the whole plot
|
||||||
series_list::Vector{Series} # arguments for each series
|
series_list::Vector{Series} # arguments for each series
|
||||||
o # the backend's plot object
|
o # the backend's plot object
|
||||||
subplots::Vector{Subplot}
|
subplots::Vector{Subplot}
|
||||||
@ -133,7 +78,7 @@ mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T}
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Plot()
|
function Plot()
|
||||||
Plot(backend(), 0, Attr(KW(), _plot_defaults), Series[], nothing,
|
Plot(backend(), 0, DefaultsDict(KW(), _plot_defaults), Series[], nothing,
|
||||||
Subplot[], SubplotMap(), EmptyLayout(),
|
Subplot[], SubplotMap(), EmptyLayout(),
|
||||||
Subplot[], false)
|
Subplot[], false)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -143,9 +143,6 @@ makevec(v::T) where {T} = T[v]
|
|||||||
maketuple(x::Real) = (x,x)
|
maketuple(x::Real) = (x,x)
|
||||||
maketuple(x::Tuple{T,S}) where {T,S} = x
|
maketuple(x::Tuple{T,S}) where {T,S} = x
|
||||||
|
|
||||||
mapFuncOrFuncs(f::Function, u::AVec) = map(f, u)
|
|
||||||
mapFuncOrFuncs(fs::AVec{F}, u::AVec) where {F<:Function} = [map(f, u) for f in fs]
|
|
||||||
|
|
||||||
for i in 2:4
|
for i in 2:4
|
||||||
@eval begin
|
@eval begin
|
||||||
unzip(v::Union{AVec{<:Tuple{Vararg{T,$i} where T}},
|
unzip(v::Union{AVec{<:Tuple{Vararg{T,$i} where T}},
|
||||||
@ -229,7 +226,7 @@ end
|
|||||||
|
|
||||||
"create an (n+1) list of the outsides of heatmap rectangles"
|
"create an (n+1) list of the outsides of heatmap rectangles"
|
||||||
function heatmap_edges(v::AVec, scale::Symbol = :identity, isedges::Bool = false)
|
function heatmap_edges(v::AVec, scale::Symbol = :identity, isedges::Bool = false)
|
||||||
f, invf = scalefunc(scale), invscalefunc(scale)
|
f, invf = scale_func(scale), inverse_scale_func(scale)
|
||||||
map(invf, _heatmap_edges(map(f,v), isedges))
|
map(invf, _heatmap_edges(map(f,v), isedges))
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -838,7 +835,7 @@ end
|
|||||||
|
|
||||||
function attr!(series::Series; kw...)
|
function attr!(series::Series; kw...)
|
||||||
plotattributes = KW(kw)
|
plotattributes = KW(kw)
|
||||||
preprocessArgs!(plotattributes)
|
preprocess_attributes!(plotattributes)
|
||||||
for (k,v) in plotattributes
|
for (k,v) in plotattributes
|
||||||
if haskey(_series_defaults, k)
|
if haskey(_series_defaults, k)
|
||||||
series[k] = v
|
series[k] = v
|
||||||
@ -852,7 +849,7 @@ end
|
|||||||
|
|
||||||
function attr!(sp::Subplot; kw...)
|
function attr!(sp::Subplot; kw...)
|
||||||
plotattributes = KW(kw)
|
plotattributes = KW(kw)
|
||||||
preprocessArgs!(plotattributes)
|
preprocess_attributes!(plotattributes)
|
||||||
for (k,v) in plotattributes
|
for (k,v) in plotattributes
|
||||||
if haskey(_subplot_defaults, k)
|
if haskey(_subplot_defaults, k)
|
||||||
sp[k] = v
|
sp[k] = v
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user