working on layouts

This commit is contained in:
Thomas Breloff 2016-05-16 14:24:45 -04:00
parent c5bcae1e34
commit ae1f5b8b06
4 changed files with 315 additions and 18 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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}