reorg/cleanup; removed old layouts and subplots; created axes.jl and layouts.jl

This commit is contained in:
Thomas Breloff 2016-05-19 10:42:59 -04:00
parent 0d96c49f4a
commit 3e8f325ddc
9 changed files with 539 additions and 1331 deletions

View File

@ -16,6 +16,7 @@ export
Subplot,
AbstractLayout,
GridLayout,
grid,
EmptyLayout,
@layout,
# RowsLayout,
@ -171,18 +172,25 @@ export
# ---------------------------------------------------------
import Measures
import Measures: Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, width, height
typealias BBox Measures.Absolute2DBox
export BBox, BoundingBox, mm, cm, inch, pt, px, pct
# ---------------------------------------------------------
include("types.jl")
include("utils.jl")
include("colors.jl")
include("components.jl")
include("axes.jl")
include("backends.jl")
include("args.jl")
include("themes.jl")
include("plot.jl")
include("series_args.jl")
include("series_new.jl")
# include("subplot.jl")
# include("layouts.jl")
include("layouts.jl")
include("subplots.jl")
include("recipes.jl")
include("animation.jl")

View File

@ -952,8 +952,12 @@ function _update_subplot_args(plt::Plot, sp::Subplot, d_in::KW, subplot_index::I
for letter in (:x, :y, :z)
# get (maybe initialize) the axis
axissym = symbol(letter, :axis)
axis = get!(spargs, axissym, Axis(letter))
axis = if haskey(spargs, axissym)
spargs[axissym]
else
spargs[axissym] = Axis(letter)
end
# grab magic args (for example `xaxis = (:flip, :log)`)
args = wraptuple(get(d_in, axissym, ()))

129
src/axes.jl Normal file
View File

@ -0,0 +1,129 @@
xaxis(args...; kw...) = Axis(:x, args...; kw...)
yaxis(args...; kw...) = Axis(:y, args...; kw...)
zaxis(args...; kw...) = Axis(:z, args...; kw...)
function Axis(letter::Symbol, args...; kw...)
# init with values from _plot_defaults
d = KW(
:letter => letter,
:extrema => (Inf, -Inf),
:discrete_map => Dict(), # map discrete values to continuous plot values
:discrete_values => Tuple{Float64,Any}[],
:use_minor => false,
:show => true, # show or hide the axis? (useful for linked subplots)
)
merge!(d, _axis_defaults)
# update the defaults
update!(Axis(d), args...; kw...)
end
# update an Axis object with magic args and keywords
function update!(a::Axis, args...; kw...)
# first process args
d = a.d
for arg in args
T = typeof(arg)
arg = get(_scaleAliases, arg, arg)
# scale, flip, label, lim, tick = axis_symbols(letter, "scale", "flip", "label", "lims", "ticks")
if typeof(arg) <: Font
d[:tickfont] = arg
d[:guidefont] = arg
elseif arg in _allScales
d[:scale] = arg
elseif arg in (:flip, :invert, :inverted)
d[:flip] = true
elseif T <: @compat(AbstractString)
d[:label] = arg
# xlims/ylims
elseif (T <: Tuple || T <: AVec) && length(arg) == 2
sym = typeof(arg[1]) <: Number ? :lims : :ticks
d[sym] = arg
# xticks/yticks
elseif T <: AVec
d[:ticks] = arg
elseif arg == nothing
d[:ticks] = []
elseif typeof(arg) <: Number
d[:rotation] = arg
else
warn("Skipped $(letter)axis arg $arg")
end
end
# then override for any keywords... only those keywords that already exists in d
for (k,v) in kw
# sym = symbol(string(k)[2:end])
if haskey(d, k)
d[k] = v
end
end
a
end
Base.show(io::IO, a::Axis) = dumpdict(a.d, "Axis", true)
Base.getindex(a::Axis, k::Symbol) = getindex(a.d, k)
Base.setindex!(a::Axis, v, ks::Symbol...) = setindex!(a.d, v, ks...)
Base.haskey(a::Axis, k::Symbol) = haskey(a.d, k)
Base.extrema(a::Axis) = a[:extrema]
# get discrete ticks, or not
function get_ticks(a::Axis)
ticks = a[:ticks]
dvals = a[:discrete_values]
if !isempty(dvals) && ticks == :auto
vals, labels = unzip(dvals)
else
ticks
end
end
function expand_extrema!(a::Axis, v::Number)
emin, emax = a[:extrema]
a[:extrema] = (min(v, emin), max(v, emax))
end
function expand_extrema!{MIN<:Number,MAX<:Number}(a::Axis, v::Tuple{MIN,MAX})
emin, emax = a[:extrema]
a[:extrema] = (min(v[1], emin), max(v[2], emax))
end
function expand_extrema!{N<:Number}(a::Axis, v::AVec{N})
if !isempty(v)
emin, emax = a[:extrema]
a[:extrema] = (min(minimum(v), emin), max(maximum(v), emax))
end
a[:extrema]
end
# these methods track the discrete values which correspond to axis continuous values (cv)
# whenever we have discrete values, we automatically set the ticks to match.
# we return the plot value
function discrete_value!(a::Axis, v)
cv = get(a[:discrete_map], v, NaN)
if isnan(cv)
emin, emax = a[:extrema]
cv = max(0.5, emax + 1.0)
expand_extrema!(a, cv)
a[:discrete_map][v] = cv
push!(a[:discrete_values], (cv, v))
end
cv
end
# add the discrete value for each item
function discrete_value!(a::Axis, v::AVec)
Float64[discrete_value!(a, vi) for vi=v]
end

View File

@ -257,145 +257,6 @@ function text(str, args...)
end
# -----------------------------------------------------------------------
xaxis(args...) = Axis(:x, args...)
yaxis(args...) = Axis(:y, args...)
zaxis(args...) = Axis(:z, args...)
# const _axis_symbols = (:label, :lims, :ticks, :scale, :flip, :rotation)
# const _axis_symbols_fonts_colors = (
# :guidefont, :tickfont,
# :foreground_color_axis,
# :foreground_color_border,
# :foreground_color_text,
# :foreground_color_guide
# )
function Axis(letter::Symbol, args...; kw...)
# init with values from _plot_defaults
d = KW(
:letter => letter,
:extrema => (Inf, -Inf),
:discrete_map => Dict(), # map discrete values to continuous plot values
:discrete_values => Tuple{Float64,Any}[],
:use_minor => false,
:show => true, # show or hide the axis? (useful for linked subplots)
)
merge!(d, _axis_defaults)
# update the defaults
update!(Axis(d), args...; kw...)
end
# update an Axis object with magic args and keywords
function update!(a::Axis, args...; kw...)
# first process args
d = a.d
for arg in args
T = typeof(arg)
arg = get(_scaleAliases, arg, arg)
# scale, flip, label, lim, tick = axis_symbols(letter, "scale", "flip", "label", "lims", "ticks")
if typeof(arg) <: Font
d[:tickfont] = arg
d[:guidefont] = arg
elseif arg in _allScales
d[:scale] = arg
elseif arg in (:flip, :invert, :inverted)
d[:flip] = true
elseif T <: @compat(AbstractString)
d[:label] = arg
# xlims/ylims
elseif (T <: Tuple || T <: AVec) && length(arg) == 2
sym = typeof(arg[1]) <: Number ? :lims : :ticks
d[sym] = arg
# xticks/yticks
elseif T <: AVec
d[:ticks] = arg
elseif arg == nothing
d[:ticks] = []
elseif typeof(arg) <: Number
d[:rotation] = arg
else
warn("Skipped $(letter)axis arg $arg")
end
end
# then override for any keywords... only those keywords that already exists in d
for (k,v) in kw
# sym = symbol(string(k)[2:end])
if haskey(d, k)
d[k] = v
end
end
a
end
Base.show(io::IO, a::Axis) = dumpdict(a.d, "Axis", true)
Base.getindex(a::Axis, k::Symbol) = getindex(a.d, k)
Base.setindex!(a::Axis, v, ks::Symbol...) = setindex!(a.d, v, ks...)
Base.haskey(a::Axis, k::Symbol) = haskey(a.d, k)
Base.extrema(a::Axis) = a[:extrema]
# get discrete ticks, or not
function get_ticks(a::Axis)
ticks = a[:ticks]
dvals = a[:discrete_values]
if !isempty(dvals) && ticks == :auto
vals, labels = unzip(dvals)
else
ticks
end
end
function expand_extrema!(a::Axis, v::Number)
emin, emax = a[:extrema]
a[:extrema] = (min(v, emin), max(v, emax))
end
function expand_extrema!{MIN<:Number,MAX<:Number}(a::Axis, v::Tuple{MIN,MAX})
emin, emax = a[:extrema]
a[:extrema] = (min(v[1], emin), max(v[2], emax))
end
function expand_extrema!{N<:Number}(a::Axis, v::AVec{N})
if !isempty(v)
emin, emax = a[:extrema]
a[:extrema] = (min(minimum(v), emin), max(maximum(v), emax))
end
a[:extrema]
end
# these methods track the discrete values which correspond to axis continuous values (cv)
# whenever we have discrete values, we automatically set the ticks to match.
# we return the plot value
function discrete_value!(a::Axis, v)
cv = get(a[:discrete_map], v, NaN)
if isnan(cv)
emin, emax = a[:extrema]
cv = max(0.5, emax + 1.0)
expand_extrema!(a, cv)
a[:discrete_map][v] = cv
push!(a[:discrete_values], (cv, v))
end
cv
end
# add the discrete value for each item
function discrete_value!(a::Axis, v::AVec)
Float64[discrete_value!(a, vi) for vi=v]
end
# -----------------------------------------------------------------------
immutable Stroke

381
src/layouts.jl Normal file
View File

@ -0,0 +1,381 @@
# NOTE: (0,0) is the top-left !!!
# allow pixels and percentages
const px = AbsoluteLength(0.254)
const pct = Length{:pct, Float64}(1.0)
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.(:+)(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
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, 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)
# -----------------------------------------------------------
# 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
# -----------------------------------------------------------
# GridLayout
# nested, gridded layout with optional size percentages
type GridLayout <: AbstractLayout
parent::AbstractLayout
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 = ones(dims[2]),
heights = ones(dims[1]),
kw...)
grid = Matrix{AbstractLayout}(dims...)
layout = GridLayout(
parent,
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
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,:]))
update_position!(layout::GridLayout) = map(update_position!, layout.grid)
# 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
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)
# @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 = (pad_left + pad_right) .* nc
total_pad_vertical = (pad_top + pad_bottom) .* nr
# @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
# normalize widths/heights so they sum to 1
denom_w = sum(layout.widths)
denom_h = sum(layout.heights)
# @show denom_w, denom_h
# we have all the data we need... lets compute the plot areas
for r=1:nr, c=1:nc
child = layout[r,c]
# get the top-left corner of this child
child_left = (c == 1 ? 0mm : right(layout[r, c-1].bbox))
child_top = (r == 1 ? 0mm : 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[c] * layout.widths[c] / denom_w
plotarea_height = total_plotarea_vertical[r] * layout.heights[r] / denom_h
child_plotarea = 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]
child_bbox = BoundingBox(child_left, child_top, child_width, child_height)
# @show (r,c) child_plotarea child_bbox
# the bounding boxes are currently relative to the parent, but we need them relative to the canvas
plotarea!(child, crop(layout.bbox, child_plotarea))
bbox!(child, crop(layout.bbox, child_bbox))
# @show child_plotarea child_bbox
# 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 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)
# nr, nc = size(layout)
# build_layout(layout, nr*nc)
# recursively get the size of the grid
n = calc_num_subplots(layout)
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 hasattr(l,:width)
layout.widths[c] = attr(l,:width)
end
if hasattr(l,:height)
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_layout(huh) = error("unhandled layout type $(typeof(huh)): $huh")
# ----------------------------------------------------------------------
# @layout macro
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
:(EmptyLayout(label = $(QuoteNode(s)), width = $w, height = $h))
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

