Plots.jl/src/plot.jl
2016-05-12 14:51:53 -04:00

393 lines
12 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
# 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
# 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)
# TODO: the grouping mechanism will be 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 = vcat(extractGroupArgs(d[:group], args...), args)
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)
next_series = pop!(still_to_process)
series_list = RecipesBase.apply_recipe(next_series.d, next_series.args...)
for series in series_list
if isempty(series.args)
push!(kw_list, series.d)
else
push!(still_to_process, series)
end
end
end
# dumpdict(d, "After plot! preprocessing")
# apply markershape_to_add and then warn if there's anything left unsupported
_add_markershape(d)
warnOnUnsupportedArgs(plt.backend, d)
warnOnUnsupportedScales(plt.backend, d)
# # # grouping
# groupby = if haskey(d, :group)
# extractGroupArgs(d[:group], args...)
# else
# nothing
# end
# TODO: why do i need to check for the subplot key?
# merge plot args
if !haskey(d, :subplot)
# merge the plot args from the recipes, then update the plot colors
for k in keys(_plotDefaults)
for kw in kw_list
if haskey(kw, k)
plt.plotargs[k] = kw[k]
end
end
end
# merge!(plt.plotargs, d)
handlePlotColors(plt.backend, plt.plotargs)
end
# _add_series(plt, d, args...)
# 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)
plt.n += 1
# TODO: can this be handled as a recipe??
# if !stringsSupported() && di[:linetype] != :pie
# setTicksFromStringVector(plt, d, di, "x")
# setTicksFromStringVector(plt, d, di, "y")
# setTicksFromStringVector(plt, d, di, "z")
# end
# TODO: unnecessary??
# # remove plot args
# for k in keys(_plotDefaults)
# delete!(di, k)
# end
# TODO: why??
# # merge in plotarg_overrides
# plotarg_overrides = pop!(di, :plotarg_overrides, nothing)
# if plotarg_overrides != nothing
# merge!(plt.plotargs, plotarg_overrides)
# end
dumpdict(kw, "before add defaults", true)
_add_defaults!(kw, plt, i)
dumpdict(kw, "after add defaults", true)
# getSeriesArgs(plt.backend, getplotargs(plt, n), d, commandIndex, convertSeriesIndex(plt, n), n)
_replace_linewidth(kw)
_add_series(plt.backend, plt, kw)
end
# _add_annotations(plt, d) # TODO
# add title, axis labels, ticks, etc
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
# _update_plot_pos_size(plt, d) # this is only used for Qwt... can we remove?
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
filter_data(v::AVec, idxfilter::AVec{Int}) = v[idxfilter]
filter_data(v, idxfilter) = v
function filter_data!(d::KW, idxfilter)
for s in (:x, :y, :z)
d[s] = filter_data(get(d, s, nothing), idxfilter)
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, :linetype, :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[:linetype] != :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
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
# --------------------------------------------------------------------
_before_add_series(plt::Plot) = nothing
# --------------------------------------------------------------------
# # 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 _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
# --------------------------------------------------------------------