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