View File

@ -1,178 +0,0 @@
# -----------------------------------------------------------
# GridLayout
# -----------------------------------------------------------
"Simple grid, indices are row-major."
immutable GridLayout <: AbstractLayout
nr::Int
nc::Int
end
Base.length(layout::GridLayout) = layout.nr * layout.nc
Base.start(layout::GridLayout) = 1
Base.done(layout::GridLayout, state) = state > length(layout)
function Base.next(layout::GridLayout, state)
r = div(state-1, layout.nc) + 1
c = mod1(state, layout.nc)
(r,c), state + 1
end
nrows(layout::GridLayout) = layout.nr
ncols(layout::GridLayout) = layout.nc
ncols(layout::GridLayout, row::Int) = layout.nc
# get the plot index given row and column
Base.getindex(layout::GridLayout, r::Int, c::Int) = (r-1) * layout.nc + c
# -----------------------------------------------------------
# RowsLayout
# -----------------------------------------------------------
"Number of plots per row"
immutable RowsLayout <: AbstractLayout
numplts::Int
rowcounts::AbstractVector{Int}
end
Base.length(layout::RowsLayout) = layout.numplts
Base.start(layout::RowsLayout) = 1
Base.done(layout::RowsLayout, state) = state > length(layout)
function Base.next(layout::RowsLayout, state)
r = 1
c = 0
for i = 1:state
c += 1
if c > layout.rowcounts[r]
r += 1
c = 1
end
end
(r,c), state + 1
end
nrows(layout::RowsLayout) = length(layout.rowcounts)
ncols(layout::RowsLayout, row::Int) = row < 1 ? 0 : (row > nrows(layout) ? 0 : layout.rowcounts[row])
# get the plot index given row and column
Base.getindex(layout::RowsLayout, r::Int, c::Int) = sum(layout.rowcounts[1:r-1]) + c
# -----------------------------------------------------------
# FlexLayout
# -----------------------------------------------------------
"Flexible, nested layout with optional size percentages."
immutable FlexLayout <: AbstractLayout
n::Int
grid::Matrix # Nested layouts. Each position
# can be a plot index or another FlexLayout
widths::Vector{Float64}
heights::Vector{Float64}
end
typealias IntOrFlex Union{Int,FlexLayout}
Base.length(layout::FlexLayout) = layout.n
Base.start(layout::FlexLayout) = 1
Base.done(layout::FlexLayout, state) = state > length(layout)
function Base.next(layout::FlexLayout, state)
# TODO: change this method to return more info
# TODO: might consider multiple iterator types.. some backends might have an easier time row-by-row for example
error()
r = 1
c = 0
for i = 1:state
c += 1
if c > layout.rowcounts[r]
r += 1
c = 1
end
end
(r,c), state + 1
end
nrows(layout::FlexLayout) = size(layout.grid, 1)
ncols(layout::FlexLayout, row::Int) = size(layout.grid, 2)
# get the plot index given row and column
Base.getindex(layout::FlexLayout, r::Int, c::Int) = layout.grid[r,c]
# -----------------------------------------------------------
# we're taking in a nested structure of some kind... parse it out and build a FlexLayout
function subplotlayout(mat::AbstractVecOrMat; widths = nothing, heights = nothing)
n = 0
nr, nc = size(mat)
grid = Array(IntOrFlex, nr, nc)
for i=1:nr, j=1:nc
v = mat[i,j]
if isa(v, Integer)
grid[i,j] = Int(v)
n += 1
elseif isa(v, Tuple)
warn("need to handle tuples somehow... (idx, sizepct)")
grid[i,j] = nothing
elseif v == nothing
grid[i,j] = nothing
elseif isa(v, AbstractVecOrMat)
grid[i,j] = layout(v)
n += grid[i,j].n
else
error("How do we process? $v")
end
end
if widths == nothing
widths = ones(nc) ./ nc
end
if heights == nothing
heights = ones(nr) ./ nr
end
FlexLayout(n, grid, widths, heights)
end
function subplotlayout(sz::Tuple{Int,Int})
GridLayout(sz...)
end
function subplotlayout(rowcounts::AVec{Int})
RowsLayout(sum(rowcounts), rowcounts)
end
function subplotlayout(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
# if it's a perfect rectangle, just create a grid
if numplts == nr * nc
return GridLayout(nr, nc)
end
# create the rowcounts vector
i = 0
rowcounts = Int[]
for r in 1:nr
cnt = min(nc, numplts - i)
push!(rowcounts, cnt)
i += cnt
end
RowsLayout(numplts, rowcounts)
end

View File

@ -1,339 +0,0 @@
### WARNING: this file is deprecated ###
# ------------------------------------------------------------
Base.string(subplt::Subplot) = "Subplot{$(subplt.backend) p=$(subplt.p) n=$(subplt.n)}"
Base.print(io::IO, subplt::Subplot) = print(io, string(subplt))
Base.show(io::IO, subplt::Subplot) = print(io, string(subplt))
function Base.copy(subplt::Subplot)
subplot(subplt.plts, subplt.layout, subplt.plotargs)
end
Base.getindex(subplt::Subplot, args...) = subplt.plts[subplt.layout[args...]]
# --------------------------------------------------------------------
getplot(subplt::Subplot, idx::Int = subplt.n) = subplt.plts[mod1(idx, subplt.p)]
getplotargs(subplt::Subplot, idx::Int) = getplot(subplt, idx).plotargs
convertSeriesIndex(subplt::Subplot, n::Int) = ceil(Int, n / subplt.p)
# ------------------------------------------------------------
function validateSubplotSupported()
if !subplotSupported()
error(CURRENT_BACKEND.sym, " does not support the subplot/subplot! commands at this time. Try one of: ", join(filter(pkg->subplotSupported(_backend_instance(pkg)), backends()),", "))
end
end
"""
Create a series of plots:
```
y = rand(100,3)
subplot(y; n = 3) # create an automatic grid, and let it figure out the nr/nc... will put plots 1 and 2 on the first row, and plot 3 by itself on the 2nd row
subplot(y; n = 3, nr = 1) # create an automatic grid, but fix the number of rows to 1 (so there are n columns)
subplot(y; n = 3, nc = 1) # create an automatic grid, but fix the number of columns to 1 (so there are n rows)
subplot(y; layout = [1, 2]) # explicit layout by row... plot #1 goes by itself in the first row, plots 2 and 3 split the 2nd row (note the n kw is unnecessary)
subplot(plts, n; nr = -1, nc = -1) # build a layout from existing plots
subplot(plts, layout) # build a layout from existing plots
```
"""
function subplot(args...; kw...)
validateSubplotSupported()
d = KW(kw)
preprocessArgs!(d)
# for plotting recipes, swap out the args and update the parameter dictionary
args = RecipesBase.apply_recipe(d, KW(kw), args...; issubplot=true)
_add_markershape(d)
# figure out the layout
layoutarg = get(d, :layout, nothing)
if layoutarg != nothing
layout = subplotlayout(layoutarg)
else
n = get(d, :n, -1)
if n < 0
error("You must specify either layout or n when creating a subplot: ", d)
end
layout = subplotlayout(n, get(d, :nr, -1), get(d, :nc, -1))
end
# initialize the individual plots
pkg = backend()
plts = Plot{typeof(pkg)}[]
for i in 1:length(layout)
di = getPlotArgs(pkg, d, i)
di[:subplot] = true
dumpdict(di, "Plot args (subplot $i)")
push!(plts, _create_plot(pkg, di))
end
# create the object and do the plotting
subplt = Subplot(nothing, plts, pkg, length(layout), 0, layout, d, false, false, false, (r,c) -> (nothing,nothing))
subplot!(subplt, args...; kw...)
subplt
end
# ------------------------------------------------------------------------------------------------
# NOTE: for the subplot calls building from existing plots, we need the first plot to be separate to ensure dispatch calls this instead of the more general subplot(args...; kw...)
# grid layout
function subplot{P}(plt1::Plot{P}, plts::Plot{P}...; kw...)
d = KW(kw)
layout = if haskey(d, :layout)
subplotlayout(d[:layout])
else
subplotlayout(length(plts)+1, get(d, :nr, -1), get(d, :nc, -1))
end
# layout = subplotlayout(length(plts)+1, get(d, :nr, -1), get(d, :nc, -1))
subplot(vcat(plt1, plts...), layout, d)
end
# explicit layout
function subplot{P,I<:Integer}(pltsPerRow::AVec{I}, plt1::Plot{P}, plts::Plot{P}...; kw...)
layout = subplotlayout(pltsPerRow)
subplot(vcat(plt1, plts...), layout, KW(kw))
end
# this will be called internally
function subplot{P<:AbstractBackend}(plts::AVec{Plot{P}}, layout::AbstractLayout, d::KW)
validateSubplotSupported()
p = length(layout)
n = sum([plt.n for plt in plts])
subplt = Subplot(nothing, collect(plts), P(), p, n, layout, KW(), false, false, false, (r,c) -> (nothing,nothing))
_preprocess_subplot(subplt, d)
_postprocess_subplot(subplt, d)
subplt
end
# TODO: hcat/vcat subplots and plots together arbitrarily
# ------------------------------------------------------------------------------------------------
function _preprocess_subplot(subplt::Subplot, d::KW, args = ())
validateSubplotSupported()
userkw = preprocessArgs!(d)
# for plotting recipes, swap out the args and update the parameter dictionary
args = RecipesBase.apply_recipe(d, userkw, args...; issubplot=true)
_add_markershape(d)
dumpdict(d, "After subplot! preprocessing")
# get the full plotargs, overriding any new settings
# TODO: subplt.plotargs should probably be merged sooner and actually used
# for color selection, etc. (i.e. if we overwrite the subplot palettes to [:heat :rainbow])
# then we need to overwrite plt[1].plotargs[:color_palette] to :heat before it's actually used
# for color selection!
# first merge the new args into the subplot's plotargs. then process the plot args and merge
# those into the plot's plotargs. (example... `palette = [:blues :reds]` goes into subplt.plotargs,
# then the ColorGradient for :blues/:reds is merged into plot 1/2 plotargs, which is then used for color selection)
for i in 1:length(subplt.layout)
subplt.plts[i].plotargs = getPlotArgs(backend(), merge(subplt.plts[i].plotargs, d), i)
end
merge!(subplt.plotargs, d)
# process links. TODO: extract to separate function
for s in (:linkx, :linky, :linkfunc)
if haskey(d, s)
setfield!(subplt, s, d[s])
delete!(d, s)
end
end
args
end
function _postprocess_subplot(subplt::Subplot, d::KW)
# init (after plot creation)
if !subplt.initialized
subplt.initialized = _create_subplot(subplt, false)
end
# add title, axis labels, ticks, etc
for (i,plt) in enumerate(subplt.plts)
di = plt.plotargs
dumpdict(di, "Updating sp $i")
_update_plot(plt, di)
end
_update_plot_pos_size(subplt, d)
# handle links
subplt.linkx && link_axis(subplt, true)
subplt.linky && link_axis(subplt, false)
# set this to be current
current(subplt)
end
# ------------------------------------------------------------------------------------------------
"""
Adds to a subplot.
"""
# current subplot
function subplot!(args...; kw...)
validateSubplotSupported()
subplot!(current(), args...; kw...)
end
# not allowed:
function subplot!(plt::Plot, args...; kw...)
error("Can't call subplot! on a Plot!")
end
# # this adds to a specific subplot... most plot commands will flow through here
function subplot!(subplt::Subplot, args...; kw...)
# validateSubplotSupported()
d = KW(kw)
args = _preprocess_subplot(subplt, d, args)
# create the underlying object (each backend will do this differently)
# note: we call it once before doing the individual plots, and once after
# this is because some backends need to set up the subplots and then plot,
# and others need to do it the other way around
if !subplt.initialized
subplt.initialized = _create_subplot(subplt, true)
end
groupby = if haskey(d, :group)
extractGroupArgs(d[:group], args...)
else
nothing
end
_add_series_subplot(subplt, d, groupby, args...)
_postprocess_subplot(subplt, d)
# show it automatically?
if haskey(d, :show) && d[:show]
gui()
end
subplt
end
# not allowed:
function plot!(subplt::Subplot, args...; kw...)
error("Can't call plot! on a Subplot!")
end
# given a fully processed KW, add the series to the Plot
function _add_series_subplot(plt::Plot, d::KW)
# setTicksFromStringVector(d, d, :x, :xticks)
# setTicksFromStringVector(d, d, :y, :yticks)
setTicksFromStringVector(plt, d, d, "x")
setTicksFromStringVector(plt, d, d, "y")
setTicksFromStringVector(plt, d, d, "z")
# this is the actual call to the backend
_add_series(plt.backend, plt, d)
_add_annotations(plt, d)
warnOnUnsupportedScales(plt.backend, d)
end
# handle the grouping... add a series for each group
function _add_series_subplot(subplt::Subplot, d::KW, groupby::GroupBy, args...)
starting_n = subplt.n
for (i, glab) in enumerate(groupby.groupLabels)
tmpd = copy(d)
tmpd[:numUncounted] = subplt.n - starting_n
_add_series_subplot(subplt, tmpd, nothing, args...;
idxfilter = groupby.groupIds[i],
grouplabel = string(glab))
end
end
# process, filter, and add to the correct plot
function _add_series_subplot(subplt::Subplot, d::KW, ::Void, args...;
idxfilter = nothing,
grouplabel = "")
process_inputs(subplt, d, args...)
if idxfilter != nothing
# add the group name as the label if there isn't one passed in
get!(d, :label, grouplabel)
# filter the data
filter_data!(d, idxfilter)
end
kwList, xmeta, ymeta = build_series_args(subplt, d)
# TODO: something useful with meta info?
for (i,di) in enumerate(kwList)
subplt.n += 1
plt = getplot(subplt)
plt.n += 1
# cleanup the dictionary that we pass into the plot! command
di[:show] = false
di[:subplot] = true
for k in (:title, :xguide, :xticks, :xlims, :xscale, :xflip,
:yguide, :yticks, :ylims, :yscale, :yflip)
delete!(di, k)
end
dumpdict(di, "subplot! kwList $i")
dumpdict(plt.plotargs, "plt.plotargs before plotting")
_replace_linewidth(di)
_add_series_subplot(plt, di)
end
end
# --------------------------------------------------------------------
# handle "linking" the subplot axes together
# each backend should implement the _remove_axis and _expand_limits methods
function link_axis(subplt::Subplot, isx::Bool)
# collect the list of plots and the expanded limits for those plots that should be linked on this axis
includedPlots = Any[]
# lims = [Inf, -Inf]
lims = Dict{Int,Any}() # maps column to xlim
for (i,(r,c)) in enumerate(subplt.layout)
# shouldlink will be a bool or nothing. if nothing, then use linkx/y (which is true if we get to this code)
shouldlink = subplt.linkfunc(r,c)[isx ? 1 : 2]
if shouldlink == nothing || shouldlink
plt = subplt.plts[i]
# if we don't have this
k = isx ? c : r
if (firstone = !haskey(lims, k))
lims[k] = [Inf, -Inf]
end
isinner = (isx && r < nrows(subplt.layout)) || (!isx && !firstone)
push!(includedPlots, (plt, isinner, k))
_expand_limits(lims[k], plt, isx)
end
end
# do the axis adjustments
for (plt, isinner, k) in includedPlots
if isinner
_remove_axis(plt, isx)
end
(isx ? xlims! : ylims!)(plt, lims[k]...)
end
end

View File

@ -1,361 +1,17 @@
function Subplot{T<:AbstractBackend}(::T; parent = RootLayout())
Subplot{T}(parent, defaultbox, defaultbox, KW(), nothing, nothing)
end
# ----------------------------------------------------------------------
plotarea!(sp::Subplot, bbox::BoundingBox) = (sp.plotarea = bbox)
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))
# # 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)
# 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
# ----------------------------------------------------------------------
Base.size(layout::EmptyLayout) = (0,0)
Base.length(layout::EmptyLayout) = 0
Base.getindex(layout::EmptyLayout, r::Int, c::Int) = nothing
# ----------------------------------------------------------------------
Base.parent(::RootLayout) = nothing
parent_bbox(::RootLayout) = defaultbox
bbox(::RootLayout) = defaultbox
# Base.size(layout::RootLayout) = (1,1)
# Base.length(layout::RootLayout) = 1
# Base.getindex(layout::RootLayout, r::Int, c::Int) = layout.child
# ----------------------------------------------------------------------
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)
# 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)
# 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
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_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)
# 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
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)
# @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 = (pad_left + pad_right) .* nc
total_pad_vertical = (pad_top + pad_bottom) .* nr
# @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
# normalize widths/heights so they sum to 1
denom_w = sum(layout.widths)
denom_h = sum(layout.heights)
# @show denom_w, denom_h
# we have all the data we need... lets compute the plot areas
for r=1:nr, c=1:nc
child = layout[r,c]
# get the top-left corner of this child
child_left = (c == 1 ? 0mm : right(layout[r, c-1].bbox))
child_top = (r == 1 ? 0mm : 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[c] * layout.widths[c] / denom_w
plotarea_height = total_plotarea_vertical[r] * layout.heights[r] / denom_h
child_plotarea = 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]
child_bbox = BoundingBox(child_left, child_top, child_width, child_height)
# @show (r,c) child_plotarea child_bbox
# the bounding boxes are currently relative to the parent, but we need them relative to the canvas
plotarea!(child, crop(layout.bbox, child_plotarea))
bbox!(child, crop(layout.bbox, child_bbox))
# @show child_plotarea child_bbox
# 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
# ----------------------------------------------------------------------
# # build_layout should return a tuple: (a top-level layout, a list of subplots, and a SubplotMap)
# function build_layout(d::KW)
# layout = get(d, :layout, default(:layout))
# T = typeof(layout)
# if T <: AbstractLayout
# build_layout(layout)
# elseif T <: Integer
# if layout == 1
# # just a single subplot
# sp = Subplot(backend())
# sp, Subplot[sp], SubplotMap((1,1) => sp)
# else
# nr, nc = compute_gridsize(layout)
# build_layout(GridLayout(nr, nc), layout)
# end
# elseif T <: NTuple{2}
# build_layout(GridLayout(layout...))
# elseif T <: NTuple{3}
# n, nr, nc = layout
# build_layout(GridLayout())
# else
# error("unhandled layout type $T: $layout")
# 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
# 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)
# nr, nc = size(layout)
# build_layout(layout, nr*nc)
# recursively get the size of the grid
n = calc_num_subplots(layout)
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)] = sp
if hasattr(l,:width)
layout.widths[c] = attr(l,:width)
end
if hasattr(l,:height)
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_layout(huh) = error("unhandled layout type $(typeof(huh)): $huh")
# ----------------------------------------------------------------------
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
# ----------------------------------------------------------------------
get_subplot(plt::Plot, sp::Subplot) = sp
get_subplot(plt::Plot, i::Integer) = plt.subplots[i]
get_subplot(plt::Plot, k) = plt.spmap[k]
@ -365,138 +21,3 @@ get_subplot_index(plt::Plot, idx::Integer) = idx
get_subplot_index(plt::Plot, sp::Subplot) = findfirst(sp->sp === plt.subplots[2], plt.subplots)
# ----------------------------------------------------------------------
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
:(EmptyLayout(label = $(QuoteNode(s)), width = $w, height = $h))
end
end
function create_grid(s::Symbol)
:(EmptyLayout(label = $(QuoteNode(s))))
end
macro layout(mat::Expr)
create_grid(mat)
end
# ----------------------------------------------------------------------
# Base.start(layout::GridLayout) = 1
# Base.done(layout::GridLayout, state) = state > length(layout)
# function Base.next(layout::GridLayout, state)
# # TODO: change this method to return more info
# # TODO: might consider multiple iterator types.. some backends might have an easier time row-by-row for example
# error()
# r = 1
# c = 0
# for i = 1:state
# c += 1
# if c > layout.rowcounts[r]
# r += 1
# c = 1
# end
# end
# (r,c), state + 1
# end
# nrows(layout::GridLayout) = size(layout, 1)
# ncols(layout::GridLayout) = size(layout, 2)
# get the plot index given row and column
# -----------------------------------------------------------
# # we're taking in a nested structure of some kind... parse it out and build a GridLayout
# function subplotlayout(mat::AbstractVecOrMat; widths = nothing, heights = nothing)
# n = 0
# nr, nc = size(mat)
# grid = Array(IntOrFlex, nr, nc)
# for i=1:nr, j=1:nc
# v = mat[i,j]
#
# if isa(v, Integer)
# grid[i,j] = Int(v)
# n += 1
#
# elseif isa(v, Tuple)
# warn("need to handle tuples somehow... (idx, sizepct)")
# grid[i,j] = nothing
#
# elseif v == nothing
# grid[i,j] = nothing
#
# elseif isa(v, AbstractVecOrMat)
# grid[i,j] = layout(v)
# n += grid[i,j].n
#
# else
# error("How do we process? $v")
# end
# end
#
# if widths == nothing
# widths = ones(nc) ./ nc
# end
# if heights == nothing
# heights = ones(nr) ./ nr
# end
#
# GridLayout(n, grid, widths, heights)
# end
#
#
# function subplotlayout(sz::Tuple{Int,Int})
# GridLayout(sz...)
# end
#
# function subplotlayout(rowcounts::AVec{Int})
# RowsLayout(sum(rowcounts), rowcounts)
# end
#
# function subplotlayout(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
#
# # if it's a perfect rectangle, just create a grid
# if numplts == nr * nc
# return GridLayout(nr, nc)
# end
#
# # create the rowcounts vector
# i = 0
# rowcounts = Int[]
# for r in 1:nr
# cnt = min(nc, numplts - i)
# push!(rowcounts, cnt)
# i += cnt
# end
#
# RowsLayout(numplts, rowcounts)
# end

