text:
using DataFrames, LinearAlgebra, MixedModels, Random, RData, SparseArrays, StatsModels, Test

if !@isdefined(dat) || !isa(dat, Dict{Symbol, DataFrame})
    const dat = Dict(Symbol(k) => v for (k, v) in
        load(joinpath(dirname(pathof(MixedModels)), "..", "test", "dat.rda")))
end

const LMM = LinearMixedModel

@testset "scalarReMat" begin
    ds = dat[:Dyestuff]
    f1 = @formula(Y ~ 1 + (1|G))
    y1, Xs1 = modelcols(apply_schema(f1, schema(ds), LMM), ds)
    sf = Xs1[2]
    psts = dat[:Pastes]
    f2 = @formula(Y ~ 1 + (1|G) + (1|H))
    y2, Xs2 = modelcols(apply_schema(f2, schema(psts), LMM), psts)
    sf1 = Xs2[2]
    sf2 = Xs2[3]

    @testset "size" begin
        @test size(sf) == (30, 6)
        @test size(sf,1) == 30
        @test size(sf,2) == 6
        @test size(sf,3) == 1
        @test size(sf1) == (60, 30)
        @test size(sf2) == (60, 10)
    end

    @testset "utilities" begin
        @test MixedModels.levs(sf) == string.('A':'F')
        @test MixedModels.nlevs(sf) == 6
        @test eltype(sf) == Float64
        @test sparse(sf) == sparse(1:30, sf.refs, ones(30))
        fsf = Matrix(sf)
        @test size(fsf) == (30, 6)
        @test count(!iszero, fsf) == 30
        @test sort!(unique(fsf)) == [0.0, 1.0]
        @test cond(sf) == 1.0
        @test MixedModels.nθ(sf) == 1
        @test MixedModels.getθ(sf) == ones(1)
        @test MixedModels.getθ!(Vector{Float64}(undef, 1), sf) == ones(1)
        @test lowerbd(sf) == zeros(1)
        @test MixedModels.getθ(setθ!(sf, [0.5])) == [0.5]
        MixedModels.unscaledre!(Vector{Float64}(undef, 30), sf)
        @test_throws DimensionMismatch MixedModels.getθ!(Float64[], sf)
        @test_throws DimensionMismatch setθ!(sf, ones(2))
    end

    @testset "products" begin
        @test ones(30, 1)'sf == fill(5.0, (1, 6))
        @test mul!(Array{Float64}(undef, (size(sf1, 2), size(sf2, 2))), sf1', sf2) == Array(sf1'sf2)

        crp = sf'sf
        @test isa(crp, Diagonal{Float64})
        crp1 = copy(crp)
        @test crp1 == crp
        @test crp[2,6] == 0
        @test crp[6,6] == 5
        @test size(crp) == (6,6)
        @test crp.diag == fill(5.,6)
        rhs = y1'sf
        @test rhs == reshape([7525.0,7640.0,7820.0,7490.0,8000.0,7350.0], (1, 6))
        @test ldiv!(crp, copy(rhs)') == [1505.,1528.,1564.,1498.,1600.,1470.]

        @test isa(sf1'sf1, Diagonal{Float64})
        @test isa(sf2'sf2, Diagonal{Float64})
        @test isa(sf2'sf1,SparseMatrixCSC{Float64})

        @test_broken lmul!(Λ(sf)', ones(6)) == fill(0.5, 6)
        @test_broken rmul!(ones(6, 6), Λ(sf)) == fill(0.5, (6, 6))

        @test_broken mul!(Matrix{Float64}(undef, 1, 6), Λ(sf), ones(1, 6)) == fill(0.5, (1,6))
    end

    @testset "reweight!" begin
        wts = rand(MersenneTwister(1234321), size(sf, 1))
        @test isapprox(vec(MixedModels.reweight!(sf, wts).wtz), wts)
    end
end

@testset "RandomEffectsTerm" begin
    slp = dat[:sleepstudy]
    contrasts =  Dict{Symbol,Any}()

    @testset "Detect same variable as blocking and experimental" begin
        f = @formula(Y ~ 1 + (1 + G|G))
        @test_throws ArgumentError apply_schema(f, schema(f, slp, contrasts), LinearMixedModel)
    end

    @testset "Detect both blocking and experimental variables" begin
        # note that U is not in the fixed effects because we want to make square
        # that we're detecting all the variables in the random effects
        f = @formula(Y ~ 1 + (1 + U|G))
        form = apply_schema(f, schema(f, slp, contrasts), LinearMixedModel)
        @test StatsModels.termvars(form.rhs) == [:U, :G]
    end
end

@testset "Categorical Blocking Variable" begin
    # deepcopy because we're going to modify it
    slp = deepcopy(dat[:sleepstudy])
    contrasts =  Dict{Symbol,Any}()
    f = @formula(Y ~ 1 + (1|G))

    # String blocking-variables work fine because StatsModels is smart enough to
    # treat strings to treat strings as Categorical. Note however that this is a
    # far less efficient to store the original dataframe, although it doesn't
    # matter for the contrast matrix
    slp[!,:G] = convert.(String, slp[!, :G])
    # @test_throws ArgumentError LinearMixedModel(f, slp)
    slp[!,:G] = parse.(Int, slp[!, :G])
    @test_throws ArgumentError LinearMixedModel(f, slp)
end

#=
@testset "vectorRe" begin
    slp = dat[:sleepstudy]
    corr = VectorFactorReTerm(slp[:G], vcat(ones(1, size(slp, 1)), slp[:U]'),
        :G, ["(Intercept)", "U"], [2])
    nocorr = VectorFactorReTerm(slp[:G], vcat(ones(1, size(slp, 1)), slp[:U]'),
            :G, ["(Intercept)", "U"], [1, 1])
    Reaction = slp[:Y]

    @testset "sizes" begin
        @test size(corr) == (180,36)
        @test size(nocorr) == (180,36)
    end

    @testset "utilities" begin
        @test MixedModels.levs(corr) == levels(slp[:G])
        @test MixedModels.nlevs(corr) == 18
        @test MixedModels.vsize(corr) == 2
        @test MixedModels.nrandomeff(corr) == 36
        @test eltype(corr) == Float64
        @test nnz(sparse(corr)) == 360
        @test cond(corr) == 1.0
        @test MixedModels.nθ(corr) == 3
        @test MixedModels.nθ(nocorr) == 2
        @test MixedModels.getθ(corr) == [1.0, 0.0, 1.0]
        @test MixedModels.getθ(nocorr) == ones(2)
        @test MixedModels.getθ!(Vector{Float64}(undef, 2), nocorr) == ones(2)
        @test lowerbd(nocorr) == zeros(2)
        @test lowerbd(corr) == [0.0, -Inf, 0.0]
        @test MixedModels.getθ(setθ!(corr, fill(0.5, 3))) == [0.5, 0.5, 0.5]
        @test_throws DimensionMismatch MixedModels.getθ!(Vector{Float64}(undef, 2), corr)
        @test_throws DimensionMismatch setθ!(corr, ones(2))
    end

    vrp = corr'corr

    @test isa(vrp, UniformBlockDiagonal{Float64})
    @test size(vrp) == (36, 36)

    @test mul!(Array{Float64}(undef, 36, 36), corr', corr) == Matrix(vrp)
    scl = ScalarFactorReTerm(slp[:G].refs, levels(slp[:G]), Array(slp[:U]), Array(slp[:U]), :G, ["U"], 1.0)

    @test sparse(corr)'sparse(scl) == corr'scl

    b = mul!(Matrix{Float64}(undef, 2,18), Λ(corr).data, ones(2,18))
    @test b == reshape(repeat([0.5, 1.0], outer=18), (2,18))

    @testset "reweight!" begin
        wts = rand(MersenneTwister(1234321), size(corr, 1))
        @test MixedModels.reweight!(corr, wts).wtz[1, :] == wts
        @test corr.z[1, :] == ones(size(corr, 1))
    end

end
=#

@testset "random effects term syntax" begin

    dat = (y = rand(18),
           g = string.(repeat('a':'f', inner=3)),
           f = string.(repeat('A':'C', outer=6)))

    @testset "fulldummy" begin
        @test_throws ArgumentError fulldummy(1)

        f = @formula(y ~ 1 + fulldummy(f))
        f1 = apply_schema(f, schema(dat))
        @test typeof(f1.rhs.terms[end]) <: FunctionTerm{typeof(fulldummy)}
        @test_throws ArgumentError modelcols(f1, dat)

        f2 = apply_schema(f, schema(dat), MixedModel)
        @test typeof(f2.rhs.terms[end]) <: CategoricalTerm{<:StatsModels.FullDummyCoding}
        @test modelcols(f2.rhs, dat)[1:3, :] == [1 1 0 0
                                                 1 0 1 0
                                                 1 0 0 1]

        # implict intercept
        ff = apply_schema(@formula(y ~ 1 + (f | g)), schema(dat), MixedModel)
        rem = modelcols(ff.rhs[end], dat)
        @test size(rem) == (18, 18)
        @test rem[1:3, 1:4] == [1 0 0 0
                                1 1 0 0
                                1 0 1 0]

        # explicit intercept
        ff = apply_schema(@formula(y ~ 1 + (1+f | g)), schema(dat), MixedModel)
        rem = modelcols(ff.rhs[end], dat)
        @test size(rem) == (18, 18)
        @test rem[1:3, 1:4] == [1 0 0 0
                                1 1 0 0
                                1 0 1 0]

        # explicit intercept + full dummy
        ff = apply_schema(@formula(y ~ 1 + (1+fulldummy(f) | g)), schema(dat), MixedModel)
        rem = modelcols(ff.rhs[end], dat)
        @test size(rem) == (18, 24)
        @test rem[1:3, 1:4] == [1 1 0 0
                                1 0 1 0
                                1 0 0 1]

        # explicit dropped intercept (implicit full dummy)
        ff = apply_schema(@formula(y ~ 1 + (0+f | g)), schema(dat), MixedModel)
        rem = modelcols(ff.rhs[end], dat)
        @test size(rem) == (18, 18)
        @test rem[1:3, 1:4] == [1 0 0 0
                                0 1 0 0
                                0 0 1 0]

    end
end


text:
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

include("ndarray/type.jl")  # type def and constructors
include("ndarray/context.jl")
include("ndarray/show.jl")
include("ndarray/remap.jl")  # provide @_remap util
include("ndarray/array.jl")
include("ndarray/arithmetic.jl")
include("ndarray/comparison.jl")
include("ndarray/io.jl")  # save/load and synchronization utils
include("ndarray/reduction.jl")
include("ndarray/statistic.jl")
include("ndarray/linalg.jl")
include("ndarray/trig.jl")
include("ndarray/activation.jl")
include("ndarray/autoimport.jl")  # auto import operators from libmxnet

ext: jl


text:
include("matrixgen.jl")
include("reptileutils.jl")

## Tools: functions for angles and sides
function findconstraints(A1, B1, n, digits = 12)
  #OBSOLTETE
  #returns a vector with ones in the locations where additional constraints would  fully constrain the system

  l = size(A)[2] #number of columns in A
  output = zeros(n)
  i = 0 #loop counter
  r = rank([A B])
  while r < n && i < n #underconstrained and not out of variables
    i += 1 #increment
    c = zeros(1, l) #new row
    c[i] = 1 #add constraint x_i = 0
    r1 = rank([A B; c 0]) #rank with new constraint x_i = 0
    if r == r1 #nothing changed
      continue #try next variable
    else #rank has increased. rank decrease should be impossible
      if size(A)[1] == l #sqaure. could be singular
        X = [A; zeros(1, l)] \ [B; 0.0] #add an empty row to trick the algorithm out of square matrix, and solve
      else
        X = A \ B #solve
      end
      if floateq(A * X, B, digits) == false #solution failed
        continue #try next variable
      end
      #new constraint is valid
      A = [A; c] #add new constraint
      B = [B; 0]
      r = rank([A B])
      output[i] = 1.0 #save this as a good constraint
    end
  end
  return output #return good constraints, or all zeros if it cannot be constrained
end

function buildanglesys(matint)
  #returns A and B of the equation AX=B for the given angle matrix
  mat = convert(Array{Angle, 2}, matint) #convert
  n = size(mat)[1]
  a = mat[:,2:end] - eye(Angle, n, n) #move lhs over
  A = [a; ones(Angle, 1, n)] # sum(theta) = pi*(n-2) constraint
  B = [-mat[:,1] * Angle(0//1, 1//1); Angle(0//1, 1//1) * (n-2)//1] #straight angle conditions; pi*(n-2 constraint)
  return A, B, n
end

function buildsidesys(matint::Array{Int, 2}, m::Int)
  #returns A and B of the equation AX=B for the given side matrix with s1 = 1
  mat = convert(Array{Side, 2}, matint, m)
  n = size(mat)[1]
  a = mat - sideeye(Side, n, m) * Side(0//1, 1//1, m)
  A = [one(Side, m) zeros(Side, (1, n-1), mvalue = m)
    zeros(Side, (n, 1), mvalue = m) a[:, 2:end]]::Array{Side, 2} #with fake first row and col forcing s1=1
  B = [one(Side, m); -a[:, 1]] #with fake first row forcing s1=1
  return A, B, n
end

function isvalidsol(angles::Array{Angle,1})
    #checks if the `angles` vector of polygon angles are valid
    #only checking the p-component since that should be all that is possible

  for i in angles
    if i.p <= 0//1 #negative angles (can't be changed because the system has no d.o.f.)
        return false
    end
    if i.p >= 2//1 #angle over 2pi (can't be changed because the system has no d.o.f.)
        return false
    end
    if i.p == 1//1 #there is a straight angle
        return false
    end
  end

  return true #all tests pass
end

function isvalidsol(sides::Array{Side,1})
    #tests whether the vector of side lengths `s` are valid
    m = findm(sides)

    if minimum(sides) <= zero(Side, m) #minimum individual side lenth
        return false
    end
    #longest side is more than the sum of the other sides (s1 is length 1)
    if 2//1 * maximum(sides) >= sum(sides)  #max side >= all sides - max side
        return false
    end
    return true #all test pass
end

function solvemat(mat, m::Int, isangle::Bool)
  #solves the given angle matrix if possible
  #returns rref form of the solution matrix
  #returns marix of zeros and rank = n+1 if there is no solution
  #returns rank as 2nd argument

  if isangle #angle matrix
    A, B, n = buildanglesys(mat) #matrices for A*X=B
  else #side matrix
    A, B, n = buildsidesys(mat, m) #matrices for A*X=B
  end

  rrefmat = rref([A B]) #row reduce

  if isangle
    rank = countnz(sum(rrefmat, 2)) #rank of matrix
  else #side
    #ifferent because of special side zero functions
    rank = countnz(sum(rrefmat .!= zero(Side, m), 2)) #rank of matrix
  end

  if rank < n #underconstrained.
    return rrefmat[1:n, :], rank
  end

  if rank == n #fully constrained
    if isvalidsol(rrefmat[1:n,end]) #last column is solutions
      return rrefmat[1:n, :], rank
    end
  end

  return zeros(rrefmat[1:n, :]), n+1 #overconstrained or invalid solutions
end

max_stars_repo_name: ericbumbalough/Reptiles

max_issues_repo_name: ericbumbalough/Reptiles

max_forks_repo_name: ericbumbalough/Reptiles


text:
abstract type AbstractχNMatrix{T} <: AbstractMatrix{T} end

struct χNMatrix{T} <: AbstractχNMatrix{T}
    map::Dict{Set{Symbol}, T}
    mat::Matrix{T}
    imat::Matrix{T}  # the inverse of mat
end

function _species(map::Dict{Set{Symbol}, T}) where T<:Real
    sps = Symbol[]
    for key in keys(map)
        append!(sps, key)
    end
    return unique(sps) |> sort
end

function _check_consistency(map::Dict{Set{Symbol}, T}) where T<:Real
    # Now sps is a list of sorted and unique symbols.
    # which make sure following loops sp1 and sp2 are distinct.
    sps = _species(map)
    for i in 1:length(sps)
        sp1 = sps[i]
        for j in (i+1):length(sps)
            sp2 = sps[j]
            !haskey(map, Set((sp1,sp2))) && return false
        end
    end
    return true
end

function _χNmap_to_matrix(map::Dict{Set{Symbol}, T}) where T<:Real
    _check_consistency(map) || error("Full map of interactions should be specified!")
    sps = _species(map)
    nc = length(sps)
    mat = zeros(T, nc, nc)
    for i in 1:nc
        sp1 = sps[i]
        for j in (i+1):nc
            sp2 = sps[j]
            χN = map[Set((sp1,sp2))]
            mat[i, j] = χN
            mat[j, i] = χN
        end
    end
    r = rank(mat)
    (r == size(mat)[1]) || error("χN map is singular! Possible indistinguishable speices are presented in the map. Consider to combine them.")
    return mat
end

function χNMatrix(map::Dict{Set{Symbol}, T}) where T<:Real
    mat = _χNmap_to_matrix(map)
    imat = inv(mat)
    return χNMatrix(map, mat, imat)
end

species(m::χNMatrix) = _species(m.map)

Base.size(A::χNMatrix) = size(A.mat)
Base.getindex(A::χNMatrix, i::Int) = getindex(A.mat, i)
Base.getindex(A::χNMatrix, I::Vararg{Int, N}) where N = getindex(A.mat, I...)

Base.getindex(::χNMatrix{T}, ::Symbol) where T = zero(T)

function Base.getindex(A::χNMatrix{T}, sp1::Symbol, sp2::Symbol) where T
    (sp1 == sp2) && return zero(T)
    return A.map[Set((sp1, sp2))]
end

function _setindex!(A::χNMatrix, v, sp1::Symbol, sp2::Symbol)
    A.map[Set((sp1, sp2))] = v
    # call _χNmap_to_matrix to validate the matrix
    mat = _χNmap_to_matrix(A.map)
    # If no error, set the value
    # update the inverse matrix
    A.mat .= mat
    A.imat .= inv(mat)
end

function _setindex!(A::χNMatrix, v, i, j)
    # Do not set value for j = k
    (i == j) && return nothing
    sps = _species(A.map)
    _setindex!(A, v, sps[i], sps[j])
end

function Base.setindex!(A::χNMatrix, v, i::Int)
    j, k = Tuple(CartesianIndices(A)[i])
    _setindex!(A, v, j, k)
    return nothing
end

function Base.setindex!(A::χNMatrix, v, i::Int, j::Int)
    _setindex!(A, v, i, j)
    return nothing
end

Base.setindex!(::χNMatrix, v, ::Symbol) = nothing

function Base.setindex!(A::χNMatrix, v, sp1::Symbol, sp2::Symbol)
    _setindex!(A, v, sp1, sp2)
end

Base.eltype(::χNMatrix{T}) where T = T

Base.showarg(io::IO, A::χNMatrix, toplevel) = print(io, typeof(A), " of species: ", _species(A.map))


text:
using SciPyDiffEq
using Test

function lorenz(u,p,t)
 du1 = 10.0(u[2]-u[1])
 du2 = u[1]*(28.0-u[3]) - u[2]
 du3 = u[1]*u[2] - (8/3)*u[3]
 [du1, du2, du3]
end
u0 = [1.0;0.0;0.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz,u0,tspan)
sol = solve(prob,SciPyDiffEq.RK45())
sol = solve(prob,SciPyDiffEq.RK23())
sol = solve(prob,SciPyDiffEq.Radau())
sol = solve(prob,SciPyDiffEq.BDF())
sol = solve(prob,SciPyDiffEq.LSODA())
sol = solve(prob,SciPyDiffEq.odeint())

function lorenz(du,u,p,t)
 du[1] = 10.0(u[2]-u[1])
 du[2] = u[1]*(28.0-u[3]) - u[2]
 du[3] = u[1]*u[2] - (8/3)*u[3]
end
u0 = [1.0;0.0;0.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz,u0,tspan)
sol = solve(prob,SciPyDiffEq.RK45())
sol(4.0)
sol = solve(prob,SciPyDiffEq.RK23())
sol(4.0)
sol = solve(prob,SciPyDiffEq.Radau())
sol(4.0)
sol = solve(prob,SciPyDiffEq.BDF())
sol(4.0)
sol = solve(prob,SciPyDiffEq.LSODA())
sol(4.0)
sol = solve(prob,SciPyDiffEq.odeint())
sol(4.0)

#using Plots; plot(sol,vars=(1,2,3))


text:
module Winston

using Cairo
using Colors
using Reexport
@reexport using Graphics
@reexport using LinearAlgebra
using Random
using Printf
using Pkg
using SparseArrays
using IniFile
using Compat
using StatsBase
using NaNMath
using Dates
using REPL
using Base.Libc: strftime
import Base: *, copy, display, get, getindex, isempty, setindex!, show, range
import Graphics: paint, width, height, save, deform, diagonal, restore,
                 rectangle, polygon, move_to, line_to, stroke, rel_line_to,
                 shift

if isdefined(Base, :Grisu)
    import Base.Grisu
else
    import Grisu
end

export
    bar,
    barh,
    closefig,
    closeall,
    colormap,
    errorbar,
    figure,
    fplot,
    gcf,
    hold,
    imagesc,
    loglog,
    oplot,
    plot,
    plothist,
    plothist2d,
    savefig,
    scatter,
    semilogx,
    semilogy,
    spy,
    stem,
    text,
    title,
    timeplot,
    xlabel,
    xlim,
    ylabel,
    ylim,

    # FramedArray
    FramedPlot,
    Plot,
    Table,

    Curve,
    FillAbove,
    FillBelow,
    FillBetween,
    FramedBar,
    Histogram,
    Image,
    Legend,
    LineX,
    LineY,
    PlotInset,
    PlotLabel,
    Points,
    Slope,
    Stems,
    SymmetricErrorBarsX,
    SymmetricErrorBarsY,

    add,
    file,
    getattr,
    setattr,
    style,
    svg,

    getcomponents,
    rmcomponents,
    grid,
    legend,

    get_context,
    device_to_data,
    data_to_device

mutable struct WinstonException <: Exception
    msg::String
end

abstract type HasAttr end
abstract type HasStyle <: HasAttr end
abstract type PlotComponent <: HasStyle end
abstract type PlotContainer <: HasAttr end

const PlotAttributes = AbstractDict # TODO: does Associative need {K,V}?

include("config.jl")
include("geom.jl")
include("renderer.jl")
include("paint.jl")

# utils -----------------------------------------------------------------------

function args2dict(args...; kvs...)
    opts = Dict{Symbol,Any}()
    iter_result = iterate(args)
    while iter_result !== nothing
        (arg, iter) = iter_result
        if typeof(arg) <: AbstractDict
            for (k,v) in arg
                opts[Symbol(k)] = v
            end
        elseif typeof(arg) <: Tuple
            opts[Symbol(arg[1])] = arg[2]
        else
            (val, iter) = iterate(args, iter)
            opts[Symbol(arg)] = val
        end
        iter_result = iterate(args, iter)
    end
    for (k,v) in kvs
        opts[k] = v
    end
    opts
end

# NOTE: these are not standard, since where a coordinate falls on the screen
# depends on the current transformation.
lowerleft(bb::BoundingBox) = Point(bb.xmin, bb.ymin)
upperleft(bb::BoundingBox) = Point(bb.xmin, bb.ymax)
lowerright(bb::BoundingBox) = Point(bb.xmax, bb.ymin)
upperright(bb::BoundingBox) = Point(bb.xmax, bb.ymax)

function bounds_within(x, y, window::BoundingBox)
    xmin, xmax, ymin, ymax = NaN, NaN, NaN, NaN
    for i = 1:min(length(x),length(y))
        xi = x[i]
        yi = y[i]
        if (window.xmin < xi < window.xmax) && (window.ymin < yi < window.ymax)
            xmin = NaNMath.min(xmin, xi)
            xmax = NaNMath.max(xmax, xi)
            ymin = NaNMath.min(ymin, yi)
            ymax = NaNMath.max(ymax, yi)
        end
    end
    BoundingBox(xmin, xmax, ymin, ymax)
end

# relative size ---------------------------------------------------------------

const Box = Union{BoundingBox,Rectangle}

function _size_relative(relsize, bbox::Box)
    w = width(bbox)
    h = height(bbox)
    yardstick = sqrt(8.) * w * h / (w + h)
    return 0.01 * relsize * yardstick
end

function _fontsize_relative(relsize, bbox::Box, device_bbox::Box)
    devsize = _size_relative(relsize, bbox)
    fontsize_min = config_value("default", "fontsize_min")
    devsize_min = _size_relative(fontsize_min, device_bbox)
    return NaNMath.max(devsize, devsize_min)
end

# PlotContext -------------------------------------------------------------

mutable struct PlotContext
    draw::Renderer
    dev_bbox::BoundingBox
    data_bbox::BoundingBox
    xlog::Bool
    ylog::Bool
    xflipped::Bool
    yflipped::Bool
    geom::AbstractProjection2
    plot_geom::AbstractProjection2
    paintc::PaintContext

    function PlotContext(device::Renderer, dev::BoundingBox, data::Rectangle, proj::AbstractProjection2, xlog=false, ylog=false)
        xflipped = data.x0 > data.x1
        yflipped = data.y0 > data.y1
        paintc = PaintContext(device, _size_relative(1,dev), _fontsize_relative(0,dev,boundingbox(device)))
        new(
            device,
            dev,
            BoundingBox(data),
            xlog,
            ylog,
            xflipped,
            yflipped,
            proj,
            PlotGeometry(Rectangle(0,1,0,1), dev),
            paintc
       )
    end
end

function _get_context(device::Renderer, ext_bbox::BoundingBox, pc::PlotContainer)
    for (key,val) in config_options("defaults")
        set(device, key, val)
    end
    ext_bbox *= 1 - getattr(pc, "page_margin")
    if hasattr(pc, "title")
        offset = _size_relative(getattr(pc,"title_offset"), ext_bbox)
        fontsize = _fontsize_relative(
            getattr(pc,"title_style")["fontsize"], ext_bbox, boundingbox(device))
        ext_bbox = deform(ext_bbox, 0, 0, 0, -offset-fontsize)
    end
    int_bbox = interior(pc, device, ext_bbox)
    invoke(compose_interior, Tuple{PlotContainer,Renderer,BoundingBox}, pc, device, int_bbox)
    ret = _context1(pc, device, int_bbox)
    ret
end

function device_to_data(ctx::PlotContext, x::Real, y::Real)
    deproject(ctx.geom, x, y)
end

function data_to_device(ctx::PlotContext, x::Union{T,AbstractArray{T}}, y::Union{T,AbstractArray{T}}) where T<:Real
    project(ctx.geom, x, y)
end

# Legend ----------------------------------------------------------------------

mutable struct Legend <: PlotComponent
    attr::PlotAttributes
    x
    y
    components::Array{PlotComponent,1}

    function Legend(x, y, components, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self.y = y
        self.components = components
        self
    end
end

_kw_rename(::Legend) = Dict(
    :face      => :fontface,
    :size      => :fontsize,
    :angle     => :textangle,
    :halign    => :texthalign,
    :valign    => :textvalign,
)

function make(self::Legend, context::PlotContext)
    key_pos = project(context.plot_geom, self.x, self.y)
    key_width = _size_relative(getattr(self, "key_width"), context.dev_bbox)
    key_height = _size_relative(getattr(self, "key_height"), context.dev_bbox)
    key_hsep = _size_relative(getattr(self, "key_hsep"), context.dev_bbox)
    key_vsep = _size_relative(getattr(self, "key_vsep"), context.dev_bbox)

    halign = kw_get(self, :texthalign)
    if halign == "left"
        text_pos = Point(key_pos[1]+key_width/2+key_hsep, key_pos[2])
    else
        text_pos = Point(key_pos[1]-key_width/2-key_hsep, key_pos[2])
    end
    bbox = BoundingBox(key_pos[1]-key_width/2, key_pos[1]+key_width/2,
                       key_pos[2]-key_height/2, key_pos[2]+key_height/2)
    dp = Vec2(0., -(key_vsep + key_height))

    objs = GroupPainter(getattr(self,:style))
    for comp in self.components
        s = getattr(comp, "label", "")
        t = TextPainter(text_pos, s; halign=halign)
        push!(objs, t)
        push!(objs, make_key(comp,bbox))
        text_pos = text_pos + dp
        bbox = shift(bbox, dp.x, dp.y)
    end
    objs
end

# ErrorBars --------------------------------------------------------------------

abstract type ErrorBar <: PlotComponent end

_kw_rename(::ErrorBar) = Dict(
    :color => :linecolor,
    :width => :linewidth,
    :kind => :linekind,
)

mutable struct ErrorBarsX <: ErrorBar
    attr::PlotAttributes
    y
    lo
    hi

    function ErrorBarsX(y, lo, hi, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.y = y
        self.lo = lo
        self.hi = hi
        self
    end
end

limits(self::ErrorBarsX, window::BoundingBox) =
    bounds_within(self.lo, self.y, window) +
    bounds_within(self.hi, self.y, window)

function make(self::ErrorBarsX, context)
    l = _size_relative(getattr(self, "barsize"), context.dev_bbox)
    objs = GroupPainter(getattr(self,:style))
    for i = 1:length(self.y)
        p = project(context.geom, self.lo[i], self.y[i])
        q = project(context.geom, self.hi[i], self.y[i])
        l0 = LinePainter(Point(p[1],p[2]), Point(q[1],q[2]))
        l1 = LinePainter(Point(p[1],p[2]-l), Point(p[1],p[2]+l))
        l2 = LinePainter(Point(q[1],q[2]-l), Point(q[1],q[2]+l))
        push!(objs, l0)
        push!(objs, l1)
        push!(objs, l2)
    end
    objs
end

mutable struct ErrorBarsY <: ErrorBar
    attr::PlotAttributes
    x
    lo
    hi

    function ErrorBarsY(x, lo, hi, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self.lo = lo
        self.hi = hi
        self
    end
end

limits(self::ErrorBarsY, window::BoundingBox) =
    bounds_within(self.x, self.lo, window) +
    bounds_within(self.x, self.hi, window)

function make(self::ErrorBarsY, context)
    objs = GroupPainter(getattr(self,:style))
    l = _size_relative(getattr(self, "barsize"), context.dev_bbox)
    for i = 1:length(self.x)
        p = project(context.geom, self.x[i], self.lo[i])
        q = project(context.geom, self.x[i], self.hi[i])
        l0 = LinePainter(Point(p[1],p[2]), Point(q[1],q[2]))
        l1 = LinePainter(Point(p[1]-l,p[2]), Point(p[1]+l,p[2]))
        l2 = LinePainter(Point(q[1]-l,q[2]), Point(q[1]+l,q[2]))
        push!(objs, l0)
        push!(objs, l1)
        push!(objs, l2)
    end
    objs
end

function SymmetricErrorBarsX(x, y, err, args...)
    xlo = x - err
    xhi = x + err
    return ErrorBarsX(y, xlo, xhi, args...)
end

function SymmetricErrorBarsY(x, y, err, args...)
    ylo = y - err
    yhi = y + err
    return ErrorBarsY(x, ylo, yhi, args...)
end

# Inset -----------------------------------------------------------------------

abstract type _Inset <: PlotComponent end

function render(self::_Inset, context::PlotContext)
    region = boundingbox(self, context)
    compose_interior(self.plot, context.draw, region)
end

mutable struct DataInset <: _Inset
    plot_limits::BoundingBox
    plot::PlotContainer
    DataInset(p::Point, q::Point, plot) = new(BoundingBox(p, q), plot)
    DataInset(p::Tuple, q::Tuple, plot) = DataInset(Point(p...), Point(q...), plot)
end

function boundingbox(self::DataInset, context::PlotContext)
    p = project(context.geom, lowerleft(self.plot_limits))
    q = project(context.geom, upperright(self.plot_limits))
    return BoundingBox(p, q)
end

function limits(self::DataInset, window::BoundingBox)
    return self.plot_limits
end

mutable struct PlotInset <: _Inset
    plot_limits::BoundingBox
    plot::PlotContainer
    PlotInset(p::Point, q::Point, plot) = new(BoundingBox(p, q), plot)
    PlotInset(p::Tuple, q::Tuple, plot) = PlotInset(Point(p...), Point(q...), plot)
end

function boundingbox(self::PlotInset, context::PlotContext)
    p = project(context.plot_geom, lowerleft(self.plot_limits))
    q = project(context.plot_geom, upperright(self.plot_limits))
    return BoundingBox(p, q)
end

function limits(self::PlotInset, window::BoundingBox)
    return BoundingBox()
end

# HalfAxis --------------------------------------------------------------------

function _magform(x)
    # Given x, returns (a,b), where x = a*10^b [a >= 1., b integral].
    if x == 0
        return 0., 0
    end
    a, b = modf(log10(abs(x)))
    a, b = 10^a, Int(b)
    if a < 1.
        a, b = a * 10, b - 1
    end
    if x < 0.
        a = -a
    end
    return a, b
end

grisu(a,b,c) = ((w,x,y) = Grisu.grisu(a,b,c); (y,Grisu.DIGITS[1:w],x))

function _format_ticklabel(x, range=0.; min_pow10=4)
    if x == 0
        return "0"
    end
    neg, digits, b = grisu(x, Grisu.SHORTEST, Int(0))
    if length(digits) > 5
        neg, digits, b = grisu(x, Grisu.PRECISION, Int(6))
        n = length(digits)
        while digits[n] == UInt32('0')
            n -= 1
        end
        digits = digits[1:n]
    end
    b -= 1
    if abs(b) >= min_pow10
        s = IOBuffer()
        if neg write(s, '-') end
        if digits != [0x31]
            write(s, Char(digits[1]))
            if length(digits) > 1
                write(s, '.')
                for i = 2:length(digits)
                    write(s, Char(digits[i]))
                end
            end
            write(s, "\\times ")
        end
        write(s, "10^{")
        write(s, string(b))
        write(s, '}')
        return String(take!(s))
    end
    # XXX: @sprint doesn't implement %.*f
    #if range < 1e-6
    #    a, b = _magform(range)
    #    return @sprintf "%.*f" (abs(b),x)
    #end
    s = sprint(show, x; context=:compact => true)
    endswith(s, ".0") ? s[1:end-2] : s
end

range(a::Real, b::Real) = (a <= b) ? (ceil(Int, a):floor(Int, b)) :
                                     (floor(Int, a):-1:ceil(Int, b))

function _ticklist_linear(lo, hi, sep, origin=0.)
    a = (lo - origin)/sep
    b = (hi - origin)/sep
    [ origin + i*sep for i in range(a,b) ]
end

function _ticks_default_linear(lim)
    a, b = _magform(abs(lim[2] - lim[1])/5.)
    if a < (1 + 2)/2.
        x = 1
    elseif a < (2 + 5)/2.
        x = 2
    elseif a < (5 + 10)/2.
        x = 5
    else
        x = 10
    end

    major_div = x * 10.0^b
    return _ticklist_linear(lim[1], lim[2], major_div)
end

function _ticks_default_log(lim)
    a = log10(lim[1])
    b = log10(lim[2])
    r = range(a, b)
    nn = length(r)

    if nn >= 10
        return 10.0 .^ _ticks_default_linear((a,b))
    elseif nn >= 2
        return 10.0 .^ r
    else
        return _ticks_default_linear(lim)
    end
end

_ticks_num_linear(lim, num) = range(lim[1], stop=lim[2], length=num)
_ticks_num_log(lim, num) = logspace(log10(lim[1]), log10(lim[2]), num)

function _subticks_linear(lim, ticks, num=nothing)
    major_div = abs(ticks[end] - ticks[1])/float(length(ticks) - 1)
    if num === nothing
        _num = 4
        a, b = _magform(major_div)
        if 1. < a < (2 + 5)/2.
            _num = 3
        end
    else
        _num = num
    end
    minor_div = major_div/float(_num+1)
    return _ticklist_linear(lim[1], lim[2], minor_div, ticks[1])
end

function _subticks_log(lim, ticks, num=nothing)
    a = log10(lim[1])
    b = log10(lim[2])
    r = range(a, b)
    nn = length(r)

    if nn >= 10
        return 10.0 .^ _subticks_linear((a,b), map(log10,ticks), num)
    elseif nn >= 2
        minor_ticks = Float64[]
        for i in (minimum(r)-1):maximum(r)
            for j in 1:9
                z = j * 10.0^i
                if (lim[1] <= z <= lim[2]) || (lim[1] >= z >= lim[2])
                    push!(minor_ticks, z)
                end
            end
        end
        return minor_ticks
    else
        return _subticks_linear(lim, ticks, num)
    end
end

abstract type HalfAxis <: PlotComponent end

mutable struct HalfAxisX <: HalfAxis
    attr::Dict
    func_ticks_default
    func_ticks_num
    func_subticks_default
    func_subticks_num

    function HalfAxisX(args...; kvs...)
        self = new(
            Dict(),
            (_ticks_default_linear, _ticks_default_log),
            (_ticks_num_linear, _ticks_num_log),
            (_subticks_linear, _subticks_log),
            (_subticks_linear, _subticks_log),
       )
        iniattr(self)
        kw_init(self, args...; kvs...)
        self
    end
end

_pos(self::HalfAxisX, context::PlotContext, a) = _pos(self, context, a, 0.)
function _pos(self::HalfAxisX, context::PlotContext, a, db)
    intcpt = _intercept(self, context)
    p = project(context.geom, a, intcpt)
    return Point(p[1], p[2] + db)
end

function _dpos(self::HalfAxisX, d)
    return Point(0., d)
end

function _align(self::HalfAxisX)
    if getattr(self, "ticklabels_dir") < 0
        return "center", "top"
    else
        return "center", "bottom"
    end
end

function _intercept(self::HalfAxisX, context)
    if getattr(self,"intercept") !== nothing
        return getattr(self, "intercept")
    end
    limits = context.data_bbox
    if xor(getattr(self, "ticklabels_dir") < 0, context.yflipped)
        return yrange(limits)[1]
    else
        return yrange(limits)[2]
    end
end

function _log(self::HalfAxisX, context)
    if getattr(self,"log") === nothing
        return context.xlog
    end
    return getattr(self, "log")
end

function _side(self::HalfAxisX)
    if getattr(self, "ticklabels_dir") < 0
        return "bottom"
    else
        return "top"
    end
end

function _range(self::HalfAxisX, context)
    r = getattr(self, "range")
    if r !== nothing
        a,b = r
        if a === nothing || b === nothing
            c,d = xrange(context.data_bbox)
            if a === nothing
                a = c
            end
            if b === nothing
                b = d
            end
            return a,b
        else
            return r
        end
    end
    return xrange(context.data_bbox)
end

function _make_grid(self::HalfAxisX, context, ticks)
    objs = GroupPainter(getattr(self,:grid_style))
    if isequal(ticks,nothing)
        return objs
    end
    for tick in ticks
        l = LineX(tick)
        push!(objs, make(l,context))
    end
    objs
end

mutable struct HalfAxisY <: HalfAxis
    attr::Dict
    func_ticks_default
    func_ticks_num
    func_subticks_default
    func_subticks_num

    function HalfAxisY(args...; kvs...)
        self = new(
            Dict(),
            (_ticks_default_linear, _ticks_default_log),
            (_ticks_num_linear, _ticks_num_log),
            (_subticks_linear, _subticks_log),
            (_subticks_linear, _subticks_log),
       )
        iniattr(self)
        kw_init(self, args...; kvs...)
        self
    end
end

_pos(self::HalfAxisY, context, a) = _pos(self, context, a, 0.)
function _pos(self::HalfAxisY, context, a, db)
    p = project(context.geom, _intercept(self, context), a)
    return Point(p[1] + db, p[2])
end

function _dpos(self::HalfAxisY, d)
    return Point(d, 0.)
end

function _align(self::HalfAxisY)
    if getattr(self, "ticklabels_dir") > 0
        return "left", "center"
    else
        return "right", "center"
    end
end

function _intercept(self::HalfAxisY, context)
    intercept = getattr(self, "intercept")
    if intercept !== nothing
        return intercept
    end
    limits = context.data_bbox
    if xor(getattr(self, "ticklabels_dir") > 0, context.xflipped)
        return xrange(limits)[2]
    else
        return xrange(limits)[1]
    end
end

function _log(self::HalfAxisY, context)
    if getattr(self,"log") === nothing
        return context.ylog
    end
    return getattr(self, "log")
end

function _side(self::HalfAxisY)
    if getattr(self, "ticklabels_dir") > 0
        return "right"
    else
        return "left"
    end
end

function _range(self::HalfAxisY, context)
    r = getattr(self, "range")
    if r !== nothing
        a,b = r
        if a === nothing || b === nothing
            c,d = yrange(context.data_bbox)
            if a === nothing
                a = c
            end
            if b === nothing
                b = d
            end
            return a,b
        else
            return r
        end
    end
    return yrange(context.data_bbox)
end

function _make_grid(self::HalfAxisY, context, ticks)
    objs = GroupPainter(getattr(self,:grid_style))
    if isequal(ticks,nothing)
        return objs
    end
    for tick in ticks
        l = LineY(tick)
        push!(objs, make(l,context))
    end
    objs
end

# defaults

_attr_map(::HalfAxis) = Dict(
    :labeloffset       => :label_offset,
    :major_ticklabels  => :ticklabels,
    :major_ticks       => :ticks,
    :minor_ticks       => :subticks,
)

function _ticks(self::HalfAxis, context)
    logidx = _log(self, context) ? 2 : 1
    r = _range(self, context)
    ticks = getattr(self, "ticks")
    if isequal(ticks,nothing)
        return self.func_ticks_default[logidx](r)
    elseif typeof(ticks) <: Integer
        return self.func_ticks_num[logidx](r, ticks)
    else
        return ticks
    end
end

function _subticks(self::HalfAxis, context, ticks)
    logidx = _log(self, context) ? 2 : 1
    r = _range(self, context)
    subticks = getattr(self, "subticks")
    if isequal(subticks,nothing)
        return self.func_subticks_default[logidx](r, ticks)
    elseif typeof(subticks) <: Integer
        return self.func_subticks_num[logidx](r, ticks, subticks)
    else
        return subticks
    end
end

function _ticklabels(self::HalfAxis, context, ticks)
    ticklabels = getattr(self, "ticklabels")
    if !isequal(ticklabels,nothing)
        return ticklabels
    end
    r = maximum(ticks) - minimum(ticks)
    [ _format_ticklabel(x,r) for x=ticks ]
end

function _make_ticklabels(self::HalfAxis, context, pos, labels)
    if isequal(labels,nothing) || length(labels) <= 0
        return GroupPainter()
    end

    dir = getattr(self, "ticklabels_dir")
    offset = _size_relative(getattr(self, "ticklabels_offset"),
        context.dev_bbox)
    draw_ticks = getattr(self, "draw_ticks")
    if draw_ticks && getattr(self, "tickdir") > 0
        offset = offset + _size_relative(
            getattr(self, "ticks_size"), context.dev_bbox)
    end

    labelpos = Point[ _pos(self, context, pos[i], dir*offset) for i=1:length(labels) ]

    halign, valign = _align(self)

    style = Dict{Symbol,Any}()
    style[:texthalign] = halign
    style[:textvalign] = valign
    for (k,v) in getattr(self, :ticklabels_style)
        style[k] = v
    end

    l = LabelsPainter(labelpos, labels; halign=style[:texthalign], valign=style[:textvalign])
    GroupPainter(style, l)
end

function _make_spine(self::HalfAxis, context)
    a, b = _range(self, context)
    p = _pos(self, context, a)
    q = _pos(self, context, b)
    GroupPainter(getattr(self,:spine_style), LinePainter(p,q))
end

function _make_strut(self::HalfAxis, context)
    a, b = _range(self, context)
    p = _pos(self, context, a)
    q = _pos(self, context, b)
    StrutPainter(BoundingBox(p,q))
end

function _make_ticks(self::HalfAxis, context, ticks, size, style)
    if isequal(ticks,nothing) || length(ticks) <= 0
        return GroupPainter()
    end

    dir = getattr(self, "tickdir") * getattr(self, "ticklabels_dir")
    ticklen = _dpos(self, dir * _size_relative(size, context.dev_bbox))

    tickpos = Point[ _pos(self, context, tick) for tick in ticks ]

    GroupPainter(style, CombPainter(tickpos, ticklen))
end

function make(self::HalfAxis, context)
    objs = GroupPainter(getattr(self,:style))
    if getattr(self, "draw_nothing")
        return objs
    end

    ticks = _ticks(self, context)
    subticks = _subticks(self, context, ticks)
    ticklabels = _ticklabels(self, context, ticks)
    draw_ticks = getattr(self, "draw_ticks")
    draw_subticks = getattr(self, "draw_subticks")
    draw_ticklabels = getattr(self, "draw_ticklabels")

    implicit_draw_subticks = draw_subticks === nothing && draw_ticks

    implicit_draw_ticklabels = draw_ticklabels === nothing &&
        (getattr(self, "range") !== nothing || getattr(self, "ticklabels") !== nothing)

    if getattr(self, "draw_grid")
        push!(objs, _make_grid(self, context, ticks))
    end

    if getattr(self, "draw_axis")
        if (draw_subticks !== nothing && draw_subticks) || implicit_draw_subticks
            push!(objs, _make_ticks(self, context, subticks,
                getattr(self, "subticks_size"),
                getattr(self, "subticks_style")))
        end

        if draw_ticks
            push!(objs, _make_ticks(self, context, ticks,
                getattr(self, "ticks_size"),
                getattr(self, "ticks_style")))
        end

        if getattr(self, "draw_spine")
            push!(objs, _make_spine(self, context))
        end
    end

    if (draw_ticklabels !== nothing && draw_ticklabels) || implicit_draw_ticklabels
        push!(objs, _make_ticklabels(self, context, ticks, ticklabels))
    end

    # has to be made last
    if hasattr(self, "label")
        if getattr(self, "label") !== nothing # XXX:remove
            isempty(objs) && push!(objs, _make_strut(self, context))
            bl = BoxLabel(
                objs,
                getattr(self, "label"),
                _side(self),
                getattr(self, "label_offset"),
                getattr(self, "label_style"))
            push!(objs, make(bl,context))
        end
    end
    objs
end

# PlotComposite ---------------------------------------------------------------

mutable struct PlotComposite <: HasStyle
    attr::Dict
    components::Vector{Any}
    dont_clip::Bool

    function PlotComposite(args...; kvs...)
        self = new(Dict(), Any[], false)
        kw_init(self, args...; kvs...)
        self
    end
end

function add(self::PlotComposite, args::PlotComponent...)
    for arg in args
        push!(self.components, arg)
    end
    self
end

function clear(self::PlotComposite)
    self.components = Any[]
end

function isempty(self::PlotComposite)
    return isempty(self.components)
end

function limits(self::PlotComposite, window::BoundingBox)
    bb = BoundingBox()
    for obj in self.components
        bb += limits(obj, window)
    end
    return bb
end

function make(self::PlotComposite, context)
end

function boundingbox(self::PlotComposite, context)
    make(self, context)
    bb = BoundingBox()
    for obj in self.components
        bb += boundingbox(obj,context)
    end
    return bb
end

function render(self::PlotComposite, context)
    make(self, context)
    push_style(context.paintc, getattr(self,"style"))
    if !self.dont_clip
        set(context.draw, "cliprect", context.dev_bbox)
    end
    for obj in self.components
        render(obj, context)
    end
    pop_style(context.paintc)
end

# FramedPlot ------------------------------------------------------------------

mutable struct _Alias <: HasAttr
    objs
    _Alias(args...) = new(args)
end

function setattr(self::_Alias, name::Symbol, value)
    for obj in self.objs
        setattr(obj, name, value)
    end
end

mutable struct FramedPlot <: PlotContainer
    attr::AbstractDict # TODO: does Associative need {K,V}?
    content1::PlotComposite
    content2::PlotComposite
    x1::HalfAxis
    y1::HalfAxis
    x2::HalfAxis
    y2::HalfAxis
    frame::_Alias
    frame1::_Alias
    frame2::_Alias
    x::_Alias
    y::_Alias

    function FramedPlot(args...; kvs...)
        x1 = HalfAxisX()
        setattr(x1, :ticklabels_dir, -1)
        y1 = HalfAxisY()
        setattr(y1, :ticklabels_dir, -1)
        x2 = HalfAxisX()
        setattr(x2, :draw_ticklabels, nothing)
        y2 = HalfAxisY()
        setattr(y2, :draw_ticklabels, nothing)
        self = new(
            Dict(),
            PlotComposite(),
            PlotComposite(),
            x1, y1, x2, y2,
            _Alias(x1, x2, y1, y2),
            _Alias(x1, y1),
            _Alias(x2, y2),
            _Alias(x1, x2),
            _Alias(y1, y2),
        )
        gs = Dict{Symbol,Any}()
        gs[:linekind] = "dot"
        setattr(self.frame, :grid_style, gs)
        setattr(self.frame, :tickdir, -1)
        setattr(self.frame1, :draw_grid, false)
        iniattr(self, args...; kvs...)
        self
    end
end

_attr_map(fp::FramedPlot) = Dict(
    :xlabel    => (fp.x1, :label),
    :ylabel    => (fp.y1, :label),
    :xlog      => (fp.x1, :log),
    :ylog      => (fp.y1, :log),
    :xrange    => (fp.x1, :range),
    :yrange    => (fp.y1, :range),
    :xtitle    => (fp.x1, :label),
    :ytitle    => (fp.y1, :label),
)

function getattr(self::FramedPlot, name::Symbol)
    am = _attr_map(self)
    if haskey(am, name)
        a,b = am[name]
        return getattr(a, b)
    else
        return self.attr[name]
    end
end

function setattr(self::FramedPlot, name::Symbol, value)
    am = _attr_map(self)
    if haskey(am, name)
        a,b = am[name]
        setattr(a, b, value)
    else
        self.attr[name] = value
    end
end

function isempty(self::FramedPlot)
    return isempty(self.content1) && isempty(self.content2)
end

function add(self::FramedPlot, args::PlotComponent...)
    add(self.content1, args...)
    self
end

function add2(self::FramedPlot, args::PlotComponent...)
    add(self.content2, args...)
    self
end

myprevfloat(x::Float64) = x - eps(x)
mynextfloat(x::Float64) = x + eps(x)

function user_range(range)
    lo = NaN
    hi = NaN
    flipped = false
    if range !== nothing
        b1 = typeof(range[1]) <: Real
        b2 = typeof(range[2]) <: Real
        if b1 && b2
            x1 = Float64(range[1])
            x2 = Float64(range[2])
            lo = myprevfloat(NaNMath.min(x1, x2))
            hi = mynextfloat(NaNMath.max(x1, x2))
            flipped = x1 > x2
        else
            b1 && (lo = myprevfloat(Float64(range[1])))
            b2 && (hi = mynextfloat(Float64(range[2])))
        end
    end
    lo, hi, flipped
end

function bbox_to_rect(bbox::BoundingBox, xflipped::Bool, yflipped::Bool)
    a = xflipped ? (bbox.xmax,bbox.xmin) : (bbox.xmin,bbox.xmax)
    b = yflipped ? (bbox.ymax,bbox.ymin) : (bbox.ymin,bbox.ymax)
    Rectangle(a..., b...)
end

function margin_expand(margin::Real, a::Real, b::Real, islog::Bool)
    if islog
        f = a == b ? 1.1 : 10.0^(0.5*margin*(log10(b) - log10(a)))
        return a/f, b*f
    else
        d = a == b ? 1. : 0.5*margin*(b - a)
        return a-d, b+d
    end
end

function override(bb1::BoundingBox, bb2::BoundingBox)
    BoundingBox(
        isnan(bb1.xmin) ? bb2.xmin : bb1.xmin,
        isnan(bb1.xmax) ? bb2.xmax : bb1.xmax,
        isnan(bb1.ymin) ? bb2.ymin : bb1.ymin,
        isnan(bb1.ymax) ? bb2.ymax : bb1.ymax)
end

function limits(margin, xrange, yrange, xlog, ylog, content)
    xmin,xmax,xflipped = user_range(xrange)
    ymin,ymax,yflipped = user_range(yrange)
    user_bbox = BoundingBox(xmin, xmax, ymin, ymax)
    geom_bbox = BoundingBox(xlog ? 0. : -Inf, Inf, ylog ? 0. : -Inf, Inf)

    # check that user values are valid
    user_bbox.xmin <= geom_bbox.xmin && throw(WinstonException("bad xmin"))
    user_bbox.xmax >= geom_bbox.xmax && throw(WinstonException("bad xmax"))
    user_bbox.ymin <= geom_bbox.ymin && throw(WinstonException("bad ymin"))
    user_bbox.ymax >= geom_bbox.ymax && throw(WinstonException("bad ymax"))

    if !isincomplete(user_bbox)
        return bbox_to_rect(user_bbox, xflipped, yflipped)
    end

    data_bbox = limits(content, geom_bbox & user_bbox)
    isincomplete(data_bbox) && throw(WinstonException("no data in range"))

    a,b = margin_expand(margin, data_bbox.xmin, data_bbox.xmax, xlog)
    c,d = margin_expand(margin, data_bbox.ymin, data_bbox.ymax, ylog)
    computed_bbox = BoundingBox(a, b, c, d)

    # user trumps computed
    bbox = override(user_bbox, computed_bbox)

    bbox_to_rect(bbox, xflipped, yflipped)
end

function limits(fp::FramedPlot)
    margin = getattr(fp,    :gutter)
    xrange = getattr(fp.x1, :range)
    yrange = getattr(fp.y1, :range)
    xlog   = getattr(fp.x1, :log)
    ylog   = getattr(fp.y1, :log)
    limits(margin, xrange, yrange, xlog, ylog, fp.content1)
end

function limits2(fp::FramedPlot)
    xrange = getattr(fp.x2, :range)
    yrange = getattr(fp.y2, :range)

    if !isempty(fp.content2)
        margin = getattr(fp,    :gutter)
        xlog   = getattr(fp.x2, :log)
        ylog   = getattr(fp.y2, :log)

        xlog === nothing && (xlog = getattr(fp.x1, :log))
        ylog === nothing && (ylog = getattr(fp.y1, :log))

        return limits(margin, xrange, yrange, xlog, ylog, fp.content2)
    end

    xrange === nothing && (xrange = getattr(fp.x1,:range))
    yrange === nothing && (yrange = getattr(fp.y1,:range))

    rect = limits(fp)
    computed_bbox = BoundingBox(rect)

    xmin,xmax,xflipped = user_range(xrange)
    ymin,ymax,yflipped = user_range(yrange)
    user_bbox = BoundingBox(xmin, xmax, ymin, ymax)

    bbox = override(user_bbox, computed_bbox)
    Rectangle(bbox, xflipped, yflipped)
end

function _context1(self::FramedPlot, device::Renderer, region::BoundingBox)
    xlog = getattr(self.x1, :log)
    ylog = getattr(self.y1, :log)
    lims = limits(self)
    proj = PlotGeometry(lims, region, xlog, ylog)
    return PlotContext(device, region, lims, proj, xlog, ylog)
end

function _context2(self::FramedPlot, device::Renderer, region::BoundingBox)
    xlog = getattr(self.x1, :log)
    ylog = getattr(self.y1, :log)

    getattr(self.x2, :log) !== nothing && (xlog = getattr(self.x2, :log))
    getattr(self.y2, :log) !== nothing && (ylog = getattr(self.y2, :log))

    lims = limits2(self)
    proj = PlotGeometry(lims, region, xlog, ylog)
    return PlotContext(device, region, lims, proj, xlog, ylog)
end

function exterior(self::FramedPlot, device::Renderer, region::BoundingBox)
    bb = region

    context1 = _context1(self, device, region)
    bb += boundingbox(self.x1, context1) +
          boundingbox(self.y1, context1)

    context2 = _context2(self, device, region)
    bb += boundingbox(self.x2, context2) +
          boundingbox(self.y2, context2)

    return bb
end

function compose_interior(self::FramedPlot, device::Renderer, region::BoundingBox)
    invoke(compose_interior, Tuple{PlotContainer,Renderer,BoundingBox}, self, device, region)

    context1 = _context1(self, device, region)
    context2 = _context2(self, device, region)

    render(self.content1, context1)
    render(self.content2, context2)

    render(self.y2, context2)
    render(self.x2, context2)
    render(self.y1, context1)
    render(self.x1, context1)
end

getcomponents(p::FramedPlot, c) = getfield(p, [:content1, :content2][c]).components
getcomponents(p::FramedPlot) = [getcomponents(p,1); getcomponents(p,2)]

rmcomponents(p::FramedPlot, i::Integer, c) = splice!(getfield(p, [:content1, :content2][c]).components, i)

function rmcomponents(p::FramedPlot, i::Integer)
    cont1 = getcomponents(p, 1)
    cont2 = getcomponents(p, 2)
    if i <= length(cont1)
        rmcomponents(p, i, 1)
    elseif i <= length(cont1) + length(cont2)
        rmcomponents(p, i - lenght(cont1), 2)
    else
        error("Requested remove item #$i from $(length(cont1)+length(cont2)) components.")
    end
end

function rmcomponents(p::FramedPlot, v::AbstractVector, args...)
    eltype(v) <: Integer && sort!(v, rev=true)
    for i in v
        rmcomponents(p, i, args...)
    end
end

function rmcomponents(p::FramedPlot, t::Type, args...)
    ctypes = map(typeof, getcomponents(p, args...))
    todel = findall(map(x -> (x <: t), ctypes))
    rmcomponents(p, todel, args...)
end

# Table ------------------------------------------------------------------------

mutable struct _Grid
    nrows::Int
    ncols::Int
    origin
    step_x
    step_y
    cell_dimen

    function _Grid(nrows, ncols, bbox, cellpadding, cellspacing)
        self = new()
        self.nrows = nrows
        self.ncols = ncols

        w, h = width(bbox), height(bbox)
        cp = _size_relative(cellpadding, bbox)
        cs = _size_relative(cellspacing, bbox)

        self.origin = lowerleft(bbox) + Point(cp,cp)
        self.step_x = (w + cs)/ncols
        self.step_y = (h + cs)/nrows
        self.cell_dimen = (self.step_x - cs - 2*cp,
            self.step_y - cs - 2*cp)
        self
    end
end

function cellbb(self::_Grid, i::Int, j::Int)
    ii = self.nrows - i
    p = self.origin + Point((j-1)*self.step_x, ii*self.step_y)
    return BoundingBox(p.x, p.x+self.cell_dimen[1], p.y, p.y + self.cell_dimen[2])
end

mutable struct Table <: PlotContainer
    attr::PlotAttributes
    rows::Int
    cols::Int
    content

    function Table(rows, cols, args...)
        self = new(Dict())
        iniattr(self, args...)
        self.rows = rows
        self.cols = cols
        self.content = Matrix{Any}(undef, rows, cols)
        self
    end
end

function getindex(self::Table, row::Int, col::Int)
    return self.content[row,col]
end

function setindex!(self::Table, obj::PlotContainer, row::Int, col::Int)
    self.content[row,col] = obj
end

function isempty(self::Table)
    for i = 1:self.rows
        for j = 1:self.cols
            isassigned(self.content, i, j) && return false
        end
    end
    true
end

function exterior(self::Table, device::Renderer, intbbox::BoundingBox)
    ext = intbbox

    if getattr(self, "align_interiors")
        g = _Grid(self.rows, self.cols, intbbox,
            getattr(self,"cellpadding"), getattr(self,"cellspacing"))

        for i = 1:self.rows
            for j = 1:self.cols
                if isassigned(self.content, i, j)
                    obj = self.content[i,j]
                    subregion = cellbb(g, i, j)
                    ext += exterior(obj, device, subregion)
                end
            end
        end
    end
    return ext
end

function compose_interior(self::Table, device::Renderer, intbbox::BoundingBox)
    invoke(compose_interior, Tuple{PlotContainer,Renderer,BoundingBox}, self, device, intbbox)

    g = _Grid(self.rows, self.cols, intbbox,
        getattr(self,"cellpadding"), getattr(self,"cellspacing"))

    for i = 1:self.rows
        for j = 1:self.cols
            if isassigned(self.content, i, j)
                obj = self.content[i,j]
                subregion = cellbb(g, i, j)
                if getattr(self, "align_interiors")
                    compose_interior(obj, device, subregion)
                else
                    compose(obj, device, subregion)
                end
            end
        end
    end
end

# Plot ------------------------------------------------------------------------

mutable struct Plot <: PlotContainer
    attr::PlotAttributes
    content

    function Plot(args...)
        self = new(Dict())
        iniattr(self, args...)
        self.content = PlotComposite()
        self
    end
end

function isempty(self::Plot)
    return isempty(self.content)
end

function add(self::Plot, args::PlotComponent...)
    add(self.content, args...)
    self
end

function limits(self::Plot, window::BoundingBox)
    return _limits(limits(self.content,window), getattr(self,"gutter"),
                   getattr(self,"xlog"), getattr(self,"ylog"),
                   getattr(self,"xrange"), getattr(self,"yrange"))
end

compose_interior(self::Plot, device::Renderer, region::BoundingBox) =
    compose_interior(self, device, region, nothing)
function compose_interior(self::Plot, device, region, lmts)
    if lmts === nothing
        lmts = limits(self)
    end
    xlog = getattr(self,"xlog")
    ylog = getattr(self,"ylog")
    proj = PlotGeometry(lmts, region, xlog, ylog)
    context = PlotContext(device, region, lmts, proj, xlog, ylog)
    render(self.content, context)
end

compose(self::Plot, device::Renderer, region::BoundingBox) =
    compose(self, device, region, nothing)
function compose(self::Plot, device, region, lmts)
    int_bbox = interior(self, device, region)
    compose_interior(self, device, int_bbox, lmts)
end

# FramedArray -----------------------------------------------------------------
#
# Quick and dirty, dirty hack...
#

function _frame_draw(obj, device, region, limits, labelticks)
    frame = Frame(labelticks)
    xlog = getattr(obj, "xlog")
    ylog = getattr(obj, "ylog")
    proj = PlotGeometry(limits, region, xlog, ylog)
    context = PlotContext(device, region, limits, proj, xlog, ylog)
    render(frame, context)
end

_frame_bbox(obj, device, region, limits) =
    _frame_bbox(obj, device, region, limits, (0,1,1,0))
function _frame_bbox(obj, device, region, limits, labelticks)
    frame = Frame(labelticks)
    xlog = getattr(obj, "xlog")
    ylog = getattr(obj, "ylog")
    proj = PlotGeometry(limits, region, xlog, ylog)
    context = PlotContext(device, region, limits, proj, xlog, ylog)
    return boundingbox(frame, context)
end

function _range_union(a, b)
    if a === nothing
        return b
    end
    if b === nothing
        return a
    end
    return NaNMath.min(a[1],b[1]), NaNMath.max(a[2],b[2])
end

mutable struct FramedArray <: PlotContainer
    attr::PlotAttributes
    nrows::Int
    ncols::Int
    content::Array{Any,2}

    function FramedArray(nrows, ncols, args...; kvs...)
        self = new(Dict())
        self.nrows = nrows
        self.ncols = ncols
        self.content = Matrix{Any}(nrows, ncols)
        for i in 1:nrows
            for j in 1:ncols
                self.content[i,j] = Plot()
            end
        end
        iniattr(self, args...; kvs...)
        self
    end
end

function getindex(self::FramedArray, row::Int, col::Int)
    return self.content[row,col]
end

# XXX:fixme
isempty(self::FramedArray) = false

function setattr(self::FramedArray, name::Symbol, value)
    _attr_distribute = Set(
        "gutter",
        "xlog",
        "ylog",
        "xrange",
        "yrange",
    )
    if name in _attr_distribute
        for i in 1:self.nrows, j=1:self.ncols
            setattr(self.content[i,j], name, value)
        end
    else
        self.attr[name] = value
    end
end

function _limits(self::FramedArray, i, j)
    if getattr(self, "uniform_limits")
        return _limits_uniform(self)
    else
        return _limits_nonuniform(self, i, j)
    end
end

function _limits_uniform(self::FramedArray)
    lmts = BoundingBox()
    for i in 1:self.nrows, j=1:self.ncols
        obj = self.content[i,j]
        # XXX:fixme
        window = BoundingBox(-Inf, Inf, -Inf, Inf)
        lmts += limits(obj, window)
    end
    return lmts
end

function _limits_nonuniform(self::FramedArray, i, j)
    lx = nothing
    for k in 1:self.nrows
        # XXX:fixme
        window = BoundingBox(-Inf, Inf, -Inf, Inf)
        l = limits(self.content[k,j], window)
        lx = _range_union(xrange(l), lx)
    end
    ly = nothing
    for k in 1:self.ncols
        # XXX:fixme
        window = BoundingBox(-Inf, Inf, -Inf, Inf)
        l = limits(self.content[i,k], window)
        ly = _range_union(yrange(l), ly)
    end
    return BoundingBox(lx[1], lx[2], ly[1], ly[2])
end

function _grid(self::FramedArray, interior)
    return _Grid(self.nrows, self.ncols, interior, 0., getattr(self, "cellspacing"))
end

function _frames_bbox(self::FramedArray, device, interior)
    bb = BoundingBox()
    g = _grid(self, interior)
    corners = [(1,1),(self.nrows,self.ncols)]

    for (i,j) in corners
        obj = self.content[i,j]
        subregion = cellbb(g, i, j)
        limits = _limits(self, i, j)
        axislabels = [0,0,0,0]
        if i == self.nrows
            axislabels[2] = 1
        end
        if j == 1
            axislabels[3] = 1
        end
        bb += _frame_bbox(obj, device, subregion, limits, axislabels)
    end

    return bb
end

function exterior(self::FramedArray, device::Renderer, int_bbox::BoundingBox)
    bb = _frames_bbox(self, device, int_bbox)

    labeloffset = _size_relative(getattr(self,"label_offset"), int_bbox)
    labelsize = _fontsize_relative(
        getattr(self,"label_size"), int_bbox, boundingbox(device))
    margin = labeloffset + labelsize

    if getattr(self,"xlabel") !== nothing
        bb = deform(bb, 0, 0, margin, 0)
    end
    if getattr(self,"ylabel") !== nothing
        bb = deform(bb, margin, 0, 0, 0)
    end

    return bb
end

function _frames_draw(self::FramedArray, device, interior)
    g = _grid(self, interior)

    for i in 1:self.nrows, j=1:self.ncols
        obj = self.content[i,j]
        subregion = cellbb(g, i, j)
        limits = _limits(self, i, j)
        axislabels = [0,0,0,0]
        if i == self.nrows
            axislabels[2] = 1
        end
        if j == 1
            axislabels[3] = 1
        end
        _frame_draw(obj, device, subregion, limits, axislabels)
    end
end

function _data_draw(self::FramedArray, device, interior)
    g = _grid(self, interior)

    for i in 1:self.nrows, j=1:self.ncols
        obj = self.content[i,j]
        subregion = cellbb(g, i, j)
        lmts = _limits(self, i, j)
        compose_interior(obj, device, subregion, lmts)
    end
end

function _labels_draw(self::FramedArray, device::Renderer, int_bbox::BoundingBox)
    bb = _frames_bbox(self, device, int_bbox)

    labeloffset = _size_relative(getattr(self,"label_offset"), int_bbox)
    labelsize = _fontsize_relative(
        getattr(self,"label_size"), int_bbox, boundingbox(device))

    save_state(device)
    set(device, "fontsize", labelsize)
    if getattr(self,"xlabel") !== nothing
        x = center(int_bbox).x
        y = ymin(bb) - labeloffset
        textdraw(device, x, y, getattr(self,"xlabel"); halign="center", valign="top")
    end
    if getattr(self,"ylabel") !== nothing
        x = xmin(bb) - labeloffset
        y = center(int_bbox).y
        textdraw(device, x, y, getattr(self,"ylabel"); angle=90., halign="center", valign="bottom")
    end
    restore_state(device)
end

function add(self::FramedArray, args::PlotComponent...)
    for i in 1:self.nrows, j=1:self.ncols
        obj = self.content[i,j]
        add(obj, args...)
    end
    self
end

function compose_interior(self::FramedArray, device::Renderer, int_bbox::BoundingBox)
    invoke(compose_interior, Tuple{PlotContainer,Renderer,BoundingBox}, self, device, int_bbox)
    _data_draw(self, device, int_bbox)
    _frames_draw(self, device, int_bbox)
    _labels_draw(self, device, int_bbox)
end

# Frame -----------------------------------------------------------------------

#type Frame
#    pc::PlotComposite
#    x1
#    x2
#    y1
#    y2
#
#    #function __init__(self, labelticks=(0,1,1,0), args...)
#        #apply(_PlotComposite.__init__, (self,), args...)
#    function Frame(labelticks, args...)
#        self = new()
#        pc = PlotComposite(args...)
#        pc.dont_clip = 1
#
#        self.x2 = _HalfAxisX()
#        self.x2.draw_ticklabels = labelticks[1]
#        self.x2.ticklabels_dir = 1
#
#        self.x1 = _HalfAxisX()
#        self.x1.draw_ticklabels = labelticks[2]
#        self.x1.ticklabels_dir = -1
#
#        self.y1 = _HalfAxisY()
#        self.y1.draw_ticklabels = labelticks[3]
#        self.y1.ticklabels_dir = -1
#
#        self.y2 = _HalfAxisY()
#        self.y2.draw_ticklabels = labelticks[4]
#        self.y2.ticklabels_dir = 1
#        self
#    end
#end
#
#function make(self::Frame, context)
#    clear(self)
#    add(self, self.x1, self.x2, self.y1, self.y2)
#end

function Frame(labelticks, args...)
    #apply(_PlotComposite.__init__, (self,), args...)
    pc = PlotComposite(args...)
    setattr(pc, "dont_clip", true)

    x2 = HalfAxisX()
    setattr(x2, "draw_ticklabels", labelticks[1]==1)
    setattr(x2, "ticklabels_dir", 1)

    x1 = HalfAxisX()
    setattr(x1, "draw_ticklabels", labelticks[2]==1)
    setattr(x1, "ticklabels_dir", -1)

    y1 = HalfAxisY()
    setattr(y1, "draw_ticklabels", labelticks[3]==1)
    setattr(y1, "ticklabels_dir", -1)

    y2 = HalfAxisY()
    setattr(y2, "draw_ticklabels", labelticks[4]==1)
    setattr(y2, "ticklabels_dir", 1)

    add(pc, x1, x2, y1, y2)
    pc
end

# PlotContainer ---------------------------------------------------------------

function show(io::IO, self::PlotContainer)
    print(io, typeof(self), "(...)")
end

function interior(self::PlotContainer, device::Renderer, exterior_bbox::BoundingBox)
    TOL = 0.005

    interior_bbox = exterior_bbox
    region_diagonal = diagonal(exterior_bbox)

    for i in 1:10
        bb = exterior(self, device, interior_bbox)

        dll = lowerleft(exterior_bbox) - lowerleft(bb)
        dur = upperright(exterior_bbox) - upperright(bb)

        sll = norm(dll) / region_diagonal
        sur = norm(dur) / region_diagonal

        if sll < TOL && sur < TOL
            # XXX:fixme
            ar = getattr(self, "aspect_ratio")
            if ar !== nothing
                interior_bbox = with_aspect_ratio(interior_bbox, ar)
            end
            return interior_bbox
        end

        scale = diagonal(interior_bbox) / diagonal(bb)
        dll = scale * dll
        dur = scale * dur

        interior_bbox = BoundingBox(lowerleft(interior_bbox) + dll,
                                    upperright(interior_bbox) + dur)
    end

    println("warning: sub-optimal solution for plot")
    return interior_bbox
end

function exterior(self::PlotContainer, device::Renderer, interior::BoundingBox)
    return interior
end

function compose_interior(self::PlotContainer, device::Renderer, int_bbox::BoundingBox)
    # XXX: separate out into its own component
    if hasattr(self, :title)
        offset = _size_relative(getattr(self, :title_offset), int_bbox)
        ext_bbox = exterior(self, device, int_bbox)
        x = center(int_bbox).x
        y = ymax(ext_bbox) + offset
        style = Dict()
        for (k,v) in getattr(self, :title_style)
            style[k] = v
        end
        style[:fontsize] = _fontsize_relative(
            getattr(self,:title_style)[:fontsize], int_bbox, boundingbox(device))
        save_state(device)
        for (key,val) in style
            set(device, key, val)
        end
        textdraw(device, x, y, getattr(self,:title); halign="center", valign="bottom")
        restore_state(device)
    end
end

function compose(self::PlotContainer, device::Renderer, region::BoundingBox)
    if isempty(self) return end
    ext_bbox = region
    if hasattr(self, :title)
        offset = _size_relative(getattr(self,"title_offset"), ext_bbox)
        fontsize = _fontsize_relative(
            getattr(self,:title_style)[:fontsize], ext_bbox, boundingbox(device))
        ext_bbox = deform(ext_bbox, 0, 0, 0, -offset-fontsize)
    end
    int_bbox = interior(self, device, ext_bbox)
    compose_interior(self, device, int_bbox)
end

page_compose(self::PlotContainer, device::GraphicsDevice) =
    page_compose(self, CairoRenderer(device))

function page_compose(self::PlotContainer, device::Renderer)
    bb = boundingbox(device)
    for (key,val) in config_options("defaults")
        set(device, key, val)
    end
    bb *= 1.0 - getattr(self, :page_margin)
    save(device.ctx)
    Cairo.scale(device.ctx,1.0,-1.0)
    Cairo.translate(device.ctx,0.0,-height(device))
    compose(self, device, bb)
    restore(device.ctx)
end

function write_to_surface(p::PlotContainer, surface)
    r = CairoRenderer(surface)
    page_compose(p, r)
    show_page(r.ctx)
    finish(surface)
end

function savesvg(p::PlotContainer, io::IO, width, height)
    surface = CairoSVGSurface(io, width, height)
    write_to_surface(p, surface)
end

function savesvg(p::PlotContainer, filename::AbstractString, width, height)
    io = Base.Filesystem.open(filename, Base.JL_O_CREAT|Base.JL_O_TRUNC|Base.JL_O_WRONLY, 0o644)
    savesvg(p, io, width, height)
    close(io)
    nothing
end

function saveeps(self::PlotContainer, filename::AbstractString, width::AbstractString, height::AbstractString)
    saveeps(self, filename, _str_size_to_pts(width), _str_size_to_pts(height))
end

function saveeps(self::PlotContainer, filename::AbstractString, width::Real, height::Real)
    surface = CairoEPSSurface(filename, width, height)
    write_to_surface(self, surface)
end

function savepdf(self, filename::AbstractString, width::AbstractString, height::AbstractString)
    savepdf(self, filename, _str_size_to_pts(width), _str_size_to_pts(height))
end

function savepdf(self::PlotContainer, filename::AbstractString, width::Real, height::Real)
    surface = CairoPDFSurface(filename, width, height)
    write_to_surface(self, surface)
end

function savepdf(plots::Vector{T}, filename::AbstractString, width::Real, height::Real) where T<:PlotContainer
    surface = CairoPDFSurface(filename, width, height)
    r = CairoRenderer(surface)
    for plt in plots
        page_compose(plt, r)
        show_page(r.ctx)
    end
    finish(surface)
end

function savepng(self::PlotContainer, io_or_filename::Union{IO,AbstractString}, width::Int, height::Int)
    surface = CairoRGBSurface(width, height)
    r = CairoRenderer(surface)
    set_source_rgb(r.ctx, 1.,1.,1.)
    paint(r.ctx)
    set_source_rgb(r.ctx, 0.,0.,0.)
    page_compose(self, r)
    write_to_png(surface, io_or_filename)
    finish(surface)
end

function savefig(self::PlotContainer, filename::AbstractString, args...; kvs...)
    extn = filename[end-2:end]
    opts = args2dict(args...; kvs...)
    if extn == "eps"
        width = get(opts,:width,config_value("eps","width"))
        height = get(opts,:height,config_value("eps","height"))
        saveeps(self, filename, width, height)
    elseif extn == "pdf"
        width = get(opts,:width,config_value("pdf","width"))
        height = get(opts,:height,config_value("pdf","height"))
        savepdf(self, filename, width, height)
    elseif extn == "png"
        width = get(opts,:width,config_value("window","width"))
        height = get(opts,:height,config_value("window","height"))
        savepng(self, filename, width, height)
    elseif extn == "svg"
        width = get(opts, :width, config_value("svg","width"))
        height = get(opts, :height, config_value("svg","height"))
        savesvg(self, filename, width, height)
    else
        error("I can't export .$extn, sorry.")
    end
end

function savefig(plots::Vector{T}, filename::AbstractString, args...; kvs...) where T<:PlotContainer
    extn = filename[end-2:end]
    opts = args2dict(args...; kvs...)
    if extn == "pdf"
        width = get(opts,:width,config_value("pdf","width"))
        height = get(opts,:height,config_value("pdf","height"))
        savepdf(plots, filename, width, height)
    else
        error("I can't export multiple pages to .$extn, sorry.")
    end
end

function svg(self::PlotContainer, args...; kvs...)
    opts = args2dict(args...; kvs...)
    width = get(opts,:width,config_value("window","width"))
    height = get(opts,:height,config_value("window","height"))
    stream = IOBuffer()

    savesvg(self, stream, width, height)

    s = takebuf_string(stream)
    a,b = search(s, "<svg")
    s[a:end]
end

#function multipage(plots, filename, args...)
#    file = _open_output(filename)
#    opt = copy(config_options("postscript"))
#    opt.update(args...)
#    device = PSRenderer(file, opt...)
#    for plot in plots
#        page_compose(plot, device)
#    end
#    delete(device)
#    _close_output(file)
#end

# LineComponent ---------------------------------------------------------------

abstract type LineComponent <: PlotComponent end

_kw_rename(::LineComponent) = Dict(
    :color => :linecolor,
    :kind => :linekind,
    :width => :linewidth,
    # deprecated
    :type => :linekind,
    :linetype => :linekind,
)

function make_key(self::LineComponent, bbox::BoundingBox)
    y = center(bbox).y
    p = Point(xmin(bbox), y)
    q = Point(xmax(bbox), y)
    GroupPainter(getattr(self,:style), LinePainter(p, q))
end

mutable struct Curve <: LineComponent
    attr::Dict
    x
    y

    function Curve(x::AbstractArray, y::AbstractArray, args...; kvs...)
        attr = Dict()
        self = new(attr, x, y)
        iniattr(self)
        kw_init(self, args...; kvs...)
        self
    end
end

limits(self::Curve, window::BoundingBox) = bounds_within(self.x, self.y, window)

function make(self::Curve, context)
    x, y = project(context.geom, self.x, self.y)
    GroupPainter(getattr(self,:style), PathPainter(x,y))
end

mutable struct Slope <: LineComponent
    attr::Dict
    slope::Real
    intercept

    function Slope(slope, intercept, args...; kvs...)
        #LineComponent.__init__(self)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.slope = slope
        self.intercept = intercept
        self
    end
end

function _x(self::Slope, y::Real)
    x0, y0 = self.intercept
    return x0 + float(y - y0) / self.slope
end

function _y(self::Slope, x::Real)
    x0, y0 = self.intercept
    return y0 + (x - x0) * self.slope
end

function make(self::Slope, context::PlotContext)
    xr = xrange(context.data_bbox)
    yr = yrange(context.data_bbox)
    if self.slope == 0
        l = Any[ Point(xr[1], self.intercept[2]),
                 Point(xr[2], self.intercept[2]) ]
    else
        l = Any[ Point(xr[1], _y(self, xr[1])),
                 Point(xr[2], _y(self, xr[2])),
                 Point(_x(self, yr[1]), yr[1]),
                 Point(_x(self, yr[2]), yr[2]) ]
    end
    m = Any[]
    for el in l
        if isinside(context.data_bbox, el)
            push!(m, el)
        end
    end
    #sort!(m)
    objs = GroupPainter(getattr(self,:style))
    if length(m) > 1
        a = project(context.geom, m[1])
        b = project(context.geom, m[end])
        push!(objs, LinePainter(a, b))
    end
    objs
end

mutable struct Histogram <: LineComponent
    attr::PlotAttributes
    edges::AbstractVector
    values::AbstractVector

    function Histogram(edges, counts, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.edges = edges
        self.values = counts
        self
    end
end

function limits(self::Histogram, window::BoundingBox)
    if getattr(self, "drop_to_zero")
        return bounds_within(self.edges, self.values, window) +
               bounds_within(self.edges, zeros(length(self.values)), window)
    else
        return bounds_within(self.edges, self.values, window)
    end
end

function make(self::Histogram, context::PlotContext)
    nval = length(self.values)
    drop_to_zero = getattr(self, "drop_to_zero")
    x = Float64[]
    y = Float64[]
    if drop_to_zero
        push!(x, first(self.edges))
        push!(y, 0.)
    end
    for i in 1:nval
        yi = self.values[i]
        push!(x, self.edges[i])
        push!(x, self.edges[i+1])
        push!(y, yi)
        push!(y, yi)
    end
    if drop_to_zero
        push!(x, last(self.edges))
        push!(y, 0.)
    end
    u, v = project(context.geom, x, y)
    GroupPainter(getattr(self,:style), PathPainter(u, v))
end

mutable struct LineX <: LineComponent
    attr::PlotAttributes
    x::Float64

    function LineX(x, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self
    end
end

function limits(self::LineX, window::BoundingBox)
    return BoundingBox(self.x, self.x, NaN, NaN)
end

function make(self::LineX, context::PlotContext)
    yr = yrange(context.data_bbox)
    a = project(context.geom, Point(self.x, yr[1]))
    b = project(context.geom, Point(self.x, yr[2]))
    GroupPainter(getattr(self,:style), LinePainter(a, b))
end

mutable struct LineY <: LineComponent
    attr::PlotAttributes
    y::Float64

    function LineY(y, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.y = y
        self
    end
end

function limits(self::LineY, window::BoundingBox)
    return BoundingBox(NaN, NaN, self.y, self.y)
end

function make(self::LineY, context::PlotContext)
    xr = xrange(context.data_bbox)
    a = project(context.geom, Point(xr[1], self.y))
    b = project(context.geom, Point(xr[2], self.y))
    GroupPainter(getattr(self,:style), LinePainter(a, b))
end

mutable struct BoxLabel <: PlotComponent
    attr::PlotAttributes
    obj
    str::AbstractString
    side
    offset

    function BoxLabel(obj, str::AbstractString, side, offset, args...; kvs...)
        @assert str !== nothing
        self = new(Dict(), obj, str, side, offset)
        kw_init(self, args...; kvs...)
        self
    end
end

_kw_rename(::BoxLabel) = Dict(
    :face => :fontface,
    :size => :fontsize,
)

function make(self::BoxLabel, context)
    bb = boundingbox(self.obj, context.paintc)
    offset = _size_relative(self.offset, context.dev_bbox)
    if self.side == "top"
        p = upperleft(bb)
        q = upperright(bb)
    elseif self.side == "bottom"
        p = lowerleft(bb)
        q = lowerright(bb)
        offset = -offset
    elseif self.side == "left"
        p = lowerleft(bb)
        q = upperleft(bb)
    elseif self.side == "right"
        p = upperright(bb)
        q = lowerright(bb)
    end

    midpoint = 0.5*(p + q)
    direction = q - p
    direction /= norm(direction)
    angle = atan(direction.y, direction.x)
    direction = rotate(direction, pi/2)
    pos = midpoint + offset*direction

    valign = (offset > 0) ? "bottom" : "top"
    tp = TextPainter(pos, self.str; angle=rad2deg(angle), valign=valign)
    GroupPainter(getattr(self,:style), tp)
end

mutable struct Stems <: LineComponent
    attr::PlotAttributes
    x
    y

    function Stems(x, y, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self.y = y
        self
    end
end

function limits(self::Stems, window::BoundingBox)
    return bounds_within(self.x, self.y, window) +
           bounds_within(self.x, zeros(length(self.y)), window)
end

function make(self::Stems, context::PlotContext)
    gp = GroupPainter(getattr(self,:style))
    n = min(length(self.x),length(self.y))
    for i = 1:n
        a = project(context.geom, Point(self.x[i],self.y[i]))
        b = project(context.geom, Point(self.x[i],0.))
        push!(gp, LinePainter(a,b))
    end
    gp
end

# LabelComponent --------------------------------------------------------------

abstract type LabelComponent <: PlotComponent end

_kw_rename(::LabelComponent) = Dict(
    :face      => :fontface,
    :size      => :fontsize,
    :angle     => :textangle,
    :halign    => :texthalign,
    :valign    => :textvalign,
)

#function limits(self::LabelComponent)
#    return BoundingBox()
#end

mutable struct DataLabel <: LabelComponent
    attr::PlotAttributes
    pos::Point
    str::AbstractString

    function DataLabel(x, y, str, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.pos = Point(x, y)
        self.str = str
        self
    end
end

function make(self::DataLabel, context)
    xy = project(context.geom, self.pos)
    # XXX:fix angle,halign,valign so that default values are not send forward
    textangle=kw_get(self,:textangle,0.0)
    texthalign=kw_get(self,:texthalign,"center")
    textvalign=kw_get(self,:textvalign,"center")

    tp = TextPainter(xy, self.str; angle=textangle,halign=texthalign,valign=textvalign)
    GroupPainter(getattr(self,:style), tp)
end

mutable struct PlotLabel <: LabelComponent
    attr::PlotAttributes
    pos::Point
    str::AbstractString

    function PlotLabel(x, y, str, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.pos = Point(x, y)
        self.str = str
        self
    end
end

function make(self::PlotLabel, context)
    pos = project(context.plot_geom, self.pos)
    # XXX:fix angle,halign,valign so that default values are not send forward
    textangle=kw_get(self,:textangle,0.0)
    texthalign=kw_get(self,:texthalign,"center")
    textvalign=kw_get(self,:textvalign,"center")

    tp = TextPainter(pos, self.str; angle=textangle,halign=texthalign,valign=textvalign)
    GroupPainter(getattr(self,:style), tp)
end

# LabelsComponent ------------------------------------------------------------
#
#type Labels <: _PlotComponent
#
#    function __init__(self, x, y, labels, args...)
#        _PlotComponent.__init__(self)
#        self.iniattr("LabelsComponent")
#        self.iniattr("Labels")
#        kw_init(self, args...; kvs...)
#        self.x = x
#        self.y = y
#        self.labels = labels
#    end
#end
#
#_kw_rename(::Labels) = [
#    "face"      => "fontface",
#    "size"      => "fontsize",
#]
#
#function limits(self::Labels)
#    p = min(self.x), min(self.y)
#    q = max(self.x), max(self.y)
#    return BoundingBox(p, q)
#end
#
#function make(self::Labels, context::PlotContext)
#    x, y = project(context.geom, self.x, self.y)
#    l = LabelsPainter(zip(x,y), self.labels, self.kw_style)
#    add(self, l)
#end

# FillComponent -------------------------------------------------------------

abstract type FillComponent <: PlotComponent end

function make_key(self::FillComponent, bbox::BoundingBox)
    p = lowerleft(bbox)
    q = upperright(bbox)
    return GroupPainter(getattr(self,:style), BoxPainter(p,q))
end

kw_defaults(::FillComponent) = Dict(
    :color => config_value("FillComponent","fillcolor"),
    :fillkind => config_value("FillComponent","fillkind"),
)

mutable struct FillAbove <: FillComponent
    attr::PlotAttributes
    x
    y

    function FillAbove(x, y, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self.y = y
        self
    end
end

limits(self::FillAbove, window::BoundingBox) = bounds_within(self.x, self.y, window)

function make(self::FillAbove, context)
    coords = map((a,b)->project(context.geom,Point(a,b)), self.x, self.y)
    max_y = ymax(context.data_bbox)
    push!(coords, project(context.geom, Point(self.x[end], max_y)))
    push!(coords, project(context.geom, Point(self.x[1], max_y)))
    GroupPainter(getattr(self,:style), PolygonPainter(coords))
end

mutable struct FillBelow <: FillComponent
    attr::PlotAttributes
    x
    y

    function FillBelow(x, y, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self.y = y
        self
    end
end

limits(self::FillBelow, window::BoundingBox) = bounds_within(self.x, self.y, window)

function make(self::FillBelow, context)
    coords = map((a,b)->project(context.geom,Point(a,b)), self.x, self.y)
    min_y = ymin(context.data_bbox)
    push!(coords, project(context.geom, Point(self.x[end], min_y)))
    push!(coords, project(context.geom, Point(self.x[1], min_y)))
    GroupPainter(getattr(self,:style), PolygonPainter(coords))
end

mutable struct FillBetween <: FillComponent
    attr::PlotAttributes
    x1
    y1
    x2
    y2

    function FillBetween(x1, y1, x2, y2, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
        self
    end
end

limits(self::FillBetween, window::BoundingBox) =
    bounds_within(self.x1, self.y1, window) +
    bounds_within(self.x2, self.y2, window)

function make(self::FillBetween, context)
    x = [self.x1; reverse(self.x2)]
    y = [self.y1; reverse(self.y2)]
    coords = map((a,b) -> project(context.geom,Point(a,b)), x, y)
    GroupPainter(getattr(self,:style), PolygonPainter(coords))
end

# ImageComponent -------------------------------------------------------------

abstract type ImageComponent <: PlotComponent end

mutable struct Image <: ImageComponent
    attr::PlotAttributes
    img
    x
    y
    w
    h

    function Image(xrange, yrange, img, args...; kvs...)
        x = minimum(xrange)
        y = minimum(yrange)
        w = abs(xrange[2] - xrange[1])
        h = abs(yrange[2] - yrange[1])
        self = new(Dict(), img, x, y, w, h)
        iniattr(self)
        kw_init(self, args...; kvs...)
        self
    end
end

limits(self::Image, window::BoundingBox) =
    bounds_within([self.x, self.x+self.w], [self.y, self.y+self.h], window)

function make(self::Image, context)
    a = project(context.geom, Point(self.x, self.y))
    b = project(context.geom, Point(self.x+self.w, self.y+self.h))
    bbox = BoundingBox(a, b)
    GroupPainter(getattr(self,:style), ImagePainter(self.img, bbox))
end

# FramedComponent ---------------------------------------------------------------

abstract type FramedComponent <: PlotComponent end

function make_key(self::FramedComponent, bbox::BoundingBox)
    p = lowerleft(bbox)
    q = upperright(bbox)
    GroupPainter(getattr(self, :style), BoxPainter(p, q))
end

_kw_rename(::FramedComponent) = Dict(:color => :fillcolor)

# FramedBar ------------------------------------------------------------------

mutable struct FramedBar <: FramedComponent
    attr::PlotAttributes
    g::AbstractVector
    h::AbstractVecOrMat

    function FramedBar(g, h, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.g = map(string, g)
        self.h = h
        self
    end
end

_kw_rename(::FramedBar) = Dict(
    :color => :fillcolor,
    :width => :barwidth,
)

function limits(self::FramedBar, window::BoundingBox)
    x = [1, length(self.g)] +
        getattr(self, "barwidth") * [-.5, .5] +
        getattr(self, "offset")
    y = [extrema(self.h)...]
    !getattr(self, "vertical") && ((x, y) = (y, x))
    bounds_within(x, y, window)
end

function make(self::FramedBar, context)
    style = getattr(self, :style)
    objs = GroupPainter(style)
    baseline = getattr(self, "baseline")
    x = collect(1:length(self.h)) .+ getattr(self, "barwidth") * [-.5 .5] + getattr(self, "offset")
    y = [baseline .* ones(length(self.h)) self.h]
    if !getattr(self, "vertical")
        x, y = y, x
        bl = LineX(baseline)
    else
        bl = LineY(baseline)
    end
    for i = 1:length(self.h)
        corners = [project(context.geom, Point(x[i,c], y[i,c])) for c in (1,2)]
        push!(objs, BoxPainter(corners...))
    end
    if haskey(style, :draw_baseline) && style[:draw_baseline]
        objs = GroupPainter(objs, make(bl, context))
    end
    objs
end

# SymbolDataComponent --------------------------------------------------------

abstract type SymbolDataComponent <: PlotComponent end

_kw_rename(::SymbolDataComponent) = Dict(
    :kind => :symbolkind,
    :size => :symbolsize,
    # deprecated
    :type => :symbolkind,
    :symboltype => :symbolkind,
)

function make_key(self::SymbolDataComponent, bbox::BoundingBox)
    pos = center(bbox)
    return GroupPainter(getattr(self,:style), SymbolPainter(pos))
end

mutable struct Points <: SymbolDataComponent
    attr::PlotAttributes
    x
    y

    function Points(x, y, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self.y = y
        self
    end
end

kw_defaults(::SymbolDataComponent) = Dict(
    :symbolkind => config_value("Points","symbolkind"),
    :symbolsize => config_value("Points","symbolsize"),
)

limits(self::SymbolDataComponent, window::BoundingBox) =
    bounds_within(self.x, self.y, window)

function make(self::SymbolDataComponent, context::PlotContext)
    x, y = project(context.geom, self.x, self.y)
    GroupPainter(getattr(self,:style), SymbolsPainter(x,y))
end

function Points(x::Real, y::Real, args...)
    return Points([x], [y], args...)
end

mutable struct ColoredPoints <: SymbolDataComponent
    attr::PlotAttributes
    x
    y
    s
    c

    function ColoredPoints(x, y, s, c, args...; kvs...)
        self = new(Dict())
        iniattr(self)
        kw_init(self, args...; kvs...)
        self.x = x
        self.y = y
        self.s = s
        self.c = c
        self
    end
end

kw_defaults(::ColoredPoints) = Dict(
    :symbolkind => config_value("Points","symbolkind"),
    :symbolsize => config_value("Points","symbolsize"),
)

limits(self::ColoredPoints, window::BoundingBox) =
    bounds_within(self.x, self.y, window)

function make(self::ColoredPoints, context::PlotContext)
    x, y = project(context.geom, self.x, self.y)
    GroupPainter(getattr(self,:style), ColoredSymbolsPainter(x, y, self.s, self.c))
end

function ColoredPoints(x::Real, y::Real, args...)
    return ColoredPoints([x], [y], args...)
end

# PlotComponent ---------------------------------------------------------------

function show(io::IO, self::PlotComponent)
    print(io, typeof(self), "(...)")
end

function limits(self::PlotComponent, window::BoundingBox)
    return BoundingBox()
end

function make_key(self::PlotComponent, bbox::BoundingBox)
end

function boundingbox(self::PlotComponent, context::PlotContext)
    objs = make(self, context)
    boundingbox(objs, context.paintc)
end

function render(self::PlotComponent, context)
    objs = make(self, context)
    paint(objs, context.paintc)
end

# HasAttr ---------------------------------------------------------------------

_attr_map(::HasAttr) = Dict()

function hasattr(self::HasAttr, name::Symbol)
    key = get(_attr_map(self), name, name)
    return haskey(self.attr, key)
end

function getattr(self::HasAttr, name::Symbol)
    key = get(_attr_map(self), name, name)
    return self.attr[key]
end

function getattr(self::HasAttr, name::Symbol, notfound)
    key = get(_attr_map(self), name, name)
    return haskey(self.attr,key) ? self.attr[key] : notfound
end

function setattr(self::HasAttr, name::Symbol, value)
    key = get(_attr_map(self), name, name)
    self.attr[key] = value
end

hasattr(self::HasAttr, name::AbstractString) = hasattr(self, Symbol(name))
getattr(self::HasAttr, name::AbstractString) = getattr(self, Symbol(name))
getattr(self::HasAttr, name::AbstractString, notfound) = getattr(self, Symbol(name), notfound)
setattr(self::HasAttr, name::AbstractString, value) = setattr(self, Symbol(name), value)
setattr(self::HasAttr; kvs...) = (for (k,v) in kvs; setattr(self, k, v); end)

function iniattr(self::HasAttr, args...; kvs...)
    types = Any[typeof(self)]
    while supertype(types[end]) != Any
        push!(types, supertype(types[end]))
    end
    for t in reverse(types)
        name = last(split(string(t), '.'))
        for (k,v) in config_options(name)
            setattr(self, k, v)
        end
    end
    for (k,v) in args2dict(args...)
        setattr(self, k, v)
    end
    for (k,v) in kvs
        setattr(self, k, v)
    end
end

# HasStyle ---------------------------------------------------------------

kw_defaults(x) = Dict{Symbol,Any}()
_kw_rename(x) = Dict{Symbol,Symbol}()

function kw_init(self::HasStyle, args...; kvs...)
    # jeez, what a mess...
    sty = Dict{Symbol,Any}()
    for (k,v) in kw_defaults(self)
        sty[k] = v
    end
    if hasattr(self, :kw_defaults)
        for (k,v) in getattr(self, :kw_defaults)
            sty[k] = v
        end
    end
    setattr(self, :style, sty)
    for (key, value) in args2dict(args...)
        kw_set(self, key, value)
    end
    for (k,v) in kvs
        kw_set(self, k, v)
    end
end

function kw_set(self::HasStyle, name, value)
    #if !hasattr(self, "style")
    #    kw_init(self)
    #end
    key = get(_kw_rename(self), name, name)
    getattr(self, :style)[key] = value
end

function style(self::HasStyle, args...; kvs...)
    for (key,val) in args2dict(args...)
        kw_set(self, key, val)
    end
    for (key,val) in kvs
        kw_set(self, key, val)
    end
end

kw_get(self::HasStyle, key) = kw_get(self, key, nothing)
function kw_get(self::HasStyle, key, notfound)
    return get(getattr(self,:style), key, notfound)
end

include("plot.jl")
include("plot_interfaces.jl")
include("contour.jl")

############################################################################

_ijulia_width = 450
_ijulia_height = 300
function set_default_plot_size(width::Int, height::Int)
    global _ijulia_width
    global _ijulia_height
    _ijulia_width = width
    _ijulia_height = height
end

show(io::IO, ::MIME"image/png", p::PlotContainer) =
    savepng(p, io, _ijulia_width, _ijulia_height)

if isdefined(Main, :IJulia)
    output_surface = :none
else
    output_surface = Winston.config_value("default","output_surface")
    output_surface = Symbol(lowercase(get(ENV, "WINSTON_OUTPUT", output_surface)))
end

mutable struct Figure
    window
    plot::PlotContainer
end

mutable struct WinstonDisplay <: AbstractDisplay
    figs::Dict{Int,Figure}
    fig_order::Vector{Int}
    current_fig::Int
    next_fig::Int
    WinstonDisplay() = new(Dict{Int,Figure}(), Int[], 0, 1)
end

function addfig(d::WinstonDisplay, i::Int, fig::Figure)
    @assert !haskey(d.figs,i)
    d.figs[i] = fig
    push!(d.fig_order, i)
    while haskey(d.figs,d.next_fig)
        d.next_fig += 1
    end
    d.current_fig = i
end

hasfig(d::WinstonDisplay, i::Int) = haskey(d.figs,i)

function switchfig(d::WinstonDisplay, i::Int)
    haskey(d.figs,i) && (d.current_fig = i)
end

function getfig(d::WinstonDisplay, i::Int)
    haskey(d.figs,i) ? d.figs[i] : error("no figure with index $i")
end

function curfig(d::WinstonDisplay)
    d.figs[d.current_fig]
end

nextfig(d::WinstonDisplay) = d.next_fig

function dropfig(d::WinstonDisplay, i::Int)
    haskey(d.figs,i) || return
    delete!(d.figs, i)
    splice!(d.fig_order, something(findfirst(isequal(i), d.fig_order), 0) )
    d.next_fig = min(d.next_fig, i)
    d.current_fig = isempty(d.fig_order) ? 0 : d.fig_order[end]
end

global _display = WinstonDisplay()
global _pwinston = FramedPlot()

function figure(;name::AbstractString="Figure $(nextfig(_display))",
                 width::Integer=Winston.config_value("window","width"),
                 height::Integer=Winston.config_value("window","height"))
    i = nextfig(_display)
    w = window(name, width, height, (x...)->dropfig(_display,i))
    isempty(_display.figs) || (global _pwinston = FramedPlot())
    addfig(_display, i, Figure(w,_pwinston))
end

function figure(i::Integer)
    switchfig(_display, i)
    fig = curfig(_display)
    global _pwinston = fig.plot
    display(_display, fig)
    nothing
end

gcf() = _display.current_fig
closefig() = closefig(_display.current_fig)

include("gtk.jl")
global window = gtkwindow
closefig(i::Integer) = gtkdestroy(getfig(_display,i).window)
closeall() = (map(closefig, keys(_display.figs)); nothing)

display(d::WinstonDisplay, f::Figure) = display(f.window, f.plot)
function display(d::WinstonDisplay, p::PlotContainer)
    isempty(d.figs) && figure()
    f = curfig(d)
    f.plot = p
    display(d, f)
end
pushdisplay(_display)
display(::REPL.REPLDisplay, ::MIME"text/plain", p::PlotContainer) = display(_display, p)

end # module


text:
@testset "fsm_retx_relaxed.jl" begin
    DEFAULT_CAPACITY = 65000
    TIMEOUT_DFLT = 1000
    NREPS = 64
    MAX_PAYLOAD_SIZE = 1460
    MAX_RETX_ATTEMPTS = 8
    @testset "single segment re-transmit" begin
        tx_ackno = WrappingInt32(rand(UInt32))
        conn = fsm_in_established(tx_ackno - 1, tx_ackno - 1; cap=DEFAULT_CAPACITY)
        data = "asdf"
        write!(conn, data)
        tick!(conn, 1)
        expect_one_seg(conn; ack=true, data, payload_size=length(data))
        tick!(conn, TIMEOUT_DFLT - 2)

        expect_no_seg(conn)
        tick!(conn, 2)
        expect_one_seg(conn; ack=true, data, payload_size=length(data))

        tick!(conn, 10TIMEOUT_DFLT + 100)
        expect_one_seg(conn; ack=true, data, payload_size=length(data))

        for i in 2:MAX_RETX_ATTEMPTS-1
            tick!(conn, (TIMEOUT_DFLT << i) - i)
            expect_no_seg(conn)
            tick!(conn, i)
            expect_one_seg(conn; ack=true, data, payload_size=length(data))
        end
        expect_state(conn, JLSponge.ESTABLISHED)
        tick!(conn, (TIMEOUT_DFLT << MAX_RETX_ATTEMPTS) + 1)
        expect_state(conn, JLSponge.RESET)
        expect_one_seg(conn; rst=true)
    end
end

max_stars_repo_name: AquaIndigo/JLSponge

max_issues_repo_name: AquaIndigo/JLSponge

max_forks_repo_name: AquaIndigo/JLSponge