428 lines
12 KiB
Julia
428 lines
12 KiB
Julia
|
|
function subplotlayout(sz::@compat(Tuple{Int,Int}))
|
|
GridLayout(sz...)
|
|
end
|
|
|
|
function subplotlayout(rowcounts::AVec{Int})
|
|
FlexLayout(sum(rowcounts), rowcounts)
|
|
end
|
|
|
|
function subplotlayout(numplts::Int, nr::Int, nc::Int)
|
|
|
|
# figure out how many rows/columns we need
|
|
if nr == -1
|
|
if nc == -1
|
|
nr = round(Int, sqrt(numplts))
|
|
nc = ceil(Int, numplts / nr)
|
|
else
|
|
nr = ceil(Int, numplts / nc)
|
|
end
|
|
else
|
|
nc = ceil(Int, numplts / nr)
|
|
end
|
|
|
|
# if it's a perfect rectangle, just create a grid
|
|
if numplts == nr * nc
|
|
return GridLayout(nr, nc)
|
|
end
|
|
|
|
# create the rowcounts vector
|
|
i = 0
|
|
rowcounts = Int[]
|
|
for r in 1:nr
|
|
cnt = min(nc, numplts - i)
|
|
push!(rowcounts, cnt)
|
|
i += cnt
|
|
end
|
|
|
|
FlexLayout(numplts, rowcounts)
|
|
end
|
|
|
|
|
|
|
|
Base.length(layout::FlexLayout) = layout.numplts
|
|
Base.start(layout::FlexLayout) = 1
|
|
Base.done(layout::FlexLayout, state) = state > length(layout)
|
|
function Base.next(layout::FlexLayout, state)
|
|
r = 1
|
|
c = 0
|
|
for i = 1:state
|
|
c += 1
|
|
if c > layout.rowcounts[r]
|
|
r += 1
|
|
c = 1
|
|
end
|
|
end
|
|
(r,c), state + 1
|
|
end
|
|
|
|
nrows(layout::FlexLayout) = length(layout.rowcounts)
|
|
ncols(layout::FlexLayout, row::Int) = row < 1 ? 0 : (row > nrows(layout) ? 0 : layout.rowcounts[row])
|
|
|
|
# get the plot index given row and column
|
|
Base.getindex(layout::FlexLayout, r::Int, c::Int) = sum(layout.rowcounts[1:r-1]) + c
|
|
|
|
Base.length(layout::GridLayout) = layout.nr * layout.nc
|
|
Base.start(layout::GridLayout) = 1
|
|
Base.done(layout::GridLayout, state) = state > length(layout)
|
|
function Base.next(layout::GridLayout, state)
|
|
r = div(state-1, layout.nc) + 1
|
|
c = mod1(state, layout.nc)
|
|
(r,c), state + 1
|
|
end
|
|
|
|
nrows(layout::GridLayout) = layout.nr
|
|
ncols(layout::GridLayout) = layout.nc
|
|
ncols(layout::GridLayout, row::Int) = layout.nc
|
|
|
|
# get the plot index given row and column
|
|
Base.getindex(layout::GridLayout, r::Int, c::Int) = (r-1) * layout.nc + c
|
|
|
|
Base.getindex(subplt::Subplot, args...) = subplt.layout[args...]
|
|
|
|
# handle "linking" the subplot axes together
|
|
# each backend should implement the handleLinkInner and expandLimits! methods
|
|
function linkAxis(subplt::Subplot, isx::Bool)
|
|
|
|
# collect the list of plots and the expanded limits for those plots that should be linked on this axis
|
|
includedPlots = Any[]
|
|
# lims = [Inf, -Inf]
|
|
lims = Dict{Int,Any}() # maps column to xlim
|
|
for (i,(r,c)) in enumerate(subplt.layout)
|
|
|
|
# shouldlink will be a bool or nothing. if nothing, then use linkx/y (which is true if we get to this code)
|
|
shouldlink = subplt.linkfunc(r,c)[isx ? 1 : 2]
|
|
if shouldlink == nothing || shouldlink
|
|
plt = subplt.plts[i]
|
|
|
|
# if we don't have this
|
|
k = isx ? c : r
|
|
if (firstone = !haskey(lims, k))
|
|
lims[k] = [Inf, -Inf]
|
|
end
|
|
|
|
isinner = (isx && r < nrows(subplt.layout)) || (!isx && !firstone)
|
|
push!(includedPlots, (plt, isinner, k))
|
|
|
|
expandLimits!(lims[k], plt, isx)
|
|
end
|
|
|
|
end
|
|
|
|
# do the axis adjustments
|
|
for (plt, isinner, k) in includedPlots
|
|
if isinner
|
|
handleLinkInner(plt, isx)
|
|
end
|
|
(isx ? xlims! : ylims!)(plt, lims[k]...)
|
|
end
|
|
end
|
|
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
|
|
Base.string(subplt::Subplot) = "Subplot{$(subplt.backend) p=$(subplt.p) n=$(subplt.n)}"
|
|
Base.print(io::IO, subplt::Subplot) = print(io, string(subplt))
|
|
Base.show(io::IO, subplt::Subplot) = print(io, string(subplt))
|
|
|
|
getplot(subplt::Subplot, idx::Int = subplt.n) = subplt.plts[mod1(idx, subplt.p)]
|
|
getinitargs(subplt::Subplot, idx::Int) = getplot(subplt, idx).initargs
|
|
convertSeriesIndex(subplt::Subplot, n::Int) = ceil(Int, n / subplt.p)
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
function validateSubplotSupported()
|
|
if !subplotSupported()
|
|
error(CURRENT_BACKEND.sym, " does not support the subplot/subplot! commands at this time. Try one of: ", join(filter(pkg->subplotSupported(backendInstance(pkg)), backends()),", "))
|
|
end
|
|
end
|
|
|
|
"""
|
|
Create a series of plots:
|
|
```
|
|
y = rand(100,3)
|
|
subplot(y; n = 3) # create an automatic grid, and let it figure out the nr/nc... will put plots 1 and 2 on the first row, and plot 3 by itself on the 2nd row
|
|
subplot(y; n = 3, nr = 1) # create an automatic grid, but fix the number of rows to 1 (so there are n columns)
|
|
subplot(y; n = 3, nc = 1) # create an automatic grid, but fix the number of columns to 1 (so there are n rows)
|
|
subplot(y; layout = [1, 2]) # explicit layout by row... plot #1 goes by itself in the first row, plots 2 and 3 split the 2nd row (note the n kw is unnecessary)
|
|
subplot(plts, n; nr = -1, nc = -1) # build a layout from existing plots
|
|
subplot(plts, layout) # build a layout from existing plots
|
|
```
|
|
"""
|
|
function subplot(args...; kw...)
|
|
validateSubplotSupported()
|
|
d = Dict(kw)
|
|
preprocessArgs!(d)
|
|
|
|
# figure out the layout
|
|
layoutarg = get(d, :layout, nothing)
|
|
if layoutarg != nothing
|
|
layout = subplotlayout(layoutarg)
|
|
else
|
|
n = get(d, :n, -1)
|
|
if n < 0
|
|
error("You must specify either layout or n when creating a subplot: ", d)
|
|
end
|
|
layout = subplotlayout(n, get(d, :nr, -1), get(d, :nc, -1))
|
|
end
|
|
|
|
# initialize the individual plots
|
|
pkg = backend()
|
|
plts = Plot[]
|
|
# ds = Dict[]
|
|
for i in 1:length(layout)
|
|
di = getPlotArgs(pkg, d, i)
|
|
di[:subplot] = true
|
|
push!(plts, plot(pkg; di...))
|
|
# push!(ds, getPlotArgs(pkg, d, i))
|
|
# ds[i][:subplot] = true
|
|
# push!(plts, plot(pkg; ds[i]...))
|
|
end
|
|
|
|
# tmpd = getPlotKeywordArgs(pkg, kw, 1, 0) # TODO: this should happen in the plot creation loop... think... what if we want to set a title per subplot??
|
|
# # shouldShow = tmpd[:show]
|
|
# # tmpd[:show] = false
|
|
# plts = Plot[plot(pkg; tmpd...) for i in 1:length(layout)]
|
|
# # tmpd[:show] = shouldShow
|
|
|
|
# create the object and do the plotting
|
|
# subplt = Subplot(nothing, plts, pkg, length(layout), 0, layout, ds, false, false, false, (r,c) -> (nothing,nothing))
|
|
subplt = Subplot(nothing, plts, pkg, length(layout), 0, layout, d, false, false, false, (r,c) -> (nothing,nothing))
|
|
subplot!(subplt, args...; kw...)
|
|
|
|
subplt
|
|
end
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
# NOTE: for the subplot calls building from existing plots, we need the first plot to be separate to ensure dispatch calls this instead of the more general subplot(args...; kw...)
|
|
|
|
# grid layout
|
|
# function subplot{P}(plt1::Plot{P}, plts::Plot{P}...; nr::Integer = -1, nc::Integer = -1, link = false, linkx = false, linky = false)
|
|
function subplot{P}(plt1::Plot{P}, plts::Plot{P}...; kw...)
|
|
d = Dict(kw)
|
|
layout = subplotlayout(length(plts)+1, get(d, :nr, -1), get(d, :nc, -1))
|
|
subplot(vcat(plt1, plts...), layout, d) #, link || linkx, link || linky)
|
|
end
|
|
|
|
# explicit layout
|
|
function subplot{P,I<:Integer}(pltsPerRow::AVec{I}, plt1::Plot{P}, plts::Plot{P}...; kw...) #link = false, linkx = false, linky = false)
|
|
layout = subplotlayout(pltsPerRow)
|
|
subplot(vcat(plt1, plts...), layout, Dict(kw)) #, link || linkx, link || linky)
|
|
end
|
|
|
|
# this will be called internally
|
|
function subplot{P<:PlottingPackage}(plts::AVec{Plot{P}}, layout::SubplotLayout, d::Dict) #, linkx::Bool, linky::Bool)
|
|
p = length(layout)
|
|
n = sum([plt.n for plt in plts])
|
|
subplt = Subplot(nothing, collect(plts), P(), p, n, layout, Dict(), false, false, false, (r,c) -> (nothing,nothing))
|
|
|
|
# # update links
|
|
# for s in (:linkx, :linky, :linkfunc)
|
|
# if haskey(d, s)
|
|
# setfield!(subplt, s, d[s])
|
|
# delete!(d, s)
|
|
# end
|
|
# end
|
|
|
|
preprocessSubplot(subplt, d)
|
|
|
|
# # init (after plot creation)
|
|
# if !subplt.initialized
|
|
# subplt.initialized = buildSubplotObject!(subplt, false)
|
|
# end
|
|
|
|
# # add title, axis labels, ticks, etc
|
|
# for (i,plt) in enumerate(subplt.plts)
|
|
# di = copy(d)
|
|
# for (k,v) in di
|
|
# if typeof(v) <: AVec
|
|
# di[k] = v[mod1(i, length(v))]
|
|
# elseif typeof(v) <: AMat
|
|
# m = size(v,2)
|
|
# di[k] = (size(v,1) == 1 ? v[1, mod1(i, m)] : v[:, mod1(i, m)])
|
|
# end
|
|
# end
|
|
# dumpdict(di, "Updating sp $i")
|
|
# updatePlotItems(plt, di)
|
|
# end
|
|
|
|
# # handle links
|
|
# subplt.linkx && linkAxis(subplt, true)
|
|
# subplt.linky && linkAxis(subplt, false)
|
|
|
|
# # set this to be current
|
|
# current(subplt)
|
|
|
|
postprocessSubplot(subplt, d)
|
|
|
|
subplt
|
|
end
|
|
|
|
# TODO: hcat/vcat subplots and plots together arbitrarily
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
function preprocessSubplot(subplt::Subplot, d::Dict)
|
|
validateSubplotSupported()
|
|
preprocessArgs!(d)
|
|
dumpdict(d, "After subplot! preprocessing")
|
|
|
|
# process links. TODO: extract to separate function
|
|
for s in (:linkx, :linky, :linkfunc)
|
|
if haskey(d, s)
|
|
setfield!(subplt, s, d[s])
|
|
delete!(d, s)
|
|
end
|
|
end
|
|
end
|
|
|
|
function postprocessSubplot(subplt::Subplot, d::Dict)
|
|
# init (after plot creation)
|
|
if !subplt.initialized
|
|
subplt.initialized = buildSubplotObject!(subplt, false)
|
|
end
|
|
|
|
# add title, axis labels, ticks, etc
|
|
for (i,plt) in enumerate(subplt.plts)
|
|
di = copy(d)
|
|
for (k,v) in di
|
|
if typeof(v) <: AVec
|
|
di[k] = v[mod1(i, length(v))]
|
|
elseif typeof(v) <: AMat
|
|
m = size(v,2)
|
|
di[k] = (size(v,1) == 1 ? v[1, mod1(i, m)] : v[:, mod1(i, m)])
|
|
end
|
|
end
|
|
dumpdict(di, "Updating sp $i")
|
|
updatePlotItems(plt, di)
|
|
end
|
|
|
|
# handle links
|
|
subplt.linkx && linkAxis(subplt, true)
|
|
subplt.linky && linkAxis(subplt, false)
|
|
|
|
# set this to be current
|
|
current(subplt)
|
|
end
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
"""
|
|
Adds to a subplot.
|
|
"""
|
|
|
|
# current subplot
|
|
function subplot!(args...; kw...)
|
|
validateSubplotSupported()
|
|
subplot!(current(), args...; kw...)
|
|
end
|
|
|
|
|
|
# not allowed:
|
|
function subplot!(plt::Plot, args...; kw...)
|
|
error("Can't call subplot! on a Plot!")
|
|
end
|
|
|
|
|
|
# # this adds to a specific subplot... most plot commands will flow through here
|
|
function subplot!(subplt::Subplot, args...; kw...)
|
|
# validateSubplotSupported()
|
|
|
|
d = Dict(kw)
|
|
preprocessSubplot(subplt, d)
|
|
# preprocessArgs!(d)
|
|
# dumpdict(d, "After subplot! preprocessing")
|
|
|
|
# # process links. TODO: extract to separate function
|
|
# for s in (:linkx, :linky, :linkfunc)
|
|
# if haskey(d, s)
|
|
# setfield!(subplt, s, d[s])
|
|
# delete!(d, s)
|
|
# end
|
|
# end
|
|
|
|
# create the underlying object (each backend will do this differently)
|
|
# note: we call it once before doing the individual plots, and once after
|
|
# this is because some backends need to set up the subplots and then plot,
|
|
# and others need to do it the other way around
|
|
if !subplt.initialized
|
|
subplt.initialized = buildSubplotObject!(subplt, true)
|
|
end
|
|
|
|
# handle grouping
|
|
group = get(d, :group, nothing)
|
|
if group == nothing
|
|
groupargs = []
|
|
else
|
|
groupargs = [extractGroupArgs(d[:group], args...)]
|
|
delete!(d, :group)
|
|
end
|
|
|
|
|
|
kwList, xmeta, ymeta = createKWargsList(subplt, groupargs..., args...; d...)
|
|
|
|
# TODO: something useful with meta info?
|
|
|
|
for (i,di) in enumerate(kwList)
|
|
|
|
subplt.n += 1
|
|
plt = getplot(subplt)
|
|
|
|
# cleanup the dictionary that we pass into the plot! command
|
|
di[:show] = false
|
|
di[:subplot] = true
|
|
for k in (:title, :xlabel, :xticks, :xlims, :xscale, :xflip,
|
|
:ylabel, :yticks, :ylims, :yscale, :yflip)
|
|
delete!(di, k)
|
|
end
|
|
dumpdict(di, "subplot! kwList $i")
|
|
|
|
plot!(plt; di...)
|
|
end
|
|
|
|
postprocessSubplot(subplt, d)
|
|
# # -- TODO: extract this section into a separate function... duplicates the other subplot ---------
|
|
|
|
# # create the underlying object (each backend will do this differently)
|
|
# if !subplt.initialized
|
|
# subplt.initialized = buildSubplotObject!(subplt, false)
|
|
# # subplt.initialized = true
|
|
# end
|
|
|
|
|
|
# # add title, axis labels, ticks, etc
|
|
# for (i,plt) in enumerate(subplt.plts)
|
|
# di = copy(d)
|
|
# for (k,v) in di
|
|
# if typeof(v) <: AVec
|
|
# di[k] = v[mod1(i, length(v))]
|
|
# elseif typeof(v) <: AMat
|
|
# m = size(v,2)
|
|
# di[k] = (size(v,1) == 1 ? v[1, mod1(i, m)] : v[:, mod1(i, m)])
|
|
# end
|
|
# end
|
|
# dumpdict(di, "Updating sp $i")
|
|
# updatePlotItems(plt, di)
|
|
# end
|
|
|
|
# subplt.linkx && linkAxis(subplt, true)
|
|
# subplt.linky && linkAxis(subplt, false)
|
|
|
|
# # set this to be current
|
|
# current(subplt)
|
|
# # --- end extract ----
|
|
|
|
# show it automatically?
|
|
if haskey(d, :show) && d[:show]
|
|
gui()
|
|
end
|
|
|
|
subplt
|
|
end
|
|
|
|
|