Plots.jl/src/plot.jl

548 lines
19 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)
# create an empty Plot, update the args using the inputs, then pass it
# to the backend to finish backend-specific initialization
plt = Plot()
_update_plot_args(plt, d)
plt.o = _create_backend_figure(plt)
# create the layout and subplots from the inputs
plt.layout, plt.subplots, plt.spmap = build_layout(plt.plotargs)
for (idx,sp) in enumerate(plt.subplots)
sp.plt = plt
_update_subplot_args(plt, sp, copy(d), idx)
end
# 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
# 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!!")
# getting ready to add the series... last update to subplot from anything
# that might have been added during series recipes
sp = d[:subplot]
sp_idx = get_subplot_index(plt, sp)
_update_subplot_args(plt, sp, d, sp_idx)
# change to a 3d projection for this subplot?
if is3d(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
# adjust extrema and discrete info
for s in (:x, :y, :z)
data = d[s]
axis = sp.attr[symbol(s, "axis")]
if eltype(data) <: Number
expand_extrema!(axis, data)
else
# TODO: need more here... gotta track the discrete reference value
# as well as any coord offset (think of boxplot shape coords... they all
# correspond to the same x-value)
d[s] = discrete_value!(axis, data)
end
end
# add the series!
warnOnUnsupportedArgs(plt.backend, d)
warnOnUnsupported(plt.backend, d)
series = Series(d)
push!(plt.series_list, series)
_add_series(plt, series)
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)
# first apply any args for the subplots
for (idx,sp) in enumerate(plt.subplots)
_update_subplot_args(plt, sp, d, idx)
end
# 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
# series should be of type RecipeData. if it's not then the inputs must not have been fully processed by recipes
if !(typeof(series) <: RecipeData)
error("Inputs couldn't be processed... expected RecipeData but got: $series")
end
# @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
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 !!!
# # TODO: move annotations into subplot update
# # 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)
# for kw in kw_list
# _update_subplot_args(plt, kw[:subplot], kw)
# # _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)
# DD(kw, "series $i before")
if !(get(kw, :seriestype, :none) in (:xerror, :yerror))
plt.n += 1
end
# get the Subplot object to which the series belongs
sp = get(kw, :subplot, :auto)
sp = if sp == :auto
mod1(i,length(plt.subplots))
else
slice_arg(sp, i)
end
sp = kw[:subplot] = get_subplot(plt, sp)
idx = get_subplot_index(plt, sp)
# strip out series annotations (those which are based on series x/y coords)
# and add them to the subplot attr
sp_anns = annotations(sp.attr[:annotations])
anns = annotations(pop!(kw, :series_annotations, []))
if length(anns) > 0
x, y = kw[:x], kw[:y]
nx, ny, na = map(length, (x,y,anns))
n = max(nx, ny, na)
anns = [(x[mod1(i,nx)], y[mod1(i,ny)], text(anns[mod1(i,na)])) for i=1:n]
end
sp.attr[:annotations] = vcat(sp_anns, anns)
# we update subplot args in case something like the color palatte is part of the recipe
# DD(sp.attr[:xaxis].d, "before USA $i")
# DD(kw, "kw")
_update_subplot_args(plt, sp, kw, idx)
# DD(sp.attr[:xaxis].d, "after USA $i")
# set default values, select from attribute cycles, and generally set the final attributes
_add_defaults!(kw, plt, sp, i)
# DD(kw, "series $i after")
#
# 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).
# DD(sp.attr[:xaxis].d, "before $i")
_apply_series_recipe(plt, kw)
# DD(sp.attr[:xaxis].d, "after $i")
end
# # now that we're done adding all the series, add the annotations
# _add_annotations(plt, anns)
# TODO just need to pass plt... and we should do all non-series updates here
_update_plot(plt, plt.plotargs)
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(_plot_defaults)
# 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 ? :xguide : :yguide
# if plotargs[lsym] == default(lsym)
# d[lsym] = string(meta)
# end
# end
# updateDictWithMeta(d::KW, plotargs::KW, meta, isx::Bool) = nothing
# --------------------------------------------------------------------
annotations(::Void) = []
annotations(anns::AVec) = anns
annotations(anns) = Any[anns]
# 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(_series_defaults,k), sargs)
plot!(plt2; sargs...)
end
plt2
end
# --------------------------------------------------------------------