From ae1f5b8b0687b58d8f30ac4fbfa8471d4058f037 Mon Sep 17 00:00:00 2001 From: Thomas Breloff Date: Mon, 16 May 2016 14:24:45 -0400 Subject: [PATCH] working on layouts --- src/backends/pyplot.jl | 100 ++++++++++++++++++++++- src/plot.jl | 8 +- src/subplots.jl | 175 +++++++++++++++++++++++++++++++++++++++-- src/types.jl | 50 ++++++++++-- 4 files changed, 315 insertions(+), 18 deletions(-) diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 7e5c0909..106a423f 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -66,6 +66,7 @@ function _initialize_backend(::PyPlotBackend) const pyticker = PyPlot.pywrap(PyPlot.pyimport("matplotlib.ticker")) const pycmap = PyPlot.pywrap(PyPlot.pyimport("matplotlib.cm")) const pynp = PyPlot.pywrap(PyPlot.pyimport("numpy")) + const pytransforms = PyPlot.pywrap(PyPlot.pyimport("matplotlib.transforms")) end PyPlot.ioff() @@ -220,20 +221,110 @@ function add_pyfixedformatter(cbar, vals::AVec) cbar[:update_ticks]() end +# --------------------------------------------------------------------------- +# Figure utils -- F*** matplotlib for making me work so hard to figure this crap out + +# the drawing surface +canvas(fig) = fig[:canvas] + +# the object controlling draw commands +renderer(fig) = canvas(fig)[:get_renderer]() + +# draw commands... paint the screen (probably updating internals too) +drawfig(fig) = fig[:draw](renderer(fig)) +drawax(ax) = ax[:draw](renderer(ax[:get_figure]())) + +bbox(obj) = obj[:get_window_extent](renderer(obj[:get_figure]())) +pos(obj) = obj[:get_position]() + +# merge a list of bounding boxes together to become the area that surrounds them all +bbox_union(bboxes) = pytransforms.Bbox[:union](bboxes) + +function bbox_pct(obj) + pybbox_pixels = obj[:get_window_extent]() + fig = obj[:get_figure]() + pybbox_pct = pybbox_pixels[:inverse_transformed](fig[:transFigure]) + left, right, bottom, top = pybbox_pct[:get_points]() + BoundingBox(left, bottom, right, top) +end + +# bbox_from_pyplot(obj) = + +function bbox_ticks(ax, letter) + # fig = ax[:get_figure]() + # @show fig + labels = ax[symbol("get_"*letter*"ticklabels")]() + # @show labels + # bboxes = [] + bbox_union = BoundingBox() + for lab in labels + # @show lab,lab[:get_text]() + bbox = bbox_pct(lab) + bbox_union = bbox_union + bbox + # @show bbox_union + end + bbox_union +end + +function bbox_axislabel(ax, letter) + pyaxis_label = ax[symbol("get_"*letter*"axis")]() + bbox_pct(pyaxis_label) +end + +# get a bounding box for the whole axis +function bbox_axis(ax, letter) + bbox_ticks(ax, letter) + bbox_axislabel(ax, letter) +end + +# get a bounding box for the title area +function bbox_title(ax) + bbox_pct(ax[:title]) +end + +xaxis_height(sp::Subplot{PyPlotBackend}) = height(bbox_axis(sp.o,"x")) +yaxis_width(sp::Subplot{PyPlotBackend}) = width(bbox_axis(sp.o,"y")) +title_height(sp::Subplot{PyPlotBackend}) = height(bbox_title(sp.o)) + +# --------------------------------------------------------------------------- + +# function used_width(sp::Subplot{PyPlotBackend}) +# ax = sp.o +# width(bbox_axis(ax,"y")) +# end +# +# function used_height(sp::Subplot{PyPlotBackend}) +# ax = sp.o +# height(bbox_axis(ax,"x")) + height(bbox_title(ax)) +# end + + +# # bounding box (relative to canvas) for plot area +# function plotarea_bbox(sp::Subplot{PyPlotBackend}) +# crop(bbox(sp), BoundingBox()) +# end + +function update_position!(sp::Subplot{PyPlotBackend}) + ax = sp.o + bb = plotarea_bbox(sp) + ax[:set_position]([f(bb) for f in (left, bottom, width, height)]) +end + # --------------------------------------------------------------------------- function getAxis(plt::Plot{PyPlotBackend}, subplot::Subplot = plt.subplots[1]) if subplot.o == nothing @show subplot fig = plt.o - @show plt + @show fig # if fig == nothing # fig = # TODO: actual coords? # NOTE: might want to use ax[:get_tightbbox](ax[:get_renderer_cache]()) to calc size of guides? # NOTE: can set subplot location later with ax[:set_position]([left, bottom, width, height]) - left, bottom, width, height = 0.3, 0.3, 0.5, 0.5 - ax = fig[:add_axes]([left, bottom, width, height]) + # left, bottom, width, height = 0.3, 0.3, 0.5, 0.5 + + # init to the full canvas, then we can set_position later + ax = fig[:add_axes]([0,0,1,1]) subplot.o = ax end subplot.o @@ -1183,6 +1274,9 @@ function finalizePlot(plt::Plot{PyPlotBackend}) ax = getLeftAxis(plt) addPyPlotLegend(plt, ax) updateAxisColors(ax, plt.plotargs) + drawfig(plt.o) + update_bboxes!(plt.layout) + update_position!(plt.layout) PyPlot.draw() end diff --git a/src/plot.jl b/src/plot.jl index 0a7b8726..8bd2d2f2 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -47,15 +47,15 @@ function plot(args...; kw...) d = KW(kw) preprocessArgs!(d) - layout = pop!(d, :layout, Subplot()) - subplots = Subplot[layout] # TODO: build full list - smap = SubplotMap(1 => layout) # TODO: actually build a map + layout, subplots, spmap = build_layout(pop!(d, :layout, :auto)) + # subplots = Subplot[layout] # TODO: build full list + # smap = SubplotMap(1 => layout) # TODO: actually build a map # TODO: this seems wrong... I only call getPlotArgs when creating a new plot?? plotargs = merge(d, getPlotArgs(pkg, d, 1)) # plt = _create_plot(pkg, plotargs) # create a new, blank plot - plt = Plot(nothing, pkg, 0, plotargs, Series[], subplots, smap, layout) + plt = Plot(nothing, pkg, 0, plotargs, Series[], subplots, spmap, layout) plt.o = _create_backend_figure(plt) # now update the plot diff --git a/src/subplots.jl b/src/subplots.jl index b024c95e..642fbaae 100644 --- a/src/subplots.jl +++ b/src/subplots.jl @@ -1,23 +1,188 @@ +left(bbox::BoundingBox) = bbox.left +bottom(bbox::BoundingBox) = bbox.bottom +right(bbox::BoundingBox) = bbox.right +top(bbox::BoundingBox) = bbox.top +width(bbox::BoundingBox) = bbox.right - bbox.left +height(bbox::BoundingBox) = bbox.top - bbox.bottom + +Base.size(bbox::BoundingBox) = (width(bbox), height(bbox)) + +# union together bounding boxes +function Base.(:+)(bb1::BoundingBox, bb2::BoundingBox) + # empty boxes don't change the union + if width(bb1) <= 0 || height(bb1) <= 0 + return bb2 + elseif width(bb2) <= 0 || height(bb2) <= 0 + return bb1 + end + BoundingBox( + min(bb1.left, bb2.left), + min(bb1.bottom, bb2.bottom), + max(bb1.right, bb2.right), + max(bb1.top, bb2.top) + ) +end + +# this creates a bounding box in the parent's scope, where the child bounding box +# is relative to the parent +function crop(parent::BoundingBox, child::BoundingBox) + l = left(parent) + width(parent) * left(child) + w = width(parent) * width(child) + b = bottom(parent) + height(parent) * bottom(child) + h = height(parent) * height(child) + BoundingBox(l, b, l+w, b+h) +end + +# ---------------------------------------------------------------------- + +# this is the available area for drawing everything in this layout... as percentages of total canvas +bbox(layout::AbstractLayout) = layout.bbox +bbox!(layout::AbstractLayout, bb::BoundingBox) = (layout.bbox = bb) + +# layouts are recursive, tree-like structures, and most will have a parent field +Base.parent(layout::AbstractLayout) = layout.parent +parent_bbox(layout::AbstractLayout) = bbox(parent(layout)) + +# this is a calculation of the percentage of free space available in the canvas +# after accounting for the size of guides and axes +free_size(layout::AbstractLayout) = (free_width(layout), free_height(layout)) +free_width(layout::AbstractLayout) = 1.0 - used_width(layout) +free_height(layout::AbstractLayout) = 1.0 - used_height(layout) + +used_size(layout::AbstractLayout) = (used_width(layout), used_height(layout)) + +# # compute the area which is available to this layout +# function bbox_pct(layout::AbstractLayout) +# +# end + +# ---------------------------------------------------------------------- + Base.size(layout::EmptyLayout) = (0,0) Base.length(layout::EmptyLayout) = 0 Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing +used_width(layout::EmptyLayout) = 0.0 +used_height(layout::EmptyLayout) = 0.0 -Base.size(layout::RootLayout) = (1,1) -Base.length(layout::RootLayout) = 1 +update_position!(layout::EmptyLayout) = nothing + +# ---------------------------------------------------------------------- + +Base.parent(::RootLayout) = nothing +parent_bbox(::RootLayout) = BoundingBox(0,0,1,1) +bbox(::RootLayout) = BoundingBox(0,0,1,1) + +# Base.size(layout::RootLayout) = (1,1) +# Base.length(layout::RootLayout) = 1 # Base.getindex(layout::RootLayout, r::Int, c::Int) = layout.child -Base.size(subplot::Subplot) = (1,1) -Base.length(subplot::Subplot) = 1 -Base.getindex(subplot::Subplot, r::Int, c::Int) = subplot +# ---------------------------------------------------------------------- +Base.size(sp::Subplot) = (1,1) +Base.length(sp::Subplot) = 1 +Base.getindex(sp::Subplot, r::Int, c::Int) = sp + + +used_width(sp::Subplot) = yaxis_width(sp) +used_height(sp::Subplot) = xaxis_height(sp) + title_height(sp) + +# used_width(subplot::Subplot) = error("used_width(::Subplot) must be implemented by each backend") +# used_height(subplot::Subplot) = error("used_height(::Subplot) must be implemented by each backend") + +# # this should return a bounding box (relative to the canvas) for the plot area (inside the spines/ticks) +# plotarea_bbox(subplot::Subplot) = error("plotarea_bbox(::Subplot) must be implemented by each backend") + +# bounding box (relative to canvas) for plot area +# note: we assume the x axis is on the left, and y axis is on the bottom +function plotarea_bbox(sp::Subplot) + crop(bbox(sp), BoundingBox(yaxis_width(sp), xaxis_height(sp), 1, 1 - title_height(sp))) +end + +# NOTE: this is unnecessary I think as it is the same as bbox(::Subplot) +# # this should return a bounding box (relative to the canvas) for the whole subplot (plotarea, ticks, and guides) +# subplot_bbox(subplot::Subplot) = error("subplot_bbox(::Subplot) must be implemented by each backend") + +# ---------------------------------------------------------------------- Base.size(layout::GridLayout) = size(layout.grid) Base.length(layout::GridLayout) = length(layout.grid) Base.getindex(layout::GridLayout, r::Int, c::Int) = layout.grid[r,c] +function Base.setindex!(layout::GridLayout, v, r::Int, c::Int) + layout.grid[r,c] = v +end +function used_width(layout::GridLayout) + w = 0.0 + nr,nc = size(layout) + for c=1:nc + w += maximum([used_width(layout(r,c)) for r=1:nr]) + end + w +end + +function used_height(layout::GridLayout) + h = 0.0 + nr,nc = size(layout) + for r=1:nr + h += maximum([used_height(layout(r,c)) for c=1:nc]) + end + h +end + +update_position!(layout::GridLayout) = map(update_position!, layout.grid) + +# a recursive method to first compute the free space by bubbling the free space +# up the tree, then assigning bounding boxes according to height/width percentages +# note: this should be called after all axis objects are updated to re-compute the +# bounding boxes for the layout tree +function update_bboxes!(layout::GridLayout) #, parent_bbox::BoundingBox = BoundingBox(0,0,1,1)) + # initialize the free space (per child!) + nr, nc = size(layout) + freew, freeh = free_size(layout) + freew /= nc + freeh /= nr + + # TODO: this should really track used/free space for each row/column so that we can align plot areas properly + + l, b = 0.0, 0.0 + for r=1:nr, c=1:nc + # compute the child's bounding box relative to the parent + child = layout[r,c] + usedw, usedh = used_size(child) + child_bbox = BoundingBox(l, b, l + usedw + freew, b + usedh + freeh) + + # then compute the bounding box relative to the canvas, and cache it in the child object + bbox!(child, crop(bbox(layout), child_bbox)) + + # now recursively update the child + update_bboxes!(child) + end +end + +# ---------------------------------------------------------------------- + +# return the top-level layout, a list of subplots, and a SubplotMap +function build_layout(s::Symbol) + s == :auto || error() # TODO: handle anything else + layout = GridLayout(1, 2) + nr, nc = size(layout) + subplots = Subplot[] + spmap = SubplotMap() + for r=1:nr, c=1:nc + sp = Subplot(backend(), parent=layout) + layout[r,c] = sp + push!(subplots, sp) + spmap[(r,c)] = sp + end + layout, subplots, spmap +end + +# ---------------------------------------------------------------------- +# ---------------------------------------------------------------------- +# ---------------------------------------------------------------------- # Base.start(layout::GridLayout) = 1 # Base.done(layout::GridLayout, state) = state > length(layout) diff --git a/src/types.jl b/src/types.jl index aa7c5ba5..571244da 100644 --- a/src/types.jl +++ b/src/types.jl @@ -35,12 +35,28 @@ end # Layouts # ----------------------------------------------------------- +# wraps bounding box coords (percent of parent area) +# NOTE: (0,0) is the bottom-left, and (1,1) is the top-right! +immutable BoundingBox + left::Float64 + bottom::Float64 + right::Float64 + top::Float64 +end +BoundingBox() = BoundingBox(0,0,0,0) + +# ----------------------------------------------------------- + abstract AbstractLayout # ----------------------------------------------------------- # contains blank space -immutable EmptyLayout <: AbstractLayout end +type EmptyLayout <: AbstractLayout + parent::AbstractLayout + bbox::BoundingBox +end +EmptyLayout(parent) = EmptyLayout(parent, BoundingBox(0,0,1,1)) # this is the parent of the top-level layout immutable RootLayout <: AbstractLayout @@ -50,8 +66,9 @@ end # ----------------------------------------------------------- # a single subplot -type Subplot <: AbstractLayout +type Subplot{T<:AbstractBackend} <: AbstractLayout parent::AbstractLayout + bbox::BoundingBox # the canvas area which is available to this subplot attr::KW # args specific to this subplot # axisviews::Vector{AxisView} o # can store backend-specific data... like a pyplot ax @@ -59,19 +76,40 @@ type Subplot <: AbstractLayout # Subplot(parent = RootLayout(); attr = KW()) end -Subplot() = Subplot(RootLayout(), KW(), nothing) +function Subplot{T<:AbstractBackend}(::T; parent = RootLayout()) + Subplot{T}(parent, BoundingBox(0,0,1,1), KW(), nothing) +end # ----------------------------------------------------------- # nested, gridded layout with optional size percentages -immutable GridLayout <: AbstractLayout +type GridLayout <: AbstractLayout parent::AbstractLayout + bbox::BoundingBox grid::Matrix{AbstractLayout} # Nested layouts. Each position is a AbstractLayout, which allows for arbitrary recursion - # widths::Vector{Float64} - # heights::Vector{Float64} + widths::Vector{Float64} + heights::Vector{Float64} attr::KW end +function GridLayout(dims...; + parent = RootLayout(), + widths = ones(dims[2]), + heights = ones(dims[1]), + kw...) + grid = Matrix{AbstractLayout}(dims...) + layout = GridLayout( + parent, + BoundingBox(0,0,1,1), + grid, + convert(Vector{Float64}, widths), + convert(Vector{Float64}, heights), + KW(kw)) + fill!(grid, EmptyLayout(layout)) + layout +end + + # ----------------------------------------------------------- typealias SubplotMap Dict{Any, Subplot}