diff --git a/REQUIRE b/REQUIRE index 66cb8d87..ec5ce47d 100644 --- a/REQUIRE +++ b/REQUIRE @@ -5,3 +5,4 @@ Colors Reexport Compat FixedSizeArrays +Measures diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index a6c3377e..68b5c32a 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -241,26 +241,33 @@ drawax(ax) = ax[:draw](renderer(ax[:get_figure]())) # # merge a list of bounding boxes together to become the area that surrounds them all # bbox_union(bboxes) = pytransforms.Bbox[:union](bboxes) -function py_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) +# function py_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 + +function py_bbox(obj) + l, r, b, t = obj[:get_window_extent]()[:get_points]() + BoundingBox(l*px, b*px, (r-l)*px, (t-b)*px) end # bbox_from_pyplot(obj) = +py_bbox_fig(plt::Plot) = py_bbox(plt.o) + function py_bbox_ticks(ax, letter) # fig = ax[:get_figure]() # @show fig labels = ax[symbol("get_"*letter*"ticklabels")]() # @show labels # bboxes = [] - bbox_union = BoundingBox() + bbox_union = defaultbox for lab in labels # @show lab,lab[:get_text]() - bbox = py_bbox_pct(lab) + bbox = py_bbox(lab) bbox_union = bbox_union + bbox # @show bbox_union end @@ -269,7 +276,7 @@ end function py_bbox_axislabel(ax, letter) pyaxis_label = ax[symbol("get_"*letter*"axis")]()[:label] - py_bbox_pct(pyaxis_label) + py_bbox(pyaxis_label) end # get a bounding box for the whole axis @@ -279,7 +286,7 @@ end # get a bounding box for the title area function py_bbox_title(ax) - py_bbox_pct(ax[:title]) + py_bbox(ax[:title]) end xaxis_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_axis(sp.o,"x")) @@ -293,26 +300,37 @@ title_height(sp::Subplot{PyPlotBackend}) = height(py_bbox_title(sp.o)) # 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_pct(ax) + 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(0, left(plot_bb) - minimum(map(left, items))) - bottompad = max(0, bottom(plot_bb) - minimum(map(bottom, items))) - rightpad = max(0, maximum(map(right, items)) - right(plot_bb)) - toppad = max(0, maximum(map(top, items)) - top(plot_bb)) - # crop(bbox(sp), BoundingBox(yaxis_width(sp), xaxis_height(sp), 1, 1 - title_height(sp))) - crop(bbox(sp), BoundingBox(leftpad, bottompad, 1 - rightpad, 1 - toppad)) + 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 # --------------------------------------------------------------------------- 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)]) + figw, figh = size(py_bbox(ax[:get_figure]())) + + plot_bb = plotarea_bbox(sp) + @show sp.bbox plot_bb + # l = float(left(plot_bb) / px) / figw + # b = float(bottom(plot_bb) / px) / figh + # w = float(width(plot_bb) / px) / figw + mms = Float64[f(plot_bb).value for f in (left, bottom, width, height)] + @show mms + pcts = mms ./ Float64[figw.value, figh.value, figw.value, figh.value] + @show pcts + ax[:set_position](pcts) end # each backend should set up the subplot here @@ -1297,7 +1315,8 @@ function finalizePlot(plt::Plot{PyPlotBackend}) ax[:title][:set_color](getPyPlotColor(plt.plotargs[:titlefont].color)) end drawfig(plt.o) - update_bboxes!(plt.layout) + plt.layout.bbox = py_bbox_fig(plt) + upadte_child_bboxes!(plt.layout) update_position!(plt.layout) PyPlot.draw() end diff --git a/src/subplots.jl b/src/subplots.jl index b1a6362e..2a20463b 100644 --- a/src/subplots.jl +++ b/src/subplots.jl @@ -1,39 +1,5 @@ -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 # ---------------------------------------------------------------------- @@ -48,8 +14,8 @@ 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) +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)) @@ -59,7 +25,7 @@ used_size(layout::AbstractLayout) = (used_width(layout), used_height(layout)) # end update_position!(layout::AbstractLayout) = nothing -update_bboxes!(layout::AbstractLayout) = nothing +upadte_child_bboxes!(layout::AbstractLayout) = nothing # ---------------------------------------------------------------------- @@ -67,15 +33,15 @@ 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 +used_width(layout::EmptyLayout) = 0pct +used_height(layout::EmptyLayout) = 0pct # ---------------------------------------------------------------------- Base.parent(::RootLayout) = nothing -parent_bbox(::RootLayout) = BoundingBox(0,0,1,1) -bbox(::RootLayout) = BoundingBox(0,0,1,1) +parent_bbox(::RootLayout) = defaultbox +bbox(::RootLayout) = defaultbox # Base.size(layout::RootLayout) = (1,1) # Base.length(layout::RootLayout) = 1 @@ -100,7 +66,10 @@ used_height(sp::Subplot) = xaxis_height(sp) + title_height(sp) # 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))) + 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) @@ -117,16 +86,21 @@ function Base.setindex!(layout::GridLayout, v, r::Int, c::Int) end function used_width(layout::GridLayout) - w = 0.0 + 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 = 0.0 + h = 0mm nr,nc = size(layout) for r=1:nr h += maximum([used_height(layout[r,c]) for c=1:nc]) @@ -140,39 +114,46 @@ update_position!(layout::GridLayout) = map(update_position!, layout.grid) # 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)) +function upadte_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 # 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 = zeros(nc) - bottoms = ones(nr) + rights = Measure[0mm for i=1:nc] #zeros(nc) .* pct + bottoms = Measure[1pct 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 - left = (c == 1 ? 0 : rights[c-1]) - top = (r == 1 ? 1 : bottoms[r-1]) + plot_l = (c == 1 ? 0mm : rights[c-1]) + plot_t = (r == 1 ? height(layout) : bottoms[r-1]) # bottom = (r == 1 ? 0 : bottoms[r-1]) - right = left + usedw + freew * layout.widths[c] - bottom = top - usedh - freeh * layout.heights[r] - # top = bottom + usedh + freeh * layout.heights[r] - child_bbox = BoundingBox(left, bottom, right, top) + 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_bboxes!(child) + upadte_child_bboxes!(child) end end diff --git a/src/types.jl b/src/types.jl index a54d4a89..6f722a75 100644 --- a/src/types.jl +++ b/src/types.jl @@ -35,20 +35,100 @@ 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 +# # 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) + +import Measures +import Measures: Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, width, height +export BBox, BoundingBox, mm, cm, inch, pt, px, pct + +typealias BBox Measures.Absolute2DBox + +# allow pixels and percentages +const px = AbsoluteLength(0.254) +const pct = Length{:pct, Float64}(1.0) + +Base.(:+)(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value * (1 + m2.value)) +Base.(:+)(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value * (1 + m1.value)) +Base.(:-)(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value * (1 - m2.value)) +Base.(:-)(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value * (m1.value - 1)) +Base.(:*)(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value * m2.value) +Base.(:*)(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value * m1.value) +Base.(:/)(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value / m2.value) +Base.(:/)(m1::Length{:pct}, m2::AbsoluteLength) = AbsoluteLength(m2.value / m1.value) + +const defaultbox = BoundingBox(0mm, 0mm, 20mm, 20mm) + +# 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 + + + +left(bbox::BoundingBox) = bbox.x0[1] +bottom(bbox::BoundingBox) = bbox.x0[2] +right(bbox::BoundingBox) = left(bbox) + width(bbox) +top(bbox::BoundingBox) = bottom(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) +ispositive(m::Measure) = m.value > 0 + +# union together bounding boxes +function Base.(:+)(bb1::BoundingBox, bb2::BoundingBox) + # empty boxes don't change the union + ispositive(width(bb1)) || return bb2 + ispositive(height(bb1)) || return bb2 + ispositive(width(bb2)) || return bb1 + ispositive(height(bb2)) || return bb1 + + # if width(bb1) <= 0mm || height(bb1) <= 0mm + # return bb2 + # elseif width(bb2) <= 0mm || height(bb2) <= 0mm + # return bb1 + # end + l = min(left(bb1), left(bb2)) + 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) +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) + # b = bottom(parent) + height(parent) * bottom(child) + # w = width(parent) * width(child) + # h = height(parent) * height(child) + l = left(parent) + left(child) + b = bottom(parent) + bottom(child) + w = width(child) + h = height(child) + BoundingBox(l, b, 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))}") end -BoundingBox() = BoundingBox(0,0,0,0) # ----------------------------------------------------------- abstract AbstractLayout +width(layout::AbstractLayout) = width(layout.bbox) +height(layout::AbstractLayout) = height(layout.bbox) + # ----------------------------------------------------------- # contains blank space @@ -56,7 +136,7 @@ type EmptyLayout <: AbstractLayout parent::AbstractLayout bbox::BoundingBox end -EmptyLayout(parent = RootLayout()) = EmptyLayout(parent, BoundingBox(0,0,1,1)) +EmptyLayout(parent = RootLayout()) = EmptyLayout(parent, defaultbox) # this is the parent of the top-level layout immutable RootLayout <: AbstractLayout @@ -77,7 +157,7 @@ type Subplot{T<:AbstractBackend} <: AbstractLayout end function Subplot{T<:AbstractBackend}(::T; parent = RootLayout()) - Subplot{T}(parent, BoundingBox(0,0,1,1), KW(), nothing) + Subplot{T}(parent, defaultbox, KW(), nothing) end # ----------------------------------------------------------- @@ -87,8 +167,8 @@ 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{Measure} + heights::Vector{Measure} attr::KW end @@ -100,10 +180,12 @@ function GridLayout(dims...; grid = Matrix{AbstractLayout}(dims...) layout = GridLayout( parent, - BoundingBox(0,0,1,1), + defaultbox, grid, - convert(Vector{Float64}, widths), - convert(Vector{Float64}, heights), + Measure[w*pct for w in widths], + Measure[h*pct for h in heights], + # convert(Vector{Float64}, widths), + # convert(Vector{Float64}, heights), KW(kw)) fill!(grid, EmptyLayout(layout)) layout diff --git a/test/REQUIRE b/test/REQUIRE index 5e5f499e..9ddfd302 100644 --- a/test/REQUIRE +++ b/test/REQUIRE @@ -3,6 +3,7 @@ julia 0.4 RecipesBase Colors Reexport +Measures FactCheck Cairo Gadfly