543 lines
18 KiB
Julia
543 lines
18 KiB
Julia
|
|
# NOTE: (0,0) is the top-left !!!
|
|
|
|
# allow pixels and percentages
|
|
const px = AbsoluteLength(0.254)
|
|
const pct = Length{:pct, Float64}(1.0)
|
|
|
|
const _cbar_width = 5mm
|
|
|
|
Base.(:.*)(m::Measure, n::Number) = m * n
|
|
Base.(:.*)(n::Number, m::Measure) = m * n
|
|
Base.(:-)(m::Measure, a::AbstractArray) = map(ai -> m - ai, a)
|
|
Base.(:-)(a::AbstractArray, m::Measure) = map(ai -> ai - m, a)
|
|
Base.zero(::Type{typeof(mm)}) = 0mm
|
|
Base.one(::Type{typeof(mm)}) = 1mm
|
|
Base.typemin(::typeof(mm)) = -Inf*mm
|
|
Base.typemax(::typeof(mm)) = Inf*mm
|
|
Base.convert{F<:AbstractFloat}(::Type{F}, l::AbsoluteLength) = convert(F, l.value)
|
|
|
|
# TODO: these are unintuitive and may cause tricky bugs
|
|
# 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)
|
|
|
|
|
|
Base.zero(::Type{typeof(pct)}) = 0pct
|
|
Base.one(::Type{typeof(pct)}) = 1pct
|
|
Base.typemin(::typeof(pct)) = 0pct
|
|
Base.typemax(::typeof(pct)) = 1pct
|
|
|
|
const defaultbox = BoundingBox(0mm, 0mm, 0mm, 0mm)
|
|
|
|
left(bbox::BoundingBox) = bbox.x0[1]
|
|
top(bbox::BoundingBox) = bbox.x0[2]
|
|
right(bbox::BoundingBox) = left(bbox) + width(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)
|
|
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
|
|
|
|
l = min(left(bb1), left(bb2))
|
|
t = min(top(bb1), top(bb2))
|
|
r = max(right(bb1), right(bb2))
|
|
b = max(bottom(bb1), bottom(bb2))
|
|
BoundingBox(l, t, r-l, b-t)
|
|
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) + left(child)
|
|
t = top(parent) + top(child)
|
|
w = width(child)
|
|
h = height(child)
|
|
BoundingBox(l, t, w, h)
|
|
end
|
|
|
|
# convert a bounding box from absolute coords to percentages...
|
|
# returns an array of percentages of figure size: [left, bottom, width, height]
|
|
function bbox_to_pcts(bb::BoundingBox, figw, figh, flipy = true)
|
|
mms = Float64[f(bb).value for f in (left,bottom,width,height)]
|
|
if flipy
|
|
mms[2] = figh.value - mms[2] # flip y when origin in bottom-left
|
|
end
|
|
mms ./ Float64[figw.value, figh.value, figw.value, figh.value]
|
|
end
|
|
|
|
function Base.show(io::IO, bbox::BoundingBox)
|
|
print(io, "BBox{l,t,r,b,w,h = $(left(bbox)),$(top(bbox)), $(right(bbox)),$(bottom(bbox)), $(width(bbox)),$(height(bbox))}")
|
|
end
|
|
|
|
# -----------------------------------------------------------
|
|
# AbstractLayout
|
|
|
|
Base.show(io::IO, layout::AbstractLayout) = print(io, "$(typeof(layout))$(size(layout))")
|
|
|
|
# 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))
|
|
|
|
# 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))
|
|
|
|
_update_position!(layout::AbstractLayout) = nothing
|
|
update_child_bboxes!(layout::AbstractLayout) = nothing
|
|
|
|
width(layout::AbstractLayout) = width(layout.bbox)
|
|
height(layout::AbstractLayout) = height(layout.bbox)
|
|
|
|
plotarea(layout::AbstractLayout) = defaultbox
|
|
plotarea!(layout::AbstractLayout, bbox::BoundingBox) = nothing
|
|
|
|
attr(layout::AbstractLayout, k::Symbol) = layout.attr[k]
|
|
attr(layout::AbstractLayout, k::Symbol, v) = get(layout.attr, k, v)
|
|
attr!(layout::AbstractLayout, v, k::Symbol) = (layout.attr[k] = v)
|
|
hasattr(layout::AbstractLayout, k::Symbol) = haskey(layout.attr, k)
|
|
|
|
leftpad(layout::AbstractLayout) = 0mm
|
|
toppad(layout::AbstractLayout) = 0mm
|
|
rightpad(layout::AbstractLayout) = 0mm
|
|
bottompad(layout::AbstractLayout) = 0mm
|
|
|
|
# -----------------------------------------------------------
|
|
# RootLayout
|
|
|
|
# this is the parent of the top-level layout
|
|
immutable RootLayout <: AbstractLayout end
|
|
|
|
Base.parent(::RootLayout) = nothing
|
|
parent_bbox(::RootLayout) = defaultbox
|
|
bbox(::RootLayout) = defaultbox
|
|
|
|
# -----------------------------------------------------------
|
|
# EmptyLayout
|
|
|
|
# contains blank space
|
|
type EmptyLayout <: AbstractLayout
|
|
parent::AbstractLayout
|
|
bbox::BoundingBox
|
|
attr::KW # store label, width, and height for initialization
|
|
# label # this is the label that the subplot will take (since we create a layout before initialization)
|
|
end
|
|
EmptyLayout(parent = RootLayout(); kw...) = EmptyLayout(parent, defaultbox, KW(kw))
|
|
|
|
Base.size(layout::EmptyLayout) = (0,0)
|
|
Base.length(layout::EmptyLayout) = 0
|
|
Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing
|
|
|
|
_update_min_padding!(layout::EmptyLayout) = nothing
|
|
|
|
# -----------------------------------------------------------
|
|
# GridLayout
|
|
|
|
# nested, gridded layout with optional size percentages
|
|
type GridLayout <: AbstractLayout
|
|
parent::AbstractLayout
|
|
minpad::Tuple # leftpad, toppad, rightpad, bottompad
|
|
bbox::BoundingBox
|
|
grid::Matrix{AbstractLayout} # Nested layouts. Each position is a AbstractLayout, which allows for arbitrary recursion
|
|
widths::Vector{Measure}
|
|
heights::Vector{Measure}
|
|
attr::KW
|
|
end
|
|
|
|
grid(args...; kw...) = GridLayout(args...; kw...)
|
|
|
|
function GridLayout(dims...;
|
|
parent = RootLayout(),
|
|
widths = zeros(dims[2]),
|
|
heights = zeros(dims[1]),
|
|
kw...)
|
|
grid = Matrix{AbstractLayout}(dims...)
|
|
layout = GridLayout(
|
|
parent,
|
|
(20mm, 5mm, 2mm, 10mm),
|
|
defaultbox,
|
|
grid,
|
|
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
|
|
end
|
|
|
|
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
|
|
|
|
leftpad(layout::GridLayout) = layout.minpad[1]
|
|
toppad(layout::GridLayout) = layout.minpad[2]
|
|
rightpad(layout::GridLayout) = layout.minpad[3]
|
|
bottompad(layout::GridLayout) = layout.minpad[4]
|
|
|
|
|
|
|
|
# leftpad, toppad, rightpad, bottompad
|
|
function _update_min_padding!(layout::GridLayout)
|
|
map(_update_min_padding!, layout.grid)
|
|
layout.minpad = (
|
|
maximum(map(leftpad, layout.grid[:,1])),
|
|
maximum(map(toppad, layout.grid[1,:])),
|
|
maximum(map(rightpad, layout.grid[:,end])),
|
|
maximum(map(bottompad, layout.grid[end,:]))
|
|
)
|
|
end
|
|
|
|
|
|
function _update_position!(layout::GridLayout)
|
|
map(_update_position!, layout.grid)
|
|
end
|
|
|
|
function recompute_lengths(v)
|
|
# dump(v)
|
|
tot = 0pct
|
|
cnt = 0
|
|
for vi in v
|
|
if vi == 0pct
|
|
cnt += 1
|
|
else
|
|
tot += vi
|
|
end
|
|
end
|
|
leftover = 1.0pct - tot
|
|
if cnt > 1 && leftover.value <= 0
|
|
error("Not enough length left over in layout! v = $v, cnt = $cnt, leftover = $leftover")
|
|
end
|
|
|
|
# now fill in the blanks
|
|
Measure[(vi == 0pct ? leftover / cnt : vi) for vi in v]
|
|
end
|
|
|
|
# recursively compute the bounding boxes for the layout and plotarea (relative to canvas!)
|
|
function update_child_bboxes!(layout::GridLayout)
|
|
nr, nc = size(layout)
|
|
|
|
# create a matrix for each minimum padding direction
|
|
_update_min_padding!(layout)
|
|
|
|
minpad_left = map(leftpad, layout.grid)
|
|
minpad_top = map(toppad, layout.grid)
|
|
minpad_right = map(rightpad, layout.grid)
|
|
minpad_bottom = map(bottompad, layout.grid)
|
|
# @show minpad_left minpad_top minpad_right minpad_bottom
|
|
|
|
# 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)
|
|
# @show pad_left pad_top pad_right pad_bottom
|
|
|
|
# scale this up to the total padding in each direction
|
|
total_pad_horizontal = sum(pad_left + pad_right)
|
|
total_pad_vertical = sum(pad_top + pad_bottom)
|
|
# @show total_pad_horizontal total_pad_vertical
|
|
|
|
# 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
|
|
# @show total_plotarea_horizontal total_plotarea_vertical
|
|
|
|
# recompute widths/heights
|
|
layout.widths = recompute_lengths(layout.widths)
|
|
layout.heights = recompute_lengths(layout.heights)
|
|
# @show layout.widths layout.heights
|
|
|
|
# normalize widths/heights so they sum to 1
|
|
# denom_w = sum(layout.widths)
|
|
# denom_h = sum(layout.heights)
|
|
# @show layout.widths layout.heights denom_w, denom_h
|
|
|
|
# we have all the data we need... lets compute the plot areas and set the bounding boxes
|
|
for r=1:nr, c=1:nc
|
|
child = layout[r,c]
|
|
|
|
# get the top-left corner of this child... the first one is top-left of the parent (i.e. layout)
|
|
child_left = (c == 1 ? left(layout.bbox) : right(layout[r, c-1].bbox))
|
|
child_top = (r == 1 ? top(layout.bbox) : bottom(layout[r-1, c].bbox))
|
|
|
|
# compute plot area
|
|
plotarea_left = child_left + pad_left[c]
|
|
plotarea_top = child_top + pad_top[r]
|
|
plotarea_width = total_plotarea_horizontal * layout.widths[c]
|
|
plotarea_height = total_plotarea_vertical * layout.heights[r]
|
|
plotarea!(child, BoundingBox(plotarea_left, plotarea_top, plotarea_width, plotarea_height))
|
|
|
|
# compute child bbox
|
|
child_width = pad_left[c] + plotarea_width + pad_right[c]
|
|
child_height = pad_top[r] + plotarea_height + pad_bottom[r]
|
|
bbox!(child, BoundingBox(child_left, child_top, child_width, child_height))
|
|
|
|
# recursively update the child's children
|
|
update_child_bboxes!(child)
|
|
end
|
|
end
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
calc_num_subplots(layout::AbstractLayout) = 1
|
|
function calc_num_subplots(layout::GridLayout)
|
|
tot = 0
|
|
for l in layout.grid
|
|
tot += calc_num_subplots(l)
|
|
end
|
|
tot
|
|
end
|
|
|
|
function compute_gridsize(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
|
|
nr, nc
|
|
end
|
|
|
|
# ----------------------------------------------------------------------
|
|
# constructors
|
|
|
|
# pass the layout arg through
|
|
function layout_args(d::KW)
|
|
layout_args(get(d, :layout, default(:layout)))
|
|
end
|
|
|
|
function layout_args(d::KW, n_override::Integer)
|
|
layout, n = layout_args(get(d, :layout, n_override))
|
|
if n != n_override
|
|
error("When doing layout, n != n_override. You're probably trying to force existing plots into a layout that doesn't fit them.")
|
|
end
|
|
layout, n
|
|
end
|
|
|
|
function layout_args(n::Integer)
|
|
nr, nc = compute_gridsize(n, -1, -1)
|
|
GridLayout(nr, nc), n
|
|
end
|
|
|
|
function layout_args{I<:Integer}(sztup::NTuple{2,I})
|
|
nr, nc = sztup
|
|
GridLayout(nr, nc), nr*nc
|
|
end
|
|
|
|
function layout_args{I<:Integer}(sztup::NTuple{3,I})
|
|
n, nr, nc = sztup
|
|
nr, nc = compute_gridsize(n, nr, nc)
|
|
GridLayout(nr, nc), n
|
|
end
|
|
|
|
# compute number of subplots
|
|
function layout_args(layout::GridLayout)
|
|
# recursively get the size of the grid
|
|
n = calc_num_subplots(layout)
|
|
layout, n
|
|
end
|
|
|
|
layout_args(huh) = error("unhandled layout type $(typeof(huh)): $huh")
|
|
|
|
# # pass the layout arg through
|
|
# function build_layout(d::KW)
|
|
# build_layout(get(d, :layout, default(:layout)))
|
|
# end
|
|
#
|
|
# function build_layout(n::Integer)
|
|
# nr, nc = compute_gridsize(n, -1, -1)
|
|
# build_layout(GridLayout(nr, nc), n)
|
|
# end
|
|
#
|
|
# function build_layout{I<:Integer}(sztup::NTuple{2,I})
|
|
# nr, nc = sztup
|
|
# build_layout(GridLayout(nr, nc))
|
|
# end
|
|
#
|
|
# function build_layout{I<:Integer}(sztup::NTuple{3,I})
|
|
# n, nr, nc = sztup
|
|
# nr, nc = compute_gridsize(n, nr, nc)
|
|
# build_layout(GridLayout(nr, nc), n)
|
|
# end
|
|
#
|
|
# # compute number of subplots
|
|
# function build_layout(layout::GridLayout)
|
|
# # recursively get the size of the grid
|
|
# n = calc_num_subplots(layout)
|
|
# build_layout(layout, n)
|
|
# end
|
|
|
|
function build_layout(args...)
|
|
layout, n = layout_args(args...)
|
|
build_layout(layout, n)
|
|
end
|
|
|
|
# n is the number of subplots
|
|
function build_layout(layout::GridLayout, n::Integer)
|
|
nr, nc = size(layout)
|
|
subplots = Subplot[]
|
|
spmap = SubplotMap()
|
|
i = 0
|
|
for r=1:nr, c=1:nc
|
|
l = layout[r,c]
|
|
if isa(l, EmptyLayout)
|
|
sp = Subplot(backend(), parent=layout)
|
|
layout[r,c] = sp
|
|
push!(subplots, sp)
|
|
spmap[attr(l,:label,gensym())] = sp
|
|
if get(l.attr, :width, :auto) != :auto
|
|
layout.widths[c] = attr(l,:width)
|
|
end
|
|
if get(l.attr, :height, :auto) != :auto
|
|
layout.heights[r] = attr(l,:height)
|
|
end
|
|
i += 1
|
|
elseif isa(l, GridLayout)
|
|
# sub-grid
|
|
l, sps, m = build_layout(l, n-i)
|
|
append!(subplots, sps)
|
|
merge!(spmap, m)
|
|
i += length(sps)
|
|
end
|
|
i >= n && break # only add n subplots
|
|
end
|
|
layout, subplots, spmap
|
|
end
|
|
|
|
# build a layout from a list of existing Plot objects
|
|
# TODO... much of the logic overlaps with the method above... can we merge?
|
|
function build_layout(layout::GridLayout, numsp::Integer, plts::AVec{Plot})
|
|
nr, nc = size(layout)
|
|
subplots = Subplot[]
|
|
spmap = SubplotMap()
|
|
i = 0
|
|
for r=1:nr, c=1:nc
|
|
l = layout[r,c]
|
|
if isa(l, EmptyLayout)
|
|
plt = shift!(plts) # grab the first plot out of the list
|
|
layout[r,c] = plt.layout
|
|
append!(subplots, plt.subplots)
|
|
merge!(spmap, plt.spmap)
|
|
if get(l.attr, :width, :auto) != :auto
|
|
layout.widths[c] = attr(l,:width)
|
|
end
|
|
if get(l.attr, :height, :auto) != :auto
|
|
layout.heights[r] = attr(l,:height)
|
|
end
|
|
i += length(plt.subplots)
|
|
elseif isa(l, GridLayout)
|
|
# sub-grid
|
|
l, sps, m = build_layout(l, numsp-i, plts)
|
|
append!(subplots, sps)
|
|
merge!(spmap, m)
|
|
i += length(sps)
|
|
end
|
|
i >= numsp && break # only add n subplots
|
|
end
|
|
layout, subplots, spmap
|
|
end
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# @layout macro
|
|
|
|
function add_layout_pct!(kw::KW, v::Expr, idx::Integer)
|
|
# dump(v)
|
|
# something like {0.2w}?
|
|
if v.head == :call && v.args[1] == :*
|
|
num = v.args[2]
|
|
if length(v.args) == 3 && isa(num, Number)
|
|
units = v.args[3]
|
|
if units == :h
|
|
return kw[:h] = num*pct
|
|
elseif units == :w
|
|
return kw[:w] = num*pct
|
|
elseif units in (:pct, :px, :mm, :cm, :inch)
|
|
return kw[idx == 1 ? :w : :h] = v
|
|
end
|
|
end
|
|
end
|
|
error("Couldn't match layout curly (idx=$idx): $v")
|
|
end
|
|
|
|
function add_layout_pct!(kw::KW, v::Number, idx::Integer)
|
|
kw[idx == 1 ? :w : :h] = v*pct
|
|
end
|
|
|
|
function create_grid(expr::Expr)
|
|
cellsym = gensym(:cell)
|
|
constructor = if expr.head == :vcat
|
|
:(let
|
|
$cellsym = GridLayout($(length(expr.args)), 1)
|
|
$([:($cellsym[$i,1] = $(create_grid(expr.args[i]))) for i=1:length(expr.args)]...)
|
|
$cellsym
|
|
end)
|
|
elseif expr.head in (:hcat,:row)
|
|
:(let
|
|
$cellsym = GridLayout(1, $(length(expr.args)))
|
|
$([:($cellsym[1,$i] = $(create_grid(expr.args[i]))) for i=1:length(expr.args)]...)
|
|
$cellsym
|
|
end)
|
|
|
|
elseif expr.head == :curly
|
|
# length(expr.args) == 3 || error("Should be width and height in curly. Got: ", expr.args)
|
|
# s,w,h = expr.args
|
|
s = expr.args[1]
|
|
kw = KW()
|
|
for (i,arg) in enumerate(expr.args[2:end])
|
|
add_layout_pct!(kw, arg, i)
|
|
end
|
|
# @show kw
|
|
:(EmptyLayout(label = $(QuoteNode(s)), width = $(get(kw, :w, QuoteNode(:auto))), height = $(get(kw, :h, QuoteNode(:auto)))))
|
|
|
|
else
|
|
# if it's something else, just return that (might be an existing layout?)
|
|
expr
|
|
end
|
|
end
|
|
|
|
function create_grid(s::Symbol)
|
|
:(EmptyLayout(label = $(QuoteNode(s))))
|
|
end
|
|
|
|
macro layout(mat::Expr)
|
|
create_grid(mat)
|
|
end
|