diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 68b5c32a..ad02fefb 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -249,14 +249,22 @@ drawax(ax) = ax[:draw](renderer(ax[:get_figure]())) # BoundingBox(left, bottom, right, top) # 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) + fl, fr, fb, ft = py_bbox_fig(obj[:get_figure]()) 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 # 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) # fig = ax[:get_figure]() @@ -289,31 +297,40 @@ function py_bbox_title(ax) py_bbox(ax[:title]) end -xaxis_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_axis(sp.o,"x")) -yaxis_width(sp::Subplot{PyPlotBackend}) = width(py_bbox_axis(sp.o,"y")) -title_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_title(sp.o)) +# TODO: need to compute each of these by subtracting the plotarea position from +# the most extreme guide/axis in each direction. Can pass method (left,top,right,bottom) +# 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 -# 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 -# TODO: really the padding below should be part of the freespace calc, and we should probably -# cache the plotarea bbox while we're doing that (need to add plotarea field to Subplot) -function plotarea_bbox(sp::Subplot{PyPlotBackend}) - ax = sp.o - plot_bb = py_bbox(ax) - xbb = py_bbox_axis(ax, "x") - ybb = py_bbox_axis(ax, "y") - titbb = py_bbox_title(ax) - 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 +min_padding_left(layout::Subplot{PyPlotBackend}) = 0mm +min_padding_top(layout::Subplot{PyPlotBackend}) = 0mm +min_padding_right(layout::Subplot{PyPlotBackend}) = 0mm +min_padding_bottom(layout::Subplot{PyPlotBackend}) = 0mm + +# xaxis_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_axis(sp.o,"x")) +# yaxis_width(sp::Subplot{PyPlotBackend}) = width(py_bbox_axis(sp.o,"y")) +# title_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_title(sp.o)) + +# # note: this overrides the default version to allow for labels that stick out the sides +# # 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 +# # TODO: really the padding below should be part of the freespace calc, and we should probably +# # cache the plotarea bbox while we're doing that (need to add plotarea field to Subplot) +# function plotarea_bbox(sp::Subplot{PyPlotBackend}) +# ax = sp.o +# plot_bb = py_bbox(ax) +# xbb = py_bbox_axis(ax, "x") +# ybb = py_bbox_axis(ax, "y") +# titbb = py_bbox_title(ax) +# 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 drawfig(plt.o) plt.layout.bbox = py_bbox_fig(plt) - upadte_child_bboxes!(plt.layout) + update_child_bboxes!(plt.layout) update_position!(plt.layout) PyPlot.draw() end diff --git a/src/plot.jl b/src/plot.jl index 477a8c30..83550e43 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -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) _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 # we simply add the GroupBy object to the front of the args list to allow # the recipe to be applied @@ -316,7 +321,7 @@ function _plot!(plt::Plot, d::KW, args...) end # 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 sp = 1 # TODO: something useful end diff --git a/src/subplots.jl b/src/subplots.jl index 2d148603..9ab4b46f 100644 --- a/src/subplots.jl +++ b/src/subplots.jl @@ -11,21 +11,26 @@ bbox!(layout::AbstractLayout, bb::BoundingBox) = (layout.bbox = bb) 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) = width(layout.bbox) - used_width(layout) -free_height(layout::AbstractLayout) = height(layout.bbox) - used_height(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) = width(layout.bbox) - used_width(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 -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.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 -used_width(sp::Subplot) = yaxis_width(sp) -used_height(sp::Subplot) = xaxis_height(sp) + title_height(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") @@ -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) # 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) - xh = xaxis_height(sp) - yw = yaxis_width(sp) - crop(bbox(sp), BoundingBox(yw, xh, width(sp) - yw, - height(sp) - xh - title_height(sp))) -end +# # 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) +# xh = xaxis_height(sp) +# yw = yaxis_width(sp) +# crop(bbox(sp), BoundingBox(yw, xh, width(sp) - yw, +# height(sp) - xh - 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) @@ -85,80 +87,145 @@ function Base.setindex!(layout::GridLayout, v, r::Int, c::Int) layout.grid[r,c] = v end -function used_width(layout::GridLayout) - w = 0mm - nr,nc = size(layout) - for c=1:nc - @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 +min_padding_left(layout::GridLayout) = maximum(map(min_padding_left, layout.grid[:,1])) +min_padding_top(layout::GridLayout) = maximum(map(min_padding_top, layout.grid[1,:])) +min_padding_right(layout::GridLayout) = maximum(map(min_padding_right, layout.grid[:,end])) +min_padding_bottom(layout::GridLayout) = maximum(map(min_padding_bottom, layout.grid[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 +# function used_width(layout::GridLayout) +# w = 0mm +# nr,nc = size(layout) +# for c=1:nc +# @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) +# 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) -# 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 upadte_child_bboxes!(layout::GridLayout) #, parent_bbox::BoundingBox = defaultbox) - # initialize the free space (per child!) + +# recursively compute the bounding boxes for the layout and plotarea (relative to canvas!) +function update_child_bboxes!(layout::GridLayout) 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 - rights = Measure[0mm for i=1:nc] #zeros(nc) .* pct - bottoms = Measure[1pct for i=1:nr] # ones(nr) .* pct + # scale this up to the total padding in each direction + total_pad_horizontal = (pad_left + pad_right) * nc + 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 - # 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 + # get the top-left corner of this child + child_left = (c == 1 ? 0mm : right(layout[r, c-1]) + child_top = (r == 1 ? 0mm : top(layout[r-1, c])) - rights[c] = right - bottoms[r] = bottom + # compute plot area + 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 - bbox!(child, crop(bbox(layout), child_bbox)) - @show child.bbox + # compute child bbox + child_width = pad_left[c] + plotarea_width + pad_right[c] + 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 - upadte_child_bboxes!(child) + # the bounding boxes are currently relative to the parent, but we need them relative to the canvas + 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 +# # 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 diff --git a/src/types.jl b/src/types.jl index 15199397..9e66fad9 100644 --- a/src/types.jl +++ b/src/types.jl @@ -35,15 +35,7 @@ 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) +# NOTE: (0,0) is the top-left !!! import Measures 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] -bottom(bbox::BoundingBox) = bbox.x0[2] +top(bbox::BoundingBox) = bbox.x0[2] 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.(:*){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)) r = max(right(bb1), right(bb2)) t = max(top(bb1), top(bb2)) - BoundingBox(l, b, r-l, t-b) + BoundingBox(l, t, r-l, t-b) end # 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) # h = height(parent) * height(child) l = left(parent) + left(child) - b = bottom(parent) + bottom(child) + t = top(parent) + top(child) w = width(child) h = height(child) - BoundingBox(l, b, w, h) + BoundingBox(l, t, w, h) end 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 # ----------------------------------------------------------- @@ -149,9 +141,11 @@ end type Subplot{T<:AbstractBackend} <: AbstractLayout parent::AbstractLayout 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 # axisviews::Vector{AxisView} 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()) 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 type GridLayout <: AbstractLayout parent::AbstractLayout