View File

@ -1,153 +1,32 @@
# TODO: I declare lots of types here because of the lacking ability to do forward declarations in current Julia
# I should move these to the relevant files when something like "extern" is implemented
typealias AVec AbstractVector
typealias AMat AbstractMatrix
typealias KW Dict{Symbol,Any}
immutable PlotsDisplay <: Display end
abstract AbstractBackend
abstract AbstractPlot{T<:AbstractBackend}
abstract AbstractLayout
typealias KW Dict{Symbol,Any}
# -----------------------------------------------------------
immutable InputWrapper{T}
obj::T
end
wrap{T}(obj::T) = InputWrapper{T}(obj)
Base.isempty(wrapper::InputWrapper) = false
# -----------------------------------------------------------
# Axes
# -----------------------------------------------------------
# simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place
type Axis #<: Associative{Symbol,Any}
type Axis
d::KW
end
type AxisView
label::UTF8String
axis::Axis
end
# -----------------------------------------------------------
# Layouts
# -----------------------------------------------------------
# NOTE: (0,0) is the top-left !!!
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.(:.*)(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.(:+)(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.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]
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
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
# -----------------------------------------------------------
abstract AbstractLayout
width(layout::AbstractLayout) = width(layout.bbox)
height(layout::AbstractLayout) = height(layout.bbox)
plotarea!(layout::AbstractLayout, bbox::BoundingBox) = nothing
attr(layout::AbstractLayout, k::Symbol) = layout.attr[k]
attr!(layout::AbstractLayout, v, k::Symbol) = (layout.attr[k] = v)
hasattr(layout::AbstractLayout, k::Symbol) = haskey(layout.attr, k)
# -----------------------------------------------------------
# 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))
# this is the parent of the top-level layout
immutable RootLayout <: AbstractLayout
# child::AbstractLayout
end
# -----------------------------------------------------------
# a single subplot
@ -156,57 +35,14 @@ type Subplot{T<:AbstractBackend} <: AbstractLayout
bbox::BoundingBox # the canvas area which is available to this subplot
plotarea::BoundingBox # the part where the data goes
attr::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
function Subplot{T<:AbstractBackend}(::T; parent = RootLayout())
Subplot{T}(parent, defaultbox, defaultbox, KW(), nothing, nothing)
end
plotarea!(sp::Subplot, bbox::BoundingBox) = (sp.plotarea = bbox)
# -----------------------------------------------------------
# nested, gridded layout with optional size percentages
type GridLayout <: AbstractLayout
parent::AbstractLayout
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
function GridLayout(dims...;
parent = RootLayout(),
widths = ones(dims[2]),
heights = ones(dims[1]),
kw...)
grid = Matrix{AbstractLayout}(dims...)
layout = GridLayout(
parent,
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
# -----------------------------------------------------------
typealias SubplotMap Dict{Any, Subplot}
# -----------------------------------------------------------
# Plot
# -----------------------------------------------------------
type Series
@ -216,6 +52,8 @@ end
attr(series::Series, k::Symbol) = series.d[k]
attr!(series::Series, v, k::Symbol) = (series.d[k] = v)
# -----------------------------------------------------------
type Plot{T<:AbstractBackend} <: AbstractPlot{T}
backend::T # the backend type
n::Int # number of series
@ -239,22 +77,5 @@ Base.getindex(plt::Plot, r::Integer, c::Integer) = plt.layout[r,c]
attr(plt::Plot, k::Symbol) = plt.plotargs[k]
attr!(plt::Plot, v, k::Symbol) = (plt.plotargs[k] = v)
# -----------------------------------------------------------
# Subplot
# -----------------------------------------------------------
# type Subplot{T<:AbstractBackend, L<:AbstractLayout} <: AbstractPlot{T}
# o # the underlying object
# plts::Vector{Plot{T}} # the individual plots
# backend::T
# p::Int # number of plots
# n::Int # number of series
# layout::L
# plotargs::KW
# initialized::Bool
# linkx::Bool
# linky::Bool
# linkfunc::Function # maps (row,column) -> (BoolOrNothing, BoolOrNothing)... if xlink/ylink are nothing, then use subplt.linkx/y
# end
# -----------------------------------------------------------------------