diff --git a/src/Plots.jl b/src/Plots.jl index bcc2fc65..4600b578 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -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") diff --git a/src/args.jl b/src/args.jl index 8f12dab8..4cb18d57 100644 --- a/src/args.jl +++ b/src/args.jl @@ -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, ())) diff --git a/src/axes.jl b/src/axes.jl new file mode 100644 index 00000000..e162274d --- /dev/null +++ b/src/axes.jl @@ -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 diff --git a/src/components.jl b/src/components.jl index a4adf002..25fc5382 100644 --- a/src/components.jl +++ b/src/components.jl @@ -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 diff --git a/src/layouts.jl b/src/layouts.jl new file mode 100644 index 00000000..9c8f5c41 --- /dev/null +++ b/src/layouts.jl @@ -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 diff --git a/src/old_layouts.jl b/src/old_layouts.jl deleted file mode 100644 index 8261a4d6..00000000 --- a/src/old_layouts.jl +++ /dev/null @@ -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 diff --git a/src/old_subplot.jl b/src/old_subplot.jl deleted file mode 100644 index 3d6a485c..00000000 --- a/src/old_subplot.jl +++ /dev/null @@ -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 diff --git a/src/subplots.jl b/src/subplots.jl index d8731385..9b86e28e 100644 --- a/src/subplots.jl +++ b/src/subplots.jl @@ -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 diff --git a/src/types.jl b/src/types.jl index 027a9421..d8e0e209 100644 --- a/src/types.jl +++ b/src/types.jl @@ -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 # -----------------------------------------------------------------------