redo the bbox logic

This commit is contained in:
Thomas Breloff 2016-05-17 17:41:03 -04:00
parent 80b9115393
commit 0d237028e7
4 changed files with 209 additions and 124 deletions

View File

@ -249,14 +249,22 @@ drawax(ax) = ax[:draw](renderer(ax[:get_figure]()))
# BoundingBox(left, bottom, right, top) # BoundingBox(left, bottom, right, top)
# end # end
function py_bbox_fig(obj)
fl, fr, fb, ft = fig[:get_window_extent]()
BoundingBox(0px, 0px, (fr-fl)*px, (ft-fb)*px)
end
# compute a bounding box (with origin top-left), however pyplot gives coords with origin bottom-left
function py_bbox(obj) function py_bbox(obj)
fl, fr, fb, ft = py_bbox_fig(obj[:get_figure]())
l, r, b, t = obj[:get_window_extent]()[:get_points]() l, r, b, t = obj[:get_window_extent]()[:get_points]()
BoundingBox(l*px, b*px, (r-l)*px, (t-b)*px) BoundingBox(l*px, (ft-t)*px, (r-l)*px, (t-b)*px)
# BoundingBox(l*px, (t*px, (r-l)*px, (t-b)*px)
end end
# bbox_from_pyplot(obj) = # bbox_from_pyplot(obj) =
py_bbox_fig(plt::Plot) = py_bbox(plt.o) py_bbox_fig(plt::Plot) = py_bbox_fig(plt.o)
function py_bbox_ticks(ax, letter) function py_bbox_ticks(ax, letter)
# fig = ax[:get_figure]() # fig = ax[:get_figure]()
@ -289,31 +297,40 @@ function py_bbox_title(ax)
py_bbox(ax[:title]) py_bbox(ax[:title])
end end
xaxis_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_axis(sp.o,"x")) # TODO: need to compute each of these by subtracting the plotarea position from
yaxis_width(sp::Subplot{PyPlotBackend}) = width(py_bbox_axis(sp.o,"y")) # the most extreme guide/axis in each direction. Can pass method (left,top,right,bottom)
title_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_title(sp.o)) # and aggregation (minimum or maximum) into a method to do this.
# note: this overrides the default version to allow for labels that stick out the sides min_padding_left(layout::Subplot{PyPlotBackend}) = 0mm
# bounding box (relative to canvas) for plot area min_padding_top(layout::Subplot{PyPlotBackend}) = 0mm
# note: we assume the x axis is on the left, and y axis is on the bottom min_padding_right(layout::Subplot{PyPlotBackend}) = 0mm
# TODO: really the padding below should be part of the freespace calc, and we should probably min_padding_bottom(layout::Subplot{PyPlotBackend}) = 0mm
# cache the plotarea bbox while we're doing that (need to add plotarea field to Subplot)
function plotarea_bbox(sp::Subplot{PyPlotBackend}) # xaxis_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_axis(sp.o,"x"))
ax = sp.o # yaxis_width(sp::Subplot{PyPlotBackend}) = width(py_bbox_axis(sp.o,"y"))
plot_bb = py_bbox(ax) # title_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_title(sp.o))
xbb = py_bbox_axis(ax, "x")
ybb = py_bbox_axis(ax, "y") # # note: this overrides the default version to allow for labels that stick out the sides
titbb = py_bbox_title(ax) # # bounding box (relative to canvas) for plot area
items = [xbb, ybb, titbb] # # note: we assume the x axis is on the left, and y axis is on the bottom
# TODO: add in margin/padding from sp.attr # # TODO: really the padding below should be part of the freespace calc, and we should probably
leftpad = max(0mm, left(plot_bb) - minimum(map(left, items))) # # cache the plotarea bbox while we're doing that (need to add plotarea field to Subplot)
bottompad = max(0mm, bottom(plot_bb) - minimum(map(bottom, items))) # function plotarea_bbox(sp::Subplot{PyPlotBackend})
rightpad = max(0mm, maximum(map(right, items)) - right(plot_bb)) # ax = sp.o
toppad = max(0mm, maximum(map(top, items)) - top(plot_bb)) # plot_bb = py_bbox(ax)
crop(bbox(sp), BoundingBox(leftpad, bottompad, # xbb = py_bbox_axis(ax, "x")
width(sp) - rightpad - leftpad, # ybb = py_bbox_axis(ax, "y")
height(sp) - toppad - bottompad)) # titbb = py_bbox_title(ax)
end # items = [xbb, ybb, titbb]
# # TODO: add in margin/padding from sp.attr
# leftpad = max(0mm, left(plot_bb) - minimum(map(left, items)))
# bottompad = max(0mm, bottom(plot_bb) - minimum(map(bottom, items)))
# rightpad = max(0mm, maximum(map(right, items)) - right(plot_bb))
# toppad = max(0mm, maximum(map(top, items)) - top(plot_bb))
# crop(bbox(sp), BoundingBox(leftpad, bottompad,
# width(sp) - rightpad - leftpad,
# height(sp) - toppad - bottompad))
# end
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -1316,7 +1333,7 @@ function finalizePlot(plt::Plot{PyPlotBackend})
end end
drawfig(plt.o) drawfig(plt.o)
plt.layout.bbox = py_bbox_fig(plt) plt.layout.bbox = py_bbox_fig(plt)
upadte_child_bboxes!(plt.layout) update_child_bboxes!(plt.layout)
update_position!(plt.layout) update_position!(plt.layout)
PyPlot.draw() PyPlot.draw()
end end

