Plots.jl/src/plot.jl

535 lines
18 KiB
Julia

type CurrentPlot
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)
end
current(plot::AbstractPlot) = (CURRENT_PLOT.nullableplot = Nullable(plot))
# ---------------------------------------------------------
Base.string(plt::Plot) = "Plot{$(plt.backend) n=$(plt.n)}"
Base.print(io::IO, plt::Plot) = print(io, string(plt))
Base.show(io::IO, plt::Plot) = print(io, string(plt))
getplot(plt::Plot) = plt
getplotargs(plt::Plot, idx::Int = 1) = plt.plotargs
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`
```
There are lots of ways to pass in data, and lots of keyword arguments... just try it and it will likely work as expected.
When you pass in matrices, it splits by columns. See the documentation for more info.
"""
# 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)
plotargs = merge(d, getPlotArgs(pkg, d, 1))
# plt = _create_plot(pkg, plotargs) # create a new, blank plot
plt = Plot(nothing, pkg, 0, plt.plotargs, KW[])
plt.o = _create_backend_figure(plt)
# now update the plot
_plot!(plt, d, args...)
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...)
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)
_plot!(plt, d, args...)
end
function strip_first_letter(s::Symbol)
str = string(s)
str[1:1], symbol(str[2:end])
end
# merge the KW d into the plot args
function _add_plotargs!(plt::Plot, d::KW)
# @show d
# handle axis updates from a recipe
for letter in ("x","y","z")
# get the Axis object
asym = symbol(letter * "axis")
axis = plt.plotargs[asym]
if axis == nothing
# create a new one on first pass
axis = Axis(letter)
end
# @show 1,typeof(axis)
# update xlabel, xscale, etc
for k in _axis_symbols
lk = symbol(letter * string(k))
if haskey(d, lk)
axis[k] = d[lk]
end
end
# @show 2,axis
# update guidefont, etc
for k in _axis_symbols_fonts_colors
if haskey(d, k)
axis[k] = d[k]
end
end
# @show 3,axis
# update extrema and discrete values
datasym = symbol(letter)
if haskey(d, datasym)
v = d[datasym]
if eltype(v) <: Number
expand_extrema!(axis, v)
else
d[datasym] = discrete_value!(axis, v)
end
end
# @show 4,axis
end
for k in keys(_plotDefaults)
if haskey(d, k)
plt.plotargs[k] = pop!(d, k)
end
end
end
# this method recursively applies series recipes when the seriestype is not supported
# natively by the backend
function _apply_series_recipe(plt::Plot, d::KW)
st = d[:seriestype]
# dumpdict(d, "apply_series_recipe", true)
if st in supportedTypes()
# println("adding series!!")
warnOnUnsupported(plt.backend, d)
_add_series(plt.backend, plt, d)
else
# get a sub list of series for this seriestype
series_list = try
RecipesBase.apply_recipe(d, Val{st}, d[:x], d[:y], d[:z])
catch
warn("Exception during apply_recipe(Val{$st}, ...) with types ($(typeof(d[:x])), $(typeof(d[:y])), $(typeof(d[:z])))")
rethrow()
end
# assuming there was no error, recursively apply the series recipes
for series in series_list
_apply_series_recipe(plt, series.d)
end
end
end
# this is the core plotting function. recursively apply recipes to build
# a list of series KW dicts.
# note: at entry, we only have those preprocessed args which were passed in... no default values yet
function _plot!(plt::Plot, d::KW, args...)
# just in case the backend needs to set up the plot (make it current or something)
_before_add_series(plt)
# 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(d, :group)
args = tuple(extractGroupArgs(d[:group], args...), args...)
end
# initialize the annotations list with what was passed in
# TODO: there must be cleaner way to handle this!
anns = annotations(get(d, :annotation, NTuple{3}[]))
if typeof(anns) <: AVec{PlotText}
anns = NTuple{3}[]
else
delete!(d, :annotation)
end
# for plotting recipes, swap out the args and update the parameter dictionary
# we are keeping a queue 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, and the rest go into the queue
# for processing.
kw_list = KW[]
still_to_process = isempty(args) ? [] : [RecipeData(copy(d), args)]
while !isempty(still_to_process)
# grab the first in line to be processed and pass it through apply_recipe
# to generate a list of RecipeData objects (data + attributes)
next_series = pop!(still_to_process)
series_list = RecipesBase.apply_recipe(next_series.d, next_series.args...)
for series in series_list
# @show series
if isempty(series.args)
# when the arg tuple is empty, that means there's nothing left to recursively
# process... finish up and add to the kw_list
kw = series.d
_add_markershape(kw)
# if there was a grouping, filter the data here
_filter_input_data!(kw)
@show typeof((kw[:x], kw[:y], kw[:z]))
# 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] = map(kw[:marker_z], kw[:x])
end
# convert a ribbon into a fillrange
if get(kw, :ribbon, nothing) != nothing
rib = kw[:ribbon]
kw[:fillrange] = (kw[:y] - rib, kw[:y] + rib)
end
# check that the backend will support the command and add it to the list
warnOnUnsupportedArgs(plt.backend, kw)
warnOnUnsupportedScales(plt.backend, kw)
push!(kw_list, kw)
# handle error bars by creating new series data... these will have
# the same series index as the series they are copied from
for esym in (:xerror, :yerror)
if get(d, esym, nothing) != nothing
# we make a copy of the KW and apply an errorbar recipe
errkw = copy(kw)
errkw[:seriestype] = esym
push!(kw_list, errkw)
end
end
else
# args are non-empty, so there's still processing to do... add it back to the queue
push!(still_to_process, series)
end
end
end
# !!! note: at this point, kw_list is fully decomposed into individual series... one KW per series !!!
# now include any annotations which were added during recipes
for kw in kw_list
append!(anns, annotations(pop!(kw, :annotation, [])))
end
# @show anns
for kw in kw_list
@show typeof((kw[:x], kw[:y], kw[:z]))
end
# merge plot args... this is where we combine all the plot args from the user and
# from the recipes... axis info, colors, etc
# TODO: why do i need to check for the subplot key?
if !haskey(d, :subplot)
for kw in vcat(kw_list, d)
_add_plotargs!(plt, kw)
end
handlePlotColors(plt.backend, plt.plotargs)
end
for kw in kw_list
@show typeof((kw[:x], kw[:y], kw[:z]))
end
# this is it folks!
# TODO: we probably shouldn't use i for tracking series index, but rather explicitly track it in recipes
for (i,kw) in enumerate(kw_list)
if !(get(kw, :seriestype, :none) in (:xerror, :yerror))
plt.n += 1
end
# TODO: can this be handled as a recipe?? (yes... need to remove)
# note: this could probably be handled using a recipe signature f{S<:Union{AbstractString,Symbol}}(v::AVec{S}, letter::AbstractString)
# that gets called from within the SliceIt section
# if !stringsSupported() && di[:seriestype] != :pie
# setTicksFromStringVector(plt, d, di, "x")
# setTicksFromStringVector(plt, d, di, "y")
# setTicksFromStringVector(plt, d, di, "z")
# end
# TODO: unnecessary?? (yes... deleted as part of _add_plotargs... remove this)
# # remove plot args
# for k in keys(_plotDefaults)
# delete!(di, k)
# end
# TODO: why?? (I think we can remove??)
# # merge in plotarg_overrides
# plotarg_overrides = pop!(di, :plotarg_overrides, nothing)
# if plotarg_overrides != nothing
# merge!(plt.plotargs, plotarg_overrides)
# end
# set default values, select from attribute cycles, and generally set the final attributes
_add_defaults!(kw, plt, i)
_replace_linewidth(kw)
# 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).
_apply_series_recipe(plt, kw)
end
# now that we're done adding all the series, add the annotations
_add_annotations(plt, anns)
# add title, axis labels, ticks, etc
# TODO: do we really need this subplot check?
if !haskey(d, :subplot)
# merge!(plt.plotargs, d) # this shouldn't be needed since we merged the keys earlier
_update_plot(plt, plt.plotargs)
end
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
end
# # handle the grouping
# function _add_series(plt::Plot, d::KW, groupby::GroupBy, args...)
# starting_n = plt.n
# for (i, glab) in enumerate(groupby.groupLabels)
# tmpd = copy(d)
# tmpd[:numUncounted] = plt.n - starting_n
# _add_series(plt, tmpd, nothing, args...;
# idxfilter = groupby.groupIds[i],
# grouplabel = string(glab))
# end
# end
function _replace_linewidth(d::KW)
# get a good default linewidth... 0 for surface and heatmaps
if get(d, :linewidth, :auto) == :auto
d[:linewidth] = (get(d, :seriestype, :path) in (:surface,:heatmap,:image) ? 0 : 1)
end
end
# # no grouping
# function _add_series(plt::Plot, d::KW, args...;
# idxfilter = nothing,
# grouplabel = "")
#
# # get the list of dictionaries, one per series
# dumpdict(d, "before process_inputs")
# process_inputs(plt, d, args...)
# dumpdict(d, "after process_inputs")
#
# if idxfilter != nothing
# # add the group name as the label if there isn't one passed in
# get!(d, :label, grouplabel)
# # filter the data
# filter_data!(d, idxfilter)
# end
# # dumpdict(d,"",true)
#
# seriesArgList, xmeta, ymeta = build_series_args(plt, d) #, idxfilter)
# # seriesArgList, xmeta, ymeta = build_series_args(plt, groupargs..., args...; d...)
#
# # # 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)
#
#
# # function _add_series(plt::Plot, ds::)
# # now we can plot the series
# for (i,di) in enumerate(seriesArgList)
# plt.n += 1
#
# if !stringsSupported() && di[:seriestype] != :pie
# setTicksFromStringVector(plt, d, di, "x")
# setTicksFromStringVector(plt, d, di, "y")
# setTicksFromStringVector(plt, d, di, "z")
# end
#
# # remove plot args
# for k in keys(_plotDefaults)
# delete!(di, k)
# end
#
# # merge in plotarg_overrides
# plotarg_overrides = pop!(di, :plotarg_overrides, nothing)
# if plotarg_overrides != nothing
# merge!(plt.plotargs, plotarg_overrides)
# end
# # dumpdict(plt.plotargs, "pargs", true)
#
# dumpdict(di, "Series $i")
#
# _replace_linewidth(di)
#
# _add_series(plt.backend, plt, di)
# end
# end
# --------------------------------------------------------------------
# function get_indices(orig, labels)
# Int[findnext(labels, l, 1) for l in orig]
# end
# # TODO: remove?? this is the old way of handling discrete data... should be
# # replaced by the Axis type and logic
# function setTicksFromStringVector(plt::Plot, d::KW, di::KW, letter)
# sym = symbol(letter)
# ticksym = symbol(letter * "ticks")
# pargs = plt.plotargs
# v = di[sym]
#
# # do we really want to do this?
# typeof(v) <: AbstractArray || return
# isempty(v) && return
# trueOrAllTrue(_ -> typeof(_) <: AbstractString, v) || return
#
# # compute the ticks and labels
# ticks, labels = if ticksType(pargs[ticksym]) == :ticks_and_labels
# # extend the existing ticks and labels. only add to labels if they're new!
# ticks, labels = pargs[ticksym]
# newlabels = filter(_ -> !(_ in labels), unique(v))
# newticks = if isempty(ticks)
# collect(1:length(newlabels))
# else
# maximum(ticks) + collect(1:length(newlabels))
# end
# ticks = vcat(ticks, newticks)
# labels = vcat(labels, newlabels)
# ticks, labels
# else
# # create new ticks and labels
# newlabels = unique(v)
# collect(1:length(newlabels)), newlabels
# end
#
# d[ticksym] = ticks, labels
# plt.plotargs[ticksym] = ticks, labels
#
# # add an origsym field so that later on we can re-compute the x vector if ticks change
# origsym = symbol(letter * "orig")
# di[origsym] = v
# di[sym] = get_indices(v, labels)
#
# # loop through existing plt.seriesargs and adjust indices if there is an origsym key
# for sargs in plt.seriesargs
# if haskey(sargs, origsym)
# # TODO: might need to call the setindex function instead to trigger a plot update for some backends??
# sargs[sym] = get_indices(sargs[origsym], labels)
# end
# end
# end
# --------------------------------------------------------------------
# TODO: remove
# # should we update the x/y label given the meta info during input slicing?
# function updateDictWithMeta(d::KW, plotargs::KW, meta::Symbol, isx::Bool)
# lsym = isx ? :xlabel : :ylabel
# if plotargs[lsym] == default(lsym)
# d[lsym] = string(meta)
# end
# end
# updateDictWithMeta(d::KW, plotargs::KW, meta, isx::Bool) = nothing
# --------------------------------------------------------------------
annotations(::@compat(Void)) = []
annotations{X,Y,V}(v::AVec{@compat(Tuple{X,Y,V})}) = v
annotations{X,Y,V}(t::@compat(Tuple{X,Y,V})) = [t]
annotations(v::AVec{PlotText}) = v
annotations(v::AVec) = map(PlotText, v)
annotations(anns) = error("Expecting a tuple (or vector of tuples) for annotations: ",
"(x, y, annotation)\n got: $(typeof(anns))")
function annotations(plt::Plot, anns)
anns = annotations(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
anns
end
function _add_annotations(plt::Plot, d::KW)
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)]
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
end
# --------------------------------------------------------------------