Plots.jl/src/pipeline.jl
2020-03-15 17:54:46 +05:30

327 lines
11 KiB
Julia
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function finalize_subplot!(plt::Plot, st, plotattributes::AKW)
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)
end
# Override the RecipesPipeline `is_st_supported` for Plots.
is_st_supported(::Plot, st::Symbol) = is_seriestype_supported(st)
# ------------------------------------------------------------------
# preprocessing
function series_idx(kw_list::AVec{KW}, kw::AKW)
Int(kw[:series_plotindex]) - Int(kw_list[1][:series_plotindex]) + 1
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})
# 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,
# 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
for defdict in (_subplot_defaults,
_axis_defaults,
_axis_defaults_byletter[:x],
_axis_defaults_byletter[:y],
_axis_defaults_byletter[:z])
if haskey(defdict, k)
reset_kw!(plotattributes, k)
end
end
end
end
args
end
# ------------------------------------------------------------------
# user recipes
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)
warnOnUnsupported_scales(plt.backend, kw)
# add the plot index
plt.n += 1
kw[:series_plotindex] = plt.n
push!(kw_list, kw)
_add_errorbar_kw(kw_list, kw)
_add_smooth_kw(kw_list, kw)
return
end
function _preprocess_userrecipe(kw::AKW)
_add_markershape(kw)
# if there was a grouping, filter the data here
_filter_input_data!(kw)
# map marker_z if it's a Function
if isa(get(kw, :marker_z, nothing), Function)
# 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])
end
# map line_z if it's a 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])
end
# convert a ribbon into a fillrange
if get(kw, :ribbon, nothing) !== nothing
make_fillrange_from_ribbon(kw)
end
return
end
function _add_errorbar_kw(kw_list::Vector{KW}, kw::AKW)
# handle error bars by creating new recipedata data... these will have
# the same recipedata index as the recipedata they are copied from
for esym in (:xerror, :yerror)
if get(kw, esym, nothing) !== nothing
# we make a copy of the KW and apply an errorbar recipe
errkw = copy(kw)
errkw[:seriestype] = esym
errkw[:label] = ""
errkw[:primary] = false
push!(kw_list, errkw)
end
end
end
function _add_smooth_kw(kw_list::Vector{KW}, kw::AKW)
# handle smoothing by adding a new series
if get(kw, :smooth, false)
x, y = kw[:x], kw[:y]
β, α = convert(Matrix{Float64}, [x ones(length(x))]) \ convert(Vector{Float64}, y)
sx = [ignorenan_minimum(x), ignorenan_maximum(x)]
sy = β .* sx .+ α
push!(kw_list, merge(copy(kw), KW(
:seriestype => :path,
:x => sx,
:y => sy,
:fillrange => nothing,
:label => "",
:primary => false,
)))
end
end
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# setup plot and subplot
function _plot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
# merge in anything meant for the Plot
for kw in kw_list, (k,v) in kw
haskey(_plot_defaults, k) && (plotattributes[k] = pop!(kw, k))
end
# TODO: init subplots here
_update_plot_args(plt, plotattributes)
if !plt.init
plt.o = Base.invokelatest(_create_backend_figure, plt)
# create the layout and subplots from the inputs
plt.layout, plt.subplots, plt.spmap = build_layout(plt.attr)
for (idx,sp) in enumerate(plt.subplots)
sp.plt = plt
sp.attr[:subplot_index] = idx
end
plt.init = true
end
# handle inset subplots
insets = plt[:inset_subplots]
if insets !== nothing
if !(typeof(insets) <: AVec)
insets = [insets]
end
for inset in insets
parent, bb = is_2tuple(inset) ? inset : (nothing, inset)
P = typeof(parent)
if P <: Integer
parent = plt.subplots[parent]
elseif P == Symbol
parent = plt.spmap[parent]
else
parent = plt.layout
end
sp = Subplot(backend(), parent=parent)
sp.plt = plt
push!(plt.subplots, sp)
push!(plt.inset_subplots, sp)
sp.attr[:relative_bbox] = bb
sp.attr[:subplot_index] = length(plt.subplots)
end
end
plt[:inset_subplots] = nothing
end
function _subplot_setup(plt::Plot, plotattributes::AKW, kw_list::Vector{KW})
# we'll keep a map of subplot to an attribute override dict.
# Subplot/Axis attributes set by a user/series recipe apply only to the
# Subplot object which they belong to.
# TODO: allow matrices to still apply to all subplots
sp_attrs = Dict{Subplot,Any}()
for kw in kw_list
# get the Subplot object to which the series belongs.
sps = get(kw, :subplot, :auto)
sp = get_subplot(plt, _cycle(sps == :auto ? plt.subplots : plt.subplots[sps], series_idx(kw_list,kw)))
kw[:subplot] = sp
# extract subplot/axis attributes from kw and add to sp_attr
attr = KW()
for (k,v) in collect(kw)
if is_subplot_attr(k) || is_axis_attr(k)
attr[k] = pop!(kw, k)
end
if is_axis_attr_noletter(k)
v = pop!(kw, k)
for letter in (:x,:y,:z)
attr[Symbol(letter,k)] = v
end
end
for k in (:scale,), letter in (:x,:y,:z)
# Series recipes may need access to this information
lk = Symbol(letter,k)
if haskey(attr, lk)
kw[lk] = attr[lk]
end
end
end
sp_attrs[sp] = attr
end
# override subplot/axis args. `sp_attrs` take precendence
for (idx,sp) in enumerate(plt.subplots)
attr = if !haskey(plotattributes, :subplot) || plotattributes[:subplot] == idx
merge(plotattributes, get(sp_attrs, sp, KW()))
else
get(sp_attrs, sp, KW())
end
_update_subplot_args(plt, sp, attr, idx, false)
end
# do we need to link any axes together?
link_axes!(plt.layout, plt[:link])
end
# getting ready to add the series... last update to subplot from anything
# that might have been added during series recipes
function _prepare_subplot(plt::Plot{T}, plotattributes::AKW) where T
st::Symbol = plotattributes[:seriestype]
sp::Subplot{T} = plotattributes[:subplot]
sp_idx = get_subplot_index(plt, sp)
_update_subplot_args(plt, sp, plotattributes, sp_idx, true)
st = _override_seriestype_check(plotattributes, st)
# change to a 3d projection for this subplot?
if is3d(st)
sp.attr[:projection] = "3d"
end
# initialize now that we know the first series type
if !haskey(sp.attr, :init)
_initialize_subplot(plt, sp)
sp.attr[:init] = true
end
sp
end
# ------------------------------------------------------------------
# series types
function _override_seriestype_check(plotattributes::AKW, st::Symbol)
# do we want to override the series type?
if !is3d(st) && !(st in (:contour,:contour3d))
z = plotattributes[:z]
if !isa(z, Nothing) && (size(plotattributes[:x]) == size(plotattributes[:y]) == size(z))
st = (st == :scatter ? :scatter3d : :path3d)
plotattributes[:seriestype] = st
end
end
st
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)
# adjust extrema and discrete info
if st == :image
xmin, xmax = ignorenan_extrema(plotattributes[:x]); ymin, ymax = ignorenan_extrema(plotattributes[:y])
expand_extrema!(sp[:xaxis], (xmin, xmax))
expand_extrema!(sp[:yaxis], (ymin, ymax))
elseif !(st in (:pie, :histogram, :bins2d, :histogram2d))
expand_extrema!(sp, plotattributes)
end
# expand for zerolines (axes through origin)
if sp[:framestyle] in (:origin, :zerolines)
expand_extrema!(sp[:xaxis], 0.0)
expand_extrema!(sp[:yaxis], 0.0)
end
end
function _add_the_series(plt, sp, plotattributes)
warnOnUnsupported_args(plt.backend, plotattributes)
warnOnUnsupported(plt.backend, plotattributes)
series = Series(plotattributes)
push!(plt.series_list, series)
push!(sp.series_list, series)
_series_added(plt, series)
end