reorg of update_subplot_args; add back recipe, DataFrames; random type stability fixes

This commit is contained in:
Thomas Breloff 2016-07-10 18:12:49 -04:00
parent 32c1c31139
commit e3e2a354e7
6 changed files with 156 additions and 201 deletions

View File

@ -141,7 +141,7 @@ include("plot.jl")
include("series.jl")
include("layouts.jl")
include("subplots.jl")
# include("recipes.jl")
include("recipes.jl")
include("animation.jl")
include("output.jl")
include("examples.jl")

View File

@ -814,9 +814,9 @@ slice_arg(v, idx) = v
# given an argument key (k), we want to extract the argument value for this index.
# matrices are sliced by column, otherwise we
# if nothing is set (or container is empty), return the default or the existing value.
function slice_arg!(d_in::KW, d_out::KW, k::Symbol, default_value, idx::Int = 1; new_key::Symbol = k, remove_pair::Bool = true)
v = get(d_in, k, get(d_out, new_key, default_value))
d_out[new_key] = if haskey(d_in, k) && typeof(v) <: AMat && !isempty(v)
function slice_arg!(d_in::KW, d_out::KW, k::Symbol, default_value, idx::Int, remove_pair::Bool)
v = get(d_in, k, get(d_out, k, default_value))
d_out[k] = if haskey(d_in, k) && typeof(v) <: AMat && !isempty(v)
slice_arg(v, idx)
else
v
@ -824,6 +824,7 @@ function slice_arg!(d_in::KW, d_out::KW, k::Symbol, default_value, idx::Int = 1;
if remove_pair
delete!(d_in, k)
end
return
end
# -----------------------------------------------------------------------------
@ -844,12 +845,13 @@ end
function color_or_nothing!(d::KW, k::Symbol)
v = d[k]
d[k] = if v == nothing || v == false
plot_color(RGBA(0,0,0,0))
RGBA{Float64}(0,0,0,0)
elseif v != :match
plot_color(v)
else
v
end
return
end
# -----------------------------------------------------------------------------
@ -938,7 +940,7 @@ Base.get(series::Series, k::Symbol, v) = get(series.d, k, v)
# update attr from an input dictionary
function _update_plot_args(plt::Plot, d_in::KW)
for (k,v) in _plot_defaults
slice_arg!(d_in, plt.attr, k, v)
slice_arg!(d_in, plt.attr, k, v, 1, true)
end
# handle colors
@ -949,20 +951,12 @@ function _update_plot_args(plt::Plot, d_in::KW)
end
plt.attr[:background_color] = bg
plt.attr[:foreground_color] = plot_color(fg)
# color_or_match!(plt.attr, :background_color_outside, bg)
color_or_nothing!(plt.attr, :background_color_outside)
end
# -----------------------------------------------------------------------------
# update a subplots args and axes
function _update_subplot_args(plt::Plot, sp::Subplot, d_in::KW, subplot_index::Integer; remove_pair = true)
anns = pop!(sp.attr, :annotations, [])
# grab those args which apply to this subplot
for (k,v) in _subplot_defaults
slice_arg!(d_in, sp.attr, k, v, subplot_index, remove_pair = remove_pair)
end
function _update_subplot_periphery(sp::Subplot, anns::AVec)
# extend annotations
sp.attr[:annotations] = vcat(anns, sp[:annotations])
@ -972,9 +966,11 @@ function _update_subplot_args(plt::Plot, sp::Subplot, d_in::KW, subplot_index::I
if sp.attr[:colorbar] == :legend
sp.attr[:colorbar] = sp.attr[:legend]
end
return
end
function _update_subplot_colors(sp::Subplot)
# background colors
# bg = color_or_match!(sp.attr, :background_color_subplot, plt.attr[:background_color])
color_or_nothing!(sp.attr, :background_color_subplot)
bg = plot_color(sp[:background_color_subplot])
sp.attr[:color_palette] = get_color_palette(sp.attr[:color_palette], bg, 30)
@ -986,60 +982,87 @@ function _update_subplot_args(plt::Plot, sp::Subplot, d_in::KW, subplot_index::I
color_or_nothing!(sp.attr, :foreground_color_legend)
color_or_nothing!(sp.attr, :foreground_color_grid)
color_or_nothing!(sp.attr, :foreground_color_title)
return
end
# for k in (:left_margin, :top_margin, :right_margin, :bottom_margin)
# if sp.attr[k] == :match
# sp.attr[k] = sp.attr[:margin]
# end
# end
function _update_axis(plt::Plot, sp::Subplot, d_in::KW, letter::Symbol, subplot_index::Int)
# get (maybe initialize) the axis
axis = get_axis(sp, letter)
_update_axis(axis, d_in, letter, subplot_index)
# convert a bool into auto or nothing
if isa(axis[:ticks], Bool)
axis[:ticks] = axis[:ticks] ? :auto : nothing
end
_update_axis_colors(axis)
_update_axis_links(plt, axis, letter)
return
end
function _update_axis(axis::Axis, d_in::KW, letter::Symbol, subplot_index::Int)
# grab magic args (for example `xaxis = (:flip, :log)`)
args = wraptuple(get(d_in, Symbol(letter, :axis), ()))
# build the KW of arguments from the letter version (i.e. xticks --> ticks)
kw = KW()
for (k,v) in _axis_defaults
# first get the args without the letter: `tickfont = font(10)`
# note: we don't pop because we want this to apply to all axes! (delete after all have finished)
if haskey(d_in, k)
kw[k] = slice_arg(d_in[k], subplot_index)
end
# then get those args that were passed with a leading letter: `xlabel = "X"`
lk = Symbol(letter, k)
if haskey(d_in, lk)
kw[k] = slice_arg(d_in[lk], subplot_index)
end
end
# update the axis
update!(axis, args...; kw...)
return
end
function _update_axis_colors(axis::Axis)
# # update the axis colors
color_or_nothing!(axis.d, :foreground_color_axis)
color_or_nothing!(axis.d, :foreground_color_border)
color_or_nothing!(axis.d, :foreground_color_guide)
color_or_nothing!(axis.d, :foreground_color_text)
return
end
function _update_axis_links(plt::Plot, axis::Axis, letter::Symbol)
# handle linking here. if we're passed a list of
# other subplots to link to, link them together
link = axis[:link]
if !isempty(link)
for other_sp in link
other_sp = get_subplot(plt, other_sp)
link_axes!(axis, get_axis(other_sp, letter))
end
axis.d[:link] = []
end
return
end
# update a subplots args and axes
function _update_subplot_args(plt::Plot, sp::Subplot, d_in::KW, subplot_index::Int, remove_pair::Bool)
anns = pop!(sp.attr, :annotations, [])
# grab those args which apply to this subplot
for (k,v) in _subplot_defaults
slice_arg!(d_in, sp.attr, k, v, subplot_index, remove_pair)
end
_update_subplot_periphery(sp, anns)
_update_subplot_colors(sp)
for letter in (:x, :y, :z)
# get (maybe initialize) the axis
axis = get_axis(sp, letter)
# grab magic args (for example `xaxis = (:flip, :log)`)
args = wraptuple(get(d_in, Symbol(letter, :axis), ()))
# build the KW of arguments from the letter version (i.e. xticks --> ticks)
kw = KW()
for (k,v) in _axis_defaults
# first get the args without the letter: `tickfont = font(10)`
# note: we don't pop because we want this to apply to all axes! (delete after all have finished)
if haskey(d_in, k)
kw[k] = slice_arg(d_in[k], subplot_index)
end
# then get those args that were passed with a leading letter: `xlabel = "X"`
lk = Symbol(letter, k)
if haskey(d_in, lk)
kw[k] = slice_arg(d_in[lk], subplot_index)
end
end
# update the axis
update!(axis, args...; kw...)
# convert a bool into auto or nothing
if isa(axis[:ticks], Bool)
axis[:ticks] = axis[:ticks] ? :auto : nothing
end
# # update the axis colors
color_or_nothing!(axis.d, :foreground_color_axis)
color_or_nothing!(axis.d, :foreground_color_border)
color_or_nothing!(axis.d, :foreground_color_guide)
color_or_nothing!(axis.d, :foreground_color_text)
# handle linking here. if we're passed a list of
# other subplots to link to, link them together
link = axis[:link]
if !isempty(link)
for other_sp in link
other_sp = get_subplot(plt, other_sp)
link_axes!(axis, get_axis(other_sp, letter))
end
axis.d[:link] = []
end
_update_axis(plt, sp, d_in, letter, subplot_index)
end
end
@ -1071,7 +1094,7 @@ function _add_defaults!(d::KW, plt::Plot, sp::Subplot, commandIndex::Int)
# add default values to our dictionary, being careful not to delete what we just added!
for (k,v) in _series_defaults
slice_arg!(d, d, k, v, commandIndex, remove_pair = false)
slice_arg!(d, d, k, v, commandIndex, false)
end
# this is how many series belong to this subplot

View File

@ -38,7 +38,7 @@ function get_axis(sp::Subplot, letter::Symbol)
sp.attr[axissym]
else
sp.attr[axissym] = Axis(sp, letter)
end
end::Axis
end
function process_axis_arg!(d::KW, arg, letter = "")

View File

@ -43,7 +43,6 @@ When you pass in matrices, it splits by columns. See the documentation for more
# this creates a new plot with args/kw and sets it to be the current plot
function plot(args...; kw...)
info("started to plot")
d = KW(kw)
preprocessArgs!(d)
@ -108,7 +107,7 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...)
# first apply any args for the subplots
for (idx,sp) in enumerate(plt.subplots)
_update_subplot_args(plt, sp, d, idx, remove_pair = false)
_update_subplot_args(plt, sp, d, idx, false)
end
# do we need to link any axes together?
@ -152,16 +151,13 @@ 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, d::KW)
st = d[:seriestype]
sp = d[:subplot]
function _prepare_subplot{T}(plt::Plot{T}, d::KW)
st::Symbol = d[:seriestype]
sp::Subplot{T} = d[:subplot]
sp_idx = get_subplot_index(plt, sp)
_update_subplot_args(plt, sp, d, sp_idx)
_update_subplot_args(plt, sp, d, sp_idx, true)
# do we want to override the series type?
if !is3d(st) && d[:z] != nothing && (size(d[:x]) == size(d[:y]) == size(d[:z]))
st = d[:seriestype] = (st == :scatter ? :scatter3d : :path3d)
end
st = _override_seriestype_check(d, st)
# change to a 3d projection for this subplot?
if is3d(st)
@ -173,7 +169,19 @@ function _prepare_subplot(plt::Plot, d::KW)
_initialize_subplot(plt, sp)
sp.attr[:init] = true
end
sp::Subplot
sp
end
function _override_seriestype_check(d::KW, st::Symbol)
# do we want to override the series type?
if !is3d(st)
z = d[:z]
if !isa(z, Void) && (size(d[:x]) == size(d[:y]) == size(z))
st = (st == :scatter ? :scatter3d : :path3d)
d[:seriestype] = st
end
end
st
end
function _prepare_annotations(sp::Subplot, d::KW)
@ -244,7 +252,7 @@ function _process_seriesrecipe(plt::Plot, d::KW)
end
function command_idx(kw_list::AVec{KW}, kw::KW)
kw[:series_plotindex] - kw_list[1][:series_plotindex] + 1
Int(kw[:series_plotindex]) - Int(kw_list[1][:series_plotindex]) + 1
end
function _expand_seriestype_array(d::KW, args)
@ -406,7 +414,7 @@ end
# 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.
function _process_plotrecipe(kw::KW, kw_list::Vector{KW}, still_to_process::Vector{KW})
function _process_plotrecipe(plt::Plot, kw::KW, kw_list::Vector{KW}, still_to_process::Vector{KW})
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)
@ -511,7 +519,7 @@ function _subplot_setup(plt::Plot, d::KW, kw_list::Vector{KW})
# override subplot/axis args. `sp_attrs` take precendence
for (idx,sp) in enumerate(plt.subplots)
attr = merge(d, get(sp_attrs, sp, KW()))
_update_subplot_args(plt, sp, attr, idx, remove_pair = false)
_update_subplot_args(plt, sp, attr, idx, false)
end
# do we need to link any axes together?
@ -543,7 +551,7 @@ function _plot!(plt::Plot, d::KW, args::Tuple)
kw_list = KW[]
while !isempty(still_to_process)
next_kw = shift!(still_to_process)
_process_plotrecipe(next_kw, kw_list, still_to_process)
_process_plotrecipe(plt, next_kw, kw_list, still_to_process)
end
# --------------------------------
@ -560,11 +568,11 @@ function _plot!(plt::Plot, d::KW, args::Tuple)
# --------------------------------
for kw in kw_list
sp = kw[:subplot]
idx = get_subplot_index(plt, sp)
sp::Subplot = kw[:subplot]
# idx = get_subplot_index(plt, sp)
# # we update subplot args in case something like the color palatte is part of the recipe
# _update_subplot_args(plt, sp, kw, idx)
# _update_subplot_args(plt, sp, kw, idx, true)
# set default values, select from attribute cycles, and generally set the final attributes
_add_defaults!(kw, plt, sp, command_idx(kw_list,kw))
@ -584,7 +592,7 @@ function _plot!(plt::Plot, d::KW, args::Tuple)
# do we want to force display?
if plt[:show]
gui()
gui(plt)
end
plt

