From 2ab0dc20d7d99b5f65627e0d367b09fa9bdc88b6 Mon Sep 17 00:00:00 2001 From: Thomas Breloff Date: Thu, 17 Mar 2016 16:52:09 -0400 Subject: [PATCH] working on series reorg --- src/plot.jl | 225 +++++++++++++++++++++++---------------------- src/series_args.jl | 108 ++++++++-------------- 2 files changed, 155 insertions(+), 178 deletions(-) diff --git a/src/plot.jl b/src/plot.jl index 215f3f35..9b348548 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -1,16 +1,16 @@ type CurrentPlot - nullableplot::Nullable{AbstractPlot} + nullableplot::Nullable{AbstractPlot} end const CURRENT_PLOT = CurrentPlot(Nullable{AbstractPlot}()) isplotnull() = isnull(CURRENT_PLOT.nullableplot) function current() - if isplotnull() - error("No current plot/subplot") - end - get(CURRENT_PLOT.nullableplot) + if isplotnull() + error("No current plot/subplot") + end + get(CURRENT_PLOT.nullableplot) end current(plot::AbstractPlot) = (CURRENT_PLOT.nullableplot = Nullable(plot)) @@ -32,9 +32,9 @@ convertSeriesIndex(plt::Plot, n::Int) = n The main plot command. Use `plot` to create a new plot object, and `plot!` to add to an existing one: ``` - plot(args...; kw...) # creates a new plot window, and sets it to be the current - plot!(args...; kw...) # adds to the `current` - plot!(plotobj, args...; kw...) # adds to the plot `plotobj` + plot(args...; kw...) # creates a new plot window, and sets it to be the current + plot!(args...; kw...) # adds to the `current` + plot!(plotobj, args...; kw...) # adds to the plot `plotobj` ``` There are lots of ways to pass in data, and lots of keyword arguments... just try it and it will likely work as expected. @@ -43,105 +43,111 @@ 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...) - pkg = backend() - d = KW(kw) - preprocessArgs!(d) - dumpdict(d, "After plot preprocessing") + pkg = backend() + d = KW(kw) + preprocessArgs!(d) + dumpdict(d, "After plot preprocessing") - plotargs = merge(d, getPlotArgs(pkg, d, 1)) - dumpdict(plotargs, "Plot args") - plt = _create_plot(pkg; plotargs...) # create a new, blank plot + plotargs = merge(d, getPlotArgs(pkg, d, 1)) + dumpdict(plotargs, "Plot args") + plt = _create_plot(pkg; plotargs...) # create a new, blank plot - delete!(d, :background_color) - plot!(plt, args...; d...) # add to it + delete!(d, :background_color) + plot!(plt, args...; d...) # add to it end # this adds to the current plot, or creates a new plot if none are current function plot!(args...; kw...) - local plt - try - plt = current() - catch - return plot(args...; kw...) - end - plot!(current(), args...; kw...) + local plt + try + plt = current() + catch + return plot(args...; kw...) + end + plot!(current(), args...; kw...) end # this adds to a specific plot... most plot commands will flow through here function plot!(plt::Plot, args...; kw...) - d = KW(kw) - preprocessArgs!(d) + d = KW(kw) + preprocessArgs!(d) - # for plotting recipes, swap out the args and update the parameter dictionary - args = _apply_recipe(d, args...; kw...) + # for plotting recipes, swap out the args and update the parameter dictionary + args = _apply_recipe(d, args...; kw...) - dumpdict(d, "After plot! preprocessing") + dumpdict(d, "After plot! preprocessing") - warnOnUnsupportedArgs(plt.backend, d) + warnOnUnsupportedArgs(plt.backend, d) - # grouping - groupargs = get(d, :group, nothing) == nothing ? [] : [extractGroupArgs(d[:group], args...)] + # just in case the backend needs to set up the plot (make it current or something) + _before_add_series(plt) - # just in case the backend needs to set up the plot (make it current or something) - _before_add_series(plt) + # grouping + groupargs = get(d, :group, nothing) == nothing ? [nothing] : [extractGroupArgs(d[:group], args...)] + # @show groupargs - # get the list of dictionaries, one per series - @show groupargs args - dumpdict(d, "before process_inputs") - process_inputs(plt, d, groupargs..., args...) - dumpdict(d, "after process_inputs") - seriesArgList, xmeta, ymeta = build_series_args(plt, d) - # seriesArgList, xmeta, ymeta = build_series_args(plt, groupargs..., args...; d...) + # TODO: get the GroupBy object (or nothing), and loop through the groups, doing the following section many times - # if we were able to extract guide information from the series inputs, then update the plot - # @show xmeta, ymeta - updateDictWithMeta(d, plt.plotargs, xmeta, true) - updateDictWithMeta(d, plt.plotargs, ymeta, false) - # now we can plot the series - for (i,di) in enumerate(seriesArgList) - plt.n += 1 + # get the list of dictionaries, one per series + @show groupargs map(typeof, args) + dumpdict(d, "before process_inputs") + process_inputs(plt, d, groupargs..., args...) + dumpdict(d, "after process_inputs", true) + seriesArgList, xmeta, ymeta = build_series_args(plt, d) + # seriesArgList, xmeta, ymeta = build_series_args(plt, groupargs..., args...; d...) - if !stringsSupported() - setTicksFromStringVector(d, di, :x, :xticks) - setTicksFromStringVector(d, di, :y, :yticks) + # if we were able to extract guide information from the series inputs, then update the plot + # @show xmeta, ymeta + updateDictWithMeta(d, plt.plotargs, xmeta, true) + updateDictWithMeta(d, plt.plotargs, ymeta, false) + + # now we can plot the series + for (i,di) in enumerate(seriesArgList) + plt.n += 1 + + if !stringsSupported() + setTicksFromStringVector(d, di, :x, :xticks) + setTicksFromStringVector(d, di, :y, :yticks) + end + + # remove plot args + for k in keys(_plotDefaults) + delete!(di, k) + end + + dumpdict(di, "Series $i") + + _add_series(plt.backend, plt; di...) end - # remove plot args - for k in keys(_plotDefaults) - delete!(di, k) + # TODO: this is the end of the groupby loop + + _add_annotations(plt, d) + + warnOnUnsupportedScales(plt.backend, d) + + + # add title, axis labels, ticks, etc + if !haskey(d, :subplot) + merge!(plt.plotargs, d) + dumpdict(plt.plotargs, "Updating plot items") + _update_plot(plt, plt.plotargs) end - dumpdict(di, "Series $i") + _update_plot_pos_size(plt, d) - _add_series(plt.backend, plt; di...) - end + current(plt) - _add_annotations(plt, d) + # NOTE: lets ignore the show param and effectively use the semicolon at the end of the REPL statement + # # do we want to show it? + if haskey(d, :show) && d[:show] + gui() + end - warnOnUnsupportedScales(plt.backend, d) - - - # add title, axis labels, ticks, etc - if !haskey(d, :subplot) - merge!(plt.plotargs, d) - dumpdict(plt.plotargs, "Updating plot items") - _update_plot(plt, plt.plotargs) - end - - _update_plot_pos_size(plt, d) - - current(plt) - - # NOTE: lets ignore the show param and effectively use the semicolon at the end of the REPL statement - # # do we want to show it? - if haskey(d, :show) && d[:show] - gui() - end - - plt + plt end # -------------------------------------------------------------------- @@ -149,21 +155,20 @@ end # if x or y are a vector of strings, we should create a list of unique strings, # and map x/y to be the index of the string... then set the x/y tick labels function setTicksFromStringVector(d::Dict, di::Dict, sym::Symbol, ticksym::Symbol) - # if the x or y values are strings, set ticks to the unique values, and x/y to the indices of the ticks + # if the x or y values are strings, set ticks to the unique values, and x/y to the indices of the ticks - v = di[sym] - isa(v, AbstractArray) || return + v = di[sym] + isa(v, AbstractArray) || return - T = eltype(v) - if T <: @compat(AbstractString) || (!isempty(T.types) && all(x -> x <: @compat(AbstractString), T.types)) + T = eltype(v) + if T <: @compat(AbstractString) || (!isempty(T.types) && all(x -> x <: @compat(AbstractString), T.types)) + ticks = unique(di[sym]) + di[sym] = Int[findnext(ticks, v, 1) for v in di[sym]] - ticks = unique(di[sym]) - di[sym] = Int[findnext(ticks, v, 1) for v in di[sym]] - - if !haskey(d, ticksym) || d[ticksym] == :auto - d[ticksym] = (collect(1:length(ticks)), UTF8String[t for t in ticks]) + if !haskey(d, ticksym) || d[ticksym] == :auto + d[ticksym] = (collect(1:length(ticks)), UTF8String[t for t in ticks]) + end end - end end # -------------------------------------------------------------------- @@ -174,10 +179,10 @@ _before_add_series(plt::Plot) = nothing # should we update the x/y label given the meta info during input slicing? function updateDictWithMeta(d::Dict, plotargs::Dict, meta::Symbol, isx::Bool) - lsym = isx ? :xlabel : :ylabel - if plotargs[lsym] == default(lsym) - d[lsym] = string(meta) - end + lsym = isx ? :xlabel : :ylabel + if plotargs[lsym] == default(lsym) + d[lsym] = string(meta) + end end updateDictWithMeta(d::Dict, plotargs::Dict, meta, isx::Bool) = nothing @@ -192,30 +197,30 @@ annotations(anns) = error("Expecting a tuple (or vector of tuples) for annotatio "(x, y, annotation)\n got: $(typeof(anns))") function _add_annotations(plt::Plot, d::Dict) - anns = annotations(get(d, :annotation, nothing)) - if !isempty(anns) + anns = annotations(get(d, :annotation, nothing)) + if !isempty(anns) - # if we just have a list of PlotText objects, then create (x,y,text) tuples - if typeof(anns) <: AVec{PlotText} - x, y = plt[plt.n] - anns = Tuple{Float64,Float64,PlotText}[(x[i], y[i], t) for (i,t) in enumerate(anns)] + # if we just have a list of PlotText objects, then create (x,y,text) tuples + if typeof(anns) <: AVec{PlotText} + x, y = plt[plt.n] + anns = Tuple{Float64,Float64,PlotText}[(x[i], y[i], t) for (i,t) in enumerate(anns)] + end + + _add_annotations(plt, anns) end - - _add_annotations(plt, anns) - end end # -------------------------------------------------------------------- function Base.copy(plt::Plot) - backend(plt.backend) - plt2 = plot(; plt.plotargs...) - for sargs in plt.seriesargs - sargs = filter((k,v) -> haskey(_seriesDefaults,k), sargs) - plot!(plt2; sargs...) - end - plt2 + backend(plt.backend) + plt2 = plot(; plt.plotargs...) + for sargs in plt.seriesargs + sargs = filter((k,v) -> haskey(_seriesDefaults,k), sargs) + plot!(plt2; sargs...) + end + plt2 end # -------------------------------------------------------------------- diff --git a/src/series_args.jl b/src/series_args.jl index 50d8a964..fb13d43e 100644 --- a/src/series_args.jl +++ b/src/series_args.jl @@ -57,50 +57,49 @@ end # -------------------------------------------------------------------- -# # in computeXandY, we take in any of the possible items, convert into proper x/y vectors, then return. -# # this is also where all the "set x to 1:length(y)" happens, and also where we assert on lengths. -# computeX(x::@compat(Void), y) = 1:size(y,1) -# computeX(x, y) = copy(x) -# computeY(x, y::Function) = map(y, x) -# computeY(x, y) = copy(y) -# function computeXandY(x, y) -# if x == nothing && isa(y, Function) -# error("If you want to plot the function `$y`, you need to define the x values somehow!") -# end -# x, y = computeX(x,y), computeY(x,y) -# # @assert length(x) == length(y) -# x, y -# end +# TODO: can we avoid the copy here? one error that crops up is that mapping functions over the same array +# result in that array being shared. push!, etc will add too many items to that array -compute_x(x::Void, y::Void, z) = 1:size(z,1) -compute_x(x::Void, y, z) = 1:size(y,1) -compute_x(x::Function, y, z) = map(x, y) -compute_x(x, y, z) = x +compute_x(x::Void, y::Void, z) = 1:size(z,1) +compute_x(x::Void, y, z) = 1:size(y,1) +compute_x(x::Function, y, z) = map(x, y) +compute_x(x, y, z) = copy(x) -compute_y(x::Void, y::Function, z) = error() -compute_y(x::Void, y::Void, z) = 1:size(z,2) -# compute_y(x::Void, y, z) = 1:size(z,2) -compute_y(x, y::Function, z) = map(y, x) -compute_y(x, y, z) = y +# compute_y(x::Void, y::Function, z) = error() +compute_y(x::Void, y::Void, z) = 1:size(z,2) +compute_y(x, y::Function, z) = map(y, x) +compute_y(x, y, z) = copy(y) -compute_z(x, y, z::Function) = map(z, x, y) -compute_z(x, y, z) = Surface(z) +compute_z(x, y, z::Function) = map(z, x, y) +compute_z(x, y, z::AbstractMatrix) = Surface(z) +compute_z(x, y, z::Void) = nothing +compute_z(x, y, z) = copy(z) -compute_xyz(x, y, z) = compute_x(x,y,z), compute_y(x,y,z), compute_z(x,y,z) +@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) + x, y, z +end # not allowed -compute_xyz(x::Void, y::FuncOrFuncs, z) = error("If you want to plot the function `$y`, you need to define the x values!") +compute_xyz(x::Void, y::FuncOrFuncs, z) = error("If you want to plot the function `$y`, you need to define the x values!") compute_xyz(x::Void, y::Void, z::FuncOrFuncs) = error("If you want to plot the function `$z`, you need to define x and y values!") -compute_xyz(x::Void, y::Void, z::Void) = error("x/y/z are all nothing!") +compute_xyz(x::Void, y::Void, z::Void) = error("x/y/z are all nothing!") # -------------------------------------------------------------------- # create n=max(mx,my) series arguments. the shorter list is cycled through # note: everything should flow through this function build_series_args(plt::AbstractPlot, kw::KW) - xs, xmeta = convertToAnyVector(pop!(kw, :x, nothing), kw) - ys, ymeta = convertToAnyVector(pop!(kw, :y, nothing), kw) - zs, zmeta = convertToAnyVector(pop!(kw, :z, nothing), kw) + x, y, z = map(a -> pop!(kw, a, nothing), (:x, :y, :z)) + if nothing == x == y == z + return [], nothing, nothing + end + + xs, xmeta = convertToAnyVector(x, kw) + ys, ymeta = convertToAnyVector(y, kw) + zs, zmeta = convertToAnyVector(z, kw) mx = length(xs) my = length(ys) @@ -126,12 +125,13 @@ function build_series_args(plt::AbstractPlot, kw::KW) d = getSeriesArgs(plt.backend, getplotargs(plt, n), d, i + numUncounted, convertSeriesIndex(plt, n), n) dumpdict(d, "after getSeriesArgs") - @show xs[mod1(i,mx)] ys[mod1(i,my)] zs[mod1(i,mz)] + @show map(typeof, (xs[mod1(i,mx)], ys[mod1(i,my)], zs[mod1(i,mz)])) d[:x], d[:y], d[:z] = compute_xyz(xs[mod1(i,mx)], ys[mod1(i,my)], zs[mod1(i,mz)]) - @show d[:x] d[:y] d[:z] + @show map(typeof, (d[:x], d[:y], d[:z])) + # # NOTE: this should be handled by the time it gets here - # lt = d[:linetype] + lt = d[:linetype] # if isa(d[:y], Surface) # if lt in (:contour, :heatmap, :surface, :wireframe) # z = d[:y] @@ -192,30 +192,6 @@ end function process_inputs(plt::AbstractPlot, d::KW) end -# # TODO: all methods should probably do this... check for (and pop!) x/y/z values if they exist -# -# function build_series_args(plt::AbstractPlot, d::KW) -# -# build_series_args(plt, d, pop!(d, :x, nothing), -# pop!(d, :y, nothing), -# pop!(d, :z, nothing); d...) -# end - - -# d = KW(kw) -# if !haskey(d, :y) -# # assume we just want to create an empty plot object which can be added to later -# return [], nothing, nothing -# # error("Called plot/subplot without args... must set y in the keyword args. Example: plot(; y=rand(10))") -# end -# -# if haskey(d, :x) -# return build_series_args(plt, d, d[:x], d[:y]) -# else -# return build_series_args(plt, d, d[:y]) -# end -# end - # -------------------------------------------------------------------- # 1 argument # -------------------------------------------------------------------- @@ -292,7 +268,7 @@ function process_inputs(plt::AbstractPlot, d::KW, x::AVec, y::AVec, zvec::AVec) if !(get(d, :linetype, :none) in _3dTypes) d[:linetype] = :path3d end - d[:x], d[:y], d[:z] = x, y, z + d[:x], d[:y], d[:z] = x, y, zvec end # surface-like... function @@ -309,7 +285,7 @@ function process_inputs{T<:Number}(plt::AbstractPlot, d::KW, x::AVec, y::AVec, z x_idx = sortperm(x) y_idx = sortperm(y) x, y = x[x_idx], y[y_idx] - zmat = z[x_idx, y_idx] + zmat = zmat[x_idx, y_idx] end d[:x], d[:y], d[:z] = x, y, Surface{Matrix{Float64}}(zmat) if !like_surface(get(d, :linetype, :none)) @@ -339,13 +315,13 @@ function process_inputs(plt::AbstractPlot, d::KW, f::FuncOrFuncs, xmin::Number, end # special handling... xmin/xmax with parametric function(s) -process_inputs{T<:Number}(plt::AbstractPlot, fx::FuncOrFuncs, fy::FuncOrFuncs, u::AVec{T}) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u)) -process_inputs{T<:Number}(plt::AbstractPlot, u::AVec{T}, fx::FuncOrFuncs, fy::FuncOrFuncs) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u)) +process_inputs{T<:Number}(plt::AbstractPlot, d::KW, fx::FuncOrFuncs, fy::FuncOrFuncs, u::AVec{T}) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u)) +process_inputs{T<:Number}(plt::AbstractPlot, d::KW, u::AVec{T}, fx::FuncOrFuncs, fy::FuncOrFuncs) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u)) process_inputs(plt::AbstractPlot, d::KW, fx::FuncOrFuncs, fy::FuncOrFuncs, umin::Number, umax::Number, numPoints::Int = 1000) = process_inputs(plt, d, fx, fy, linspace(umin, umax, numPoints)) # special handling... 3D parametric function(s) -process_inputs{T<:Number}(plt::AbstractPlot, fx::FuncOrFuncs, fy::FuncOrFuncs, fz::FuncOrFuncs, u::AVec{T}) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u), mapFuncOrFuncs(fz, u)) -process_inputs{T<:Number}(plt::AbstractPlot, u::AVec{T}, fx::FuncOrFuncs, fy::FuncOrFuncs, fz::FuncOrFuncs) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u), mapFuncOrFuncs(fz, u)) +process_inputs{T<:Number}(plt::AbstractPlot, d::KW, fx::FuncOrFuncs, fy::FuncOrFuncs, fz::FuncOrFuncs, u::AVec{T}) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u), mapFuncOrFuncs(fz, u)) +process_inputs{T<:Number}(plt::AbstractPlot, d::KW, u::AVec{T}, fx::FuncOrFuncs, fy::FuncOrFuncs, fz::FuncOrFuncs) = process_inputs(plt, d, mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u), mapFuncOrFuncs(fz, u)) process_inputs(plt::AbstractPlot, d::KW, fx::FuncOrFuncs, fy::FuncOrFuncs, fz::FuncOrFuncs, umin::Number, umax::Number, numPoints::Int = 1000) = process_inputs(plt, d, fx, fy, fz, linspace(umin, umax, numPoints)) @@ -415,10 +391,6 @@ end function setup_dataframes() @require DataFrames begin - # function process_inputs(plt::AbstractPlot, d::KW, df::DataFrames.AbstractDataFrame, args...) - # process_inputs(plt, d, args..., dataframe = df) - # end - function process_inputs(plt::AbstractPlot, d::KW, df::DataFrames.AbstractDataFrame, args...) d[:dataframe] = df process_inputs(plt, d, args...)