# RecipesPipeline API ## Warnings function RecipesPipeline.warn_on_recipe_aliases!( plt::Plot, plotattributes::AKW, recipe_type, signature_string ) for k in keys(plotattributes) if !is_default_attribute(k) dk = get(_keyAliases, k, k) if k !== dk @warn "Attribute alias `$k` detected in the $recipe_type recipe defined for the signature $signature_string. To ensure expected behavior it is recommended to use the default attribute `$dk`." end plotattributes[dk] = RecipesPipeline.pop_kw!(plotattributes, k) end end end ## Grouping RecipesPipeline.splittable_attribute(plt::Plot, key, val::SeriesAnnotations, len) = RecipesPipeline.splittable_attribute(plt, key, val.strs, len) function RecipesPipeline.split_attribute(plt::Plot, key, val::SeriesAnnotations, indices) split_strs = RecipesPipeline.split_attribute(plt, key, val.strs, indices) return SeriesAnnotations(split_strs, val.font, val.baseshape, val.scalefactor) end ## Preprocessing attributes function RecipesPipeline.preprocess_axis_args!(plt::Plot, plotattributes, letter) # Fix letter for seriestypes that are x only but data gets passed as y if treats_y_as_x(get(plotattributes, :seriestype, :path)) if get(plotattributes, :orientation, :vertical) == :vertical letter = :x end end plotattributes[:letter] = letter RecipesPipeline.preprocess_axis_args!(plt, plotattributes) end RecipesPipeline.preprocess_attributes!(plt::Plot, plotattributes) = RecipesPipeline.preprocess_attributes!(plotattributes) # in src/args.jl RecipesPipeline.is_axis_attribute(plt::Plot, attr) = is_axis_attr_noletter(attr) # in src/args.jl RecipesPipeline.is_subplot_attribute(plt::Plot, attr) = is_subplot_attr(attr) # in src/args.jl ## User recipes function RecipesPipeline.process_userrecipe!(plt::Plot, kw_list, kw) _preprocess_userrecipe(kw) warn_on_unsupported_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) # 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, :zerror) 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 RecipesPipeline.get_axis_limits(plt::Plot, f, letter) = axis_limits(plt[1], letter) ## Plot recipes RecipesPipeline.type_alias(plt::Plot) = get(_typeAliases, st, st) ## Plot setup function RecipesPipeline.plot_setup!(plt::Plot, plotattributes, kw_list) _plot_setup(plt, plotattributes, kw_list) _subplot_setup(plt, plotattributes, kw_list) return nothing end function RecipesPipeline.process_sliced_series_attributes!(plt::Plots.Plot, kw_list) # swap errors err_inds = findall(kw -> get(kw, :seriestype, :path) in (:xerror, :yerror, :zerror), kw_list) for ind in err_inds if get(kw_list[ind-1],:seriestype,:path) == :scatter tmp = copy(kw_list[ind]) kw_list[ind] = copy(kw_list[ind-1]) kw_list[ind-1] = tmp end end return nothing end # TODO: Should some of this logic be moved to RecipesPipeline? 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 function series_idx(kw_list::AVec{KW}, kw::AKW) Int(kw[:series_plotindex]) - Int(kw_list[1][:series_plotindex]) + 1 end ## Series recipes function RecipesPipeline.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)) return nothing end RecipesPipeline.series_defaults(plt::Plot) = _series_defaults # in args.jl RecipesPipeline.is_seriestype_supported(plt::Plot, st) = is_seriestype_supported(st) function RecipesPipeline.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 # 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 RecipesPipeline.needs_3d_axes(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 function _override_seriestype_check(plotattributes::AKW, st::Symbol) # do we want to override the series type? if !RecipesPipeline.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 _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 (: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) extra_kwargs = warn_on_unsupported_args(plt.backend, plotattributes) if (kw = plt[:extra_kwargs]) isa AbstractDict plt[:extra_plot_kwargs] = get(kw,:plot,KW()) sp[:extra_kwargs] = get(kw,:subplot, KW()) plotattributes[:extra_kwargs] = get(kw,:series,KW()) elseif plt[:extra_kwargs] == :plot plt[:extra_plot_kwargs] = extra_kwargs elseif plt[:extra_kwargs] == :subplot sp[:extra_kwargs] = extra_kwargs elseif plt[:extra_kwargs] == :series plotattributes[:extra_kwargs] = extra_kwargs else ArgumentError("Unsupported type for extra keyword arguments") end warn_on_unsupported(plt.backend, plotattributes) series = Series(plotattributes) push!(plt.series_list, series) push!(sp.series_list, series) _series_added(plt, series) end