View File

@ -1,10 +1,5 @@
# TODO: there should be a distinction between an object that will manage a full plot, vs a component of a plot.
# the PlotRecipe as currently implemented is more of a "custom component"
# a recipe should fully describe the plotting command(s) and call them, likewise for updating.
# actually... maybe those should explicitly derive from AbstractPlot???
"""
You can easily define your own plotting recipes with convenience methods:
@ -106,60 +101,51 @@ num_series(x) = 1
RecipesBase.apply_recipe{T}(d::KW, ::Type{T}, plt::Plot) = throw(MethodError("Unmatched plot recipe: $T"))
# # TODO: remove when StatPlots is ready
# if is_installed("DataFrames")
# @eval begin
# import DataFrames
# TODO: remove when StatPlots is ready
if is_installed("DataFrames")
@eval begin
import DataFrames
# # if it's one symbol, set the guide and return the column
# function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, sym::Symbol)
# get!(d, Symbol(letter * "guide"), string(sym))
# collect(df[sym])
# end
# if it's one symbol, set the guide and return the column
function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, sym::Symbol)
get!(d, Symbol(letter * "guide"), string(sym))
collect(df[sym])
end
# # if it's an array of symbols, set the labels and return a Vector{Any} of columns
# function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, syms::AbstractArray{Symbol})
# get!(d, :label, reshape(syms, 1, length(syms)))
# Any[collect(df[s]) for s in syms]
# end
# if it's an array of symbols, set the labels and return a Vector{Any} of columns
function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, syms::AbstractArray{Symbol})
get!(d, :label, reshape(syms, 1, length(syms)))
Any[collect(df[s]) for s in syms]
end
# # for anything else, no-op
# function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, anything)
# anything
# end
# for anything else, no-op
function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, anything)
anything
end
# # handle grouping by DataFrame column
# function extractGroupArgs(group::Symbol, df::DataFrames.AbstractDataFrame, args...)
# extractGroupArgs(collect(df[group]))
# end
# handle grouping by DataFrame column
function extractGroupArgs(group::Symbol, df::DataFrames.AbstractDataFrame, args...)
extractGroupArgs(collect(df[group]))
end
# # if a DataFrame is the first arg, lets swap symbols out for columns
# @recipe function f(df::DataFrames.AbstractDataFrame, args...)
# # if any of these attributes are symbols, swap out for the df column
# for k in (:fillrange, :line_z, :marker_z, :markersize, :ribbon, :weights, :xerror, :yerror)
# if haskey(d, k) && isa(d[k], Symbol)
# d[k] = collect(df[d[k]])
# end
# end
# if a DataFrame is the first arg, lets swap symbols out for columns
@recipe function f(df::DataFrames.AbstractDataFrame, args...)
# if any of these attributes are symbols, swap out for the df column
for k in (:fillrange, :line_z, :marker_z, :markersize, :ribbon, :weights, :xerror, :yerror)
if haskey(d, k) && isa(d[k], Symbol)
d[k] = collect(df[d[k]])
end
end
# # return a list of new arguments
# tuple(Any[handle_dfs(df, d, (i==1 ? "x" : i==2 ? "y" : "z"), arg) for (i,arg) in enumerate(args)]...)
# end
# end
# end
# return a list of new arguments
tuple(Any[handle_dfs(df, d, (i==1 ? "x" : i==2 ? "y" : "z"), arg) for (i,arg) in enumerate(args)]...)
end
end
end
# ---------------------------------------------------------------------------
# """
# `apply_series_recipe` should take a processed series KW dict and break it up
# into component parts. For example, a box plot is made up of `shape` for the
# boxes, `path` for the lines, and `scatter` for the outliers.
#
# Returns a Vector{KW}.
# """
# apply_series_recipe(d::KW, st) = KW[d]
# for seriestype `line`, need to sort by x values
@recipe function f(::Type{Val{:line}}, x, y, z)
@ -174,21 +160,6 @@ RecipesBase.apply_recipe{T}(d::KW, ::Type{T}, plt::Plot) = throw(MethodError("Un
end
@deps line path
# @recipe function f(::Type{Val{:sticks}}, x, y, z)
# nx = length(x)
# n = 3nx
# newx, newy = zeros(n), zeros(n)
# for i=1:nx
# rng = 3i-2:3i
# newx[rng] = x[i]
# newy[rng] = [0., y[i], 0.]
# end
# x := newx
# y := newy
# seriestype := :path
# ()
# end
# @deps sticks path
function hvline_limits(axis::Axis)
vmin, vmax = axis_limits(axis)
@ -898,53 +869,6 @@ end
@deps quiver shape path
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# function rotate(x::Real, y::Real, θ::Real; center = (0,0))
# cx = x - center[1]
# cy = y - center[2]
# xrot = cx * cos(θ) - cy * sin(θ)
# yrot = cy * cos(θ) + cx * sin(θ)
# xrot + center[1], yrot + center[2]
# end
#
# # ---------------------------------------------------------------------------
#
# type EllipseRecipe <: PlotRecipe
# w::Float64
# h::Float64
# x::Float64
# y::Float64
# θ::Float64
# end
# EllipseRecipe(w,h,x,y) = EllipseRecipe(w,h,x,y,0)
#
# # return x,y coords of a rotated ellipse, centered at the origin
# function rotatedEllipse(w, h, x, y, θ, rotθ)
# # # coord before rotation
# xpre = w * cos(θ)
# ypre = h * sin(θ)
#
# # rotate and translate
# r = rotate(xpre, ypre, rotθ)
# x + r[1], y + r[2]
# end
#
# function getRecipeXY(ep::EllipseRecipe)
# x, y = unzip([rotatedEllipse(ep.w, ep.h, ep.x, ep.y, u, ep.θ) for u in linspace(0,2π,100)])
# top = rotate(0, ep.h, ep.θ)
# right = rotate(ep.w, 0, ep.θ)
# linex = Float64[top[1], 0, right[1]] + ep.x
# liney = Float64[top[2], 0, right[2]] + ep.y
# Any[x, linex], Any[y, liney]
# end
#
# function getRecipeArgs(ep::EllipseRecipe)
# [(:line, (3, [:dot :solid], [:red :blue], :path))]
# end
# -------------------------------------------------
# TODO: move OHLC to PlotRecipes finance.jl

View File

@ -30,7 +30,7 @@ get_subplot(plt::Plot, i::Integer) = plt.subplots[i]
get_subplot(plt::Plot, k) = plt.spmap[k]
get_subplot(series::Series) = series.d[:subplot]
get_subplot_index(plt::Plot, idx::Integer) = idx
get_subplot_index(plt::Plot, idx::Integer) = Int(idx)
get_subplot_index(plt::Plot, sp::Subplot) = findfirst(_ -> _ === sp, plt.subplots)
series_list(sp::Subplot) = filter(series -> series.d[:subplot] === sp, sp.plt.series_list)