View File

@ -200,6 +200,11 @@ function _plot!(plt::Plot, d::KW, args...)
# just in case the backend needs to set up the plot (make it current or something) # just in case the backend needs to set up the plot (make it current or something)
_before_add_series(plt) _before_add_series(plt)
# first apply any args for the subplots
for sp in plt.subplots
_update_subplot_args(plt, sp, d)
end
# the grouping mechanism is a recipe on a GroupBy object # 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 # we simply add the GroupBy object to the front of the args list to allow
# the recipe to be applied # the recipe to be applied
@ -316,7 +321,7 @@ function _plot!(plt::Plot, d::KW, args...)
end end
# get the Subplot object to which the series belongs # get the Subplot object to which the series belongs
sp = slice_arg(get(kw,:subplot,1), i) sp = slice_arg(get(kw, :subplot, :auto), i)
if sp == :auto if sp == :auto
sp = 1 # TODO: something useful sp = 1 # TODO: something useful
end end

View File

@ -11,21 +11,26 @@ bbox!(layout::AbstractLayout, bb::BoundingBox) = (layout.bbox = bb)
Base.parent(layout::AbstractLayout) = layout.parent Base.parent(layout::AbstractLayout) = layout.parent
parent_bbox(layout::AbstractLayout) = bbox(parent(layout)) parent_bbox(layout::AbstractLayout) = bbox(parent(layout))
# this is a calculation of the percentage of free space available in the canvas # # this is a calculation of the percentage of free space available in the canvas
# after accounting for the size of guides and axes # # after accounting for the size of guides and axes
free_size(layout::AbstractLayout) = (free_width(layout), free_height(layout)) # free_size(layout::AbstractLayout) = (free_width(layout), free_height(layout))
free_width(layout::AbstractLayout) = width(layout.bbox) - used_width(layout) # free_width(layout::AbstractLayout) = width(layout.bbox) - used_width(layout)
free_height(layout::AbstractLayout) = height(layout.bbox) - used_height(layout) # free_height(layout::AbstractLayout) = height(layout.bbox) - used_height(layout)
used_size(layout::AbstractLayout) = (used_width(layout), used_height(layout)) # NOTE: these should be implemented for subplots in each backend!
# they represent the minimum size of the axes and guides
min_padding_left(layout::AbstractLayout) = 0mm
min_padding_top(layout::AbstractLayout) = 0mm
min_padding_right(layout::AbstractLayout) = 0mm
min_padding_bottom(layout::AbstractLayout) = 0mm
padding_w(layout::AbstractLayout) = left_padding(layout) + right_padding(layout)
padding_h(layout::AbstractLayout) = bottom_padding(layout) + top_padding(layout)
padding(layout::AbstractLayout) = (padding_w(layout), padding_h(layout))
# # compute the area which is available to this layout
# function bbox_pct(layout::AbstractLayout)
#
# end
update_position!(layout::AbstractLayout) = nothing update_position!(layout::AbstractLayout) = nothing
upadte_child_bboxes!(layout::AbstractLayout) = nothing update_child_bboxes!(layout::AbstractLayout) = nothing
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@ -33,9 +38,6 @@ Base.size(layout::EmptyLayout) = (0,0)
Base.length(layout::EmptyLayout) = 0 Base.length(layout::EmptyLayout) = 0
Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing
used_width(layout::EmptyLayout) = 0pct
used_height(layout::EmptyLayout) = 0pct
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@ -54,8 +56,8 @@ Base.length(sp::Subplot) = 1
Base.getindex(sp::Subplot, r::Int, c::Int) = sp Base.getindex(sp::Subplot, r::Int, c::Int) = sp
used_width(sp::Subplot) = yaxis_width(sp) # used_width(sp::Subplot) = yaxis_width(sp)
used_height(sp::Subplot) = xaxis_height(sp) + title_height(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_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") # used_height(subplot::Subplot) = error("used_height(::Subplot) must be implemented by each backend")
@ -63,14 +65,14 @@ used_height(sp::Subplot) = xaxis_height(sp) + title_height(sp)
# # this should return a bounding box (relative to the canvas) for the plot area (inside the spines/ticks) # # 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") # plotarea_bbox(subplot::Subplot) = error("plotarea_bbox(::Subplot) must be implemented by each backend")
# bounding box (relative to canvas) for plot area # # 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 # # note: we assume the x axis is on the left, and y axis is on the bottom
function plotarea_bbox(sp::Subplot) # function plotarea_bbox(sp::Subplot)
xh = xaxis_height(sp) # xh = xaxis_height(sp)
yw = yaxis_width(sp) # yw = yaxis_width(sp)
crop(bbox(sp), BoundingBox(yw, xh, width(sp) - yw, # crop(bbox(sp), BoundingBox(yw, xh, width(sp) - yw,
height(sp) - xh - title_height(sp))) # height(sp) - xh - title_height(sp)))
end # end
# NOTE: this is unnecessary I think as it is the same as bbox(::Subplot) # 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) # # this should return a bounding box (relative to the canvas) for the whole subplot (plotarea, ticks, and guides)
@ -85,80 +87,145 @@ function Base.setindex!(layout::GridLayout, v, r::Int, c::Int)
layout.grid[r,c] = v layout.grid[r,c] = v
end end
function used_width(layout::GridLayout) min_padding_left(layout::GridLayout) = maximum(map(min_padding_left, layout.grid[:,1]))
w = 0mm min_padding_top(layout::GridLayout) = maximum(map(min_padding_top, layout.grid[1,:]))
nr,nc = size(layout) min_padding_right(layout::GridLayout) = maximum(map(min_padding_right, layout.grid[:,end]))
for c=1:nc min_padding_bottom(layout::GridLayout) = maximum(map(min_padding_bottom, layout.grid[end,:]))
@show w
w += maximum([used_width(layout[r,c]) for r=1:nr])
for r=1:nr
@show used_width(layout[r,c])
end
@show w
end
w
end
function used_height(layout::GridLayout) # function used_width(layout::GridLayout)
h = 0mm # w = 0mm
nr,nc = size(layout) # nr,nc = size(layout)
for r=1:nr # for c=1:nc
h += maximum([used_height(layout[r,c]) for c=1:nc]) # @show w
end # w += maximum([used_width(layout[r,c]) for r=1:nr])
h # for r=1:nr
end # @show used_width(layout[r,c])
# end
# @show w
# end
# w
# end
#
# function used_height(layout::GridLayout)
# h = 0mm
# 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) 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 # recursively compute the bounding boxes for the layout and plotarea (relative to canvas!)
# note: this should be called after all axis objects are updated to re-compute the function update_child_bboxes!(layout::GridLayout)
# bounding boxes for the layout tree
function upadte_child_bboxes!(layout::GridLayout) #, parent_bbox::BoundingBox = defaultbox)
# initialize the free space (per child!)
nr, nc = size(layout) nr, nc = size(layout)
freew, freeh = free_size(layout)
@show freew, freeh
freew /= sum(layout.widths)
freeh /= sum(layout.heights)
@show freew, freeh
@show layout.bbox # create a matrix for each minimum padding direction
minpad_left = map(min_padding_left, layout.grid)
minpad_top = map(min_padding_top, layout.grid)
minpad_right = map(min_padding_right, layout.grid)
minpad_bottom = map(min_padding_bottom, layout.grid)
# TODO: this should really track used/free space for each row/column so that we can align plot areas properly # get the max horizontal (left and right) padding over columns,
# and max vertical (bottom and top) padding over rows
# TODO: add extra padding here
pad_left = maximum(minpad_left, 1)
pad_top = maximum(minpad_top, 2)
pad_right = maximum(minpad_right, 1)
pad_bottom = maximum(minpad_bottom, 2)
# l, b = 0.0, 0.0 # scale this up to the total padding in each direction
rights = Measure[0mm for i=1:nc] #zeros(nc) .* pct total_pad_horizontal = (pad_left + pad_right) * nc
bottoms = Measure[1pct for i=1:nr] # ones(nr) .* pct total_pad_vertical = (pad_top + pad_bottom) * nr
# now we can compute the total plot area in each direction
total_plotarea_horizontal = width(layout) - total_pad_horizontal
total_plotarea_vertical = height(layout) - total_pad_vertical
# normalize widths/heights so they sum to 1
layout.widths ./ sum(layout.widths)
layout.heights ./ sum(layout.heights)
# we have all the data we need... lets compute the plot areas
for r=1:nr, c=1:nc for r=1:nr, c=1:nc
# compute the child's bounding box relative to the parent
child = layout[r,c] child = layout[r,c]
usedw, usedh = used_size(child)
@show r,c, usedw, usedh
plot_l = (c == 1 ? 0mm : rights[c-1]) # get the top-left corner of this child
plot_t = (r == 1 ? height(layout) : bottoms[r-1]) child_left = (c == 1 ? 0mm : right(layout[r, c-1])
# bottom = (r == 1 ? 0 : bottoms[r-1]) child_top = (r == 1 ? 0mm : top(layout[r-1, c]))
plot_w = freew * layout.widths[c]
plot_h = freeh * layout.heights[r]
right = plot_l + usedw + plot_w
bottom = plot_t - usedh - plot_h
# plot_t = bottom + usedh + freeh * layout.heights[r]
child_bbox = BoundingBox(plot_l, bottom, plot_w, plot_h)
@show child_bbox
rights[c] = right # compute plot area
bottoms[r] = bottom plotarea_left = child_left + pad_left[c]
plotarea_top = child_top + pad_top[r]
plotarea_width = total_pad_horizontal * layout.widths[c]
plotarea_height = total_pad_vertical * layout.heights[r]
child.plotarea = BoundingBox(plotarea_left, plotarea_top, plotarea_width, plotarea_height)
# then compute the bounding box relative to the canvas, and cache it in the child object # compute child bbox
bbox!(child, crop(bbox(layout), child_bbox)) child_width = pad_left[c] + plotarea_width + pad_right[c]
@show child.bbox child_height = pad_top[r] + plotarea_height + pad_bottom[r]
child.bbox = BoundingBox(child_left, child_top, child_width, child_height)
# now recursively update the child # the bounding boxes are currently relative to the parent, but we need them relative to the canvas
upadte_child_bboxes!(child) child.plotarea = crop(layout.bbox, child.plotarea)
child.bbox = crop(layout.bbox, child.plotarea)
# recursively update the child's children
update_child_bboxes!(child)
end end
end end
# # 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_child_bboxes!(layout::GridLayout) #, parent_bbox::BoundingBox = defaultbox)
# # initialize the free space (per child!)
# nr, nc = size(layout)
# freew, freeh = free_size(layout)
# @show freew, freeh
# freew /= sum(layout.widths)
# freeh /= sum(layout.heights)
# @show freew, freeh
#
# @show layout.bbox
#
# # 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
# rights = Measure[0mm for i=1:nc] #zeros(nc) .* pct
# bottoms = Measure[0mm for i=1:nr] # ones(nr) .* pct
# 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)
# @show r,c, usedw, usedh
#
# plot_l = (c == 1 ? 0mm : rights[c-1])
# plot_t = (r == 1 ? height(layout) : bottoms[r-1])
# # bottom = (r == 1 ? 0 : bottoms[r-1])
# plot_w = freew * layout.widths[c]
# plot_h = freeh * layout.heights[r]
# right = plot_l + usedw + plot_w
# bottom = plot_t - usedh - plot_h
# # plot_t = bottom + usedh + freeh * layout.heights[r]
# child_bbox = BoundingBox(plot_l, bottom, plot_w, plot_h)
# @show child_bbox
#
# rights[c] = right
# bottoms[r] = bottom
#
# # then compute the bounding box relative to the canvas, and cache it in the child object
# bbox!(child, crop(bbox(layout), child_bbox))
# @show child.bbox
#
# # now recursively update the child
# update_child_bboxes!(child)
# end
# end
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# return the top-level layout, a list of subplots, and a SubplotMap # return the top-level layout, a list of subplots, and a SubplotMap

View File

@ -35,15 +35,7 @@ end
# Layouts # Layouts
# ----------------------------------------------------------- # -----------------------------------------------------------
# # wraps bounding box coords (percent of parent area) # NOTE: (0,0) is the top-left !!!
# # 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)
import Measures import Measures
import Measures: Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, width, height import Measures: Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, width, height
@ -76,9 +68,9 @@ const defaultbox = BoundingBox(0mm, 0mm, 0mm, 0mm)
left(bbox::BoundingBox) = bbox.x0[1] left(bbox::BoundingBox) = bbox.x0[1]
bottom(bbox::BoundingBox) = bbox.x0[2] top(bbox::BoundingBox) = bbox.x0[2]
right(bbox::BoundingBox) = left(bbox) + width(bbox) right(bbox::BoundingBox) = left(bbox) + width(bbox)
top(bbox::BoundingBox) = bottom(bbox) + height(bbox) bottom(bbox::BoundingBox) = top(bbox) + height(bbox)
Base.size(bbox::BoundingBox) = (width(bbox), height(bbox)) Base.size(bbox::BoundingBox) = (width(bbox), height(bbox))
# Base.(:*){T,N}(m1::Length{T,N}, m2::Length{T,N}) = Length{T,N}(m1.value * m2.value) # Base.(:*){T,N}(m1::Length{T,N}, m2::Length{T,N}) = Length{T,N}(m1.value * m2.value)
@ -101,7 +93,7 @@ function Base.(:+)(bb1::BoundingBox, bb2::BoundingBox)
b = min(bottom(bb1), bottom(bb2)) b = min(bottom(bb1), bottom(bb2))
r = max(right(bb1), right(bb2)) r = max(right(bb1), right(bb2))
t = max(top(bb1), top(bb2)) t = max(top(bb1), top(bb2))
BoundingBox(l, b, r-l, t-b) BoundingBox(l, t, r-l, t-b)
end end
# this creates a bounding box in the parent's scope, where the child bounding box # this creates a bounding box in the parent's scope, where the child bounding box
@ -112,14 +104,14 @@ function crop(parent::BoundingBox, child::BoundingBox)
# w = width(parent) * width(child) # w = width(parent) * width(child)
# h = height(parent) * height(child) # h = height(parent) * height(child)
l = left(parent) + left(child) l = left(parent) + left(child)
b = bottom(parent) + bottom(child) t = top(parent) + top(child)
w = width(child) w = width(child)
h = height(child) h = height(child)
BoundingBox(l, b, w, h) BoundingBox(l, t, w, h)
end end
function Base.show(io::IO, bbox::BoundingBox) function Base.show(io::IO, bbox::BoundingBox)
print(io, "BBox{l,b,r,t,w,h = $(left(bbox)), $(bottom(bbox)), $(right(bbox)), $(top(bbox)), $(width(bbox)), $(height(bbox))}") print(io, "BBox{l,t,r,b,w,h = $(left(bbox)),$(top(bbox)), $(right(bbox)),$(bottom(bbox)), $(width(bbox)),$(height(bbox))}")
end end
# ----------------------------------------------------------- # -----------------------------------------------------------
@ -149,9 +141,11 @@ end
type Subplot{T<:AbstractBackend} <: AbstractLayout type Subplot{T<:AbstractBackend} <: AbstractLayout
parent::AbstractLayout parent::AbstractLayout
bbox::BoundingBox # the canvas area which is available to this subplot bbox::BoundingBox # the canvas area which is available to this subplot
plotarea::BoundingBox # the part where the data goes
subplotargs::KW # args specific to this subplot subplotargs::KW # args specific to this subplot
# axisviews::Vector{AxisView} # axisviews::Vector{AxisView}
o # can store backend-specific data... like a pyplot ax o # can store backend-specific data... like a pyplot ax
plt # the enclosing Plot object (can't give it a type because of no forward declarations)
# Subplot(parent = RootLayout(); attr = KW()) # Subplot(parent = RootLayout(); attr = KW())
end end
@ -162,6 +156,8 @@ end
# ----------------------------------------------------------- # -----------------------------------------------------------
# TODO: i'll want a plotarea! method to set the plotarea only if the field exists
# nested, gridded layout with optional size percentages # nested, gridded layout with optional size percentages
type GridLayout <: AbstractLayout type GridLayout <: AbstractLayout
parent::AbstractLayout parent::AbstractLayout