Plots.jl/src/axes.jl

461 lines
14 KiB
Julia

# xaxis(args...; kw...) = Axis(:x, args...; kw...)
# yaxis(args...; kw...) = Axis(:y, args...; kw...)
# zaxis(args...; kw...) = Axis(:z, args...; kw...)
# -------------------------------------------------------------------------
function Axis(sp::Subplot, letter::Symbol, args...; kw...)
# init with values from _plot_defaults
d = KW(
:letter => letter,
# :extrema => (Inf, -Inf),
:extrema => Extrema(),
:discrete_map => Dict(), # map discrete values to discrete indices
:continuous_values => zeros(0),
:use_minor => false,
:show => true, # show or hide the axis? (useful for linked subplots)
)
# get defaults from letter version, unless match
for (k,v) in _axis_defaults
lk = Symbol(letter, k)
lv = _axis_defaults_byletter[lk]
d[k] = (lv == :match ? v : lv)
end
# merge!(d, _axis_defaults)
d[:discrete_values] = []
# update the defaults
update!(Axis(sp, d), args...; kw...)
end
function get_axis(sp::Subplot, letter::Symbol)
axissym = Symbol(letter, :axis)
if haskey(sp.attr, axissym)
sp.attr[axissym]
else
sp.attr[axissym] = Axis(sp, letter)
end::Axis
end
function process_axis_arg!(d::KW, arg, letter = "")
T = typeof(arg)
arg = get(_scaleAliases, arg, arg)
if typeof(arg) <: Font
d[Symbol(letter,:tickfont)] = arg
d[Symbol(letter,:guidefont)] = arg
elseif arg in _allScales
d[Symbol(letter,:scale)] = arg
elseif arg in (:flip, :invert, :inverted)
d[Symbol(letter,:flip)] = true
elseif T <: AbstractString
d[Symbol(letter,:guide)] = arg
# xlims/ylims
elseif (T <: Tuple || T <: AVec) && length(arg) == 2
sym = typeof(arg[1]) <: Number ? :lims : :ticks
d[Symbol(letter,sym)] = arg
# xticks/yticks
elseif T <: AVec
d[Symbol(letter,:ticks)] = arg
elseif arg == nothing
d[Symbol(letter,:ticks)] = []
elseif typeof(arg) <: Number
d[Symbol(letter,:rotation)] = arg
else
warn("Skipped $(letter)axis arg $arg")
end
end
# update an Axis object with magic args and keywords
function update!(axis::Axis, args...; kw...)
# first process args
d = axis.d
for arg in args
process_axis_arg!(d, arg)
end
# then override for any keywords... only those keywords that already exists in d
for (k,v) in kw
if haskey(d, k)
if k == :discrete_values
# add these discrete values to the axis
for vi in v
discrete_value!(axis, vi)
end
else
d[k] = v
end
end
end
# replace scale aliases
if haskey(_scaleAliases, d[:scale])
d[:scale] = _scaleAliases[d[:scale]]
end
axis
end
# -------------------------------------------------------------------------
Base.show(io::IO, axis::Axis) = dumpdict(axis.d, "Axis", true)
# Base.getindex(axis::Axis, k::Symbol) = getindex(axis.d, k)
Base.setindex!(axis::Axis, v, ks::Symbol...) = setindex!(axis.d, v, ks...)
Base.haskey(axis::Axis, k::Symbol) = haskey(axis.d, k)
Base.extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax))
const _scale_funcs = Dict{Symbol,Function}(
:log10 => log10,
:log2 => log2,
:ln => log,
)
const _inv_scale_funcs = Dict{Symbol,Function}(
:log10 => exp10,
:log2 => exp2,
:ln => exp,
)
const _label_func = Dict{Symbol,Function}(
:log10 => x -> "10^$x",
:log2 => x -> "2^$x",
:ln => x -> "e^$x",
# :log2 => exp2,
# :ln => exp,
)
scalefunc(scale::Symbol) = x -> get(_scale_funcs, scale, identity)(Float64(x))
invscalefunc(scale::Symbol) = x -> get(_inv_scale_funcs, scale, identity)(Float64(x))
labelfunc(scale::Symbol) = get(_label_func, scale, string)
function optimal_ticks_and_labels(axis::Axis, ticks = nothing)
lims = axis_limits(axis)
# scale the limits
scale = axis[:scale]
scaled_lims = map(scalefunc(scale), lims)
# @show lims scaled_lims
# get a list of well-laid-out ticks
cv = if ticks == nothing
optimize_ticks(scaled_lims...,
k_min = 5, # minimum number of ticks
k_max = 8, # maximum number of ticks
span_buffer = 0.0 # padding buffer in case nice ticks are closeby
)[1]
else
ticks
end
# # expand to ensure we see all the ticks
# expand_extrema!(axis, cv)
# rescale and return values and labels
# @show cv
ticklabels = map(labelfunc(scale), Showoff.showoff(cv, :plain))
tickvals = map(invscalefunc(scale), cv)
# @show tickvals ticklabels
# ticklabels = Showoff.showoff(tickvals, scale == :log10 ? :scientific : :auto)
tickvals, ticklabels
# basestr = scale == :log10 ? "10^" : scale == :log2 ? "2^" : scale == :ln ? "e^" : ""
# tickvals, ["$basestr$cvi" for cvi in cv]
end
# return (continuous_values, discrete_values) for the ticks on this axis
function get_ticks(axis::Axis)
ticks = axis[:ticks]
ticks in (nothing, false) && return nothing
dvals = axis[:discrete_values]
cv, dv = if !isempty(dvals) && ticks == :auto
# discrete ticks...
axis[:continuous_values], dvals
elseif ticks == :auto
# compute optimal ticks and labels
optimal_ticks_and_labels(axis)
elseif typeof(ticks) <: AVec
# override ticks, but get the labels
optimal_ticks_and_labels(axis, ticks)
elseif typeof(ticks) <: NTuple{2}
# assuming we're passed (ticks, labels)
ticks
else
error("Unknown ticks type in get_ticks: $(typeof(ticks))")
end
# @show ticks dvals cv dv
# TODO: better/smarter cutoff values for sampling ticks
if length(cv) > 30
rng = Int[round(Int,i) for i in linspace(1, length(cv), 15)]
cv[rng], dv[rng]
else
cv, dv
end
end
# -------------------------------------------------------------------------
function expand_extrema!(ex::Extrema, v::Number)
ex.emin = min(v, ex.emin)
ex.emax = max(v, ex.emax)
ex
end
function expand_extrema!(axis::Axis, v::Number)
expand_extrema!(axis[:extrema], v)
end
function expand_extrema!{MIN<:Number,MAX<:Number}(axis::Axis, v::Tuple{MIN,MAX})
ex = axis[:extrema]
ex.emin = min(v[1], ex.emin)
ex.emax = max(v[2], ex.emax)
ex
end
function expand_extrema!{N<:Number}(axis::Axis, v::AVec{N})
ex = axis[:extrema]
for vi in v
expand_extrema!(ex, vi)
end
ex
end
function expand_extrema!(sp::Subplot, d::KW)
# first expand for the data
for letter in (:x, :y, :z)
data = d[letter]
axis = sp.attr[Symbol(letter, "axis")]
if eltype(data) <: Number || (isa(data, Surface) && all(di -> isa(di, Number), data.surf))
if !(eltype(data) <: Number)
# huh... must have been a mis-typed surface? lets swap it out
data = d[letter] = Surface(Matrix{Float64}(data.surf))
end
expand_extrema!(axis, data)
elseif data != nothing
# TODO: need more here... gotta track the discrete reference value
# as well as any coord offset (think of boxplot shape coords... they all
# correspond to the same x-value)
d[letter], d[Symbol(letter,"_discrete_indices")] = discrete_value!(axis, data)
expand_extrema!(axis, d[letter])
end
end
# # expand for fillrange/bar_width
# fillaxis, baraxis = sp.attr[:yaxis], sp.attr[:xaxis]
# if isvertical(d)
# fillaxis, baraxis = baraxis, fillaxis
# end
# expand for fillrange
vert = isvertical(d)
fr = d[:fillrange]
if fr == nothing && d[:seriestype] == :bar
fr = 0.0
end
if fr != nothing
axis = sp.attr[vert ? :yaxis : :xaxis]
if typeof(fr) <: Tuple
for fri in fr
expand_extrema!(axis, fri)
end
else
expand_extrema!(axis, fr)
end
end
# expand for bar_width
if d[:seriestype] == :bar
dsym = vert ? :x : :y
data = d[dsym]
bw = d[:bar_width]
if bw == nothing
bw = d[:bar_width] = mean(diff(data))
end
# @show data bw
axis = sp.attr[Symbol(dsym, :axis)]
expand_extrema!(axis, maximum(data) + 0.5maximum(bw))
expand_extrema!(axis, minimum(data) - 0.5minimum(bw))
end
end
function expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax)
expand_extrema!(sp[:xaxis], (xmin, xmax))
expand_extrema!(sp[:yaxis], (ymin, ymax))
end
# -------------------------------------------------------------------------
# push the limits out slightly
function widen(lmin, lmax)
span = lmax - lmin
# eps = max(1e-16, min(1e-2span, 1e-10))
eps = max(1e-16, 0.03span)
lmin-eps, lmax+eps
end
# figure out if widening is a good idea. if there's a scale set it's too tricky,
# so lazy out and don't widen
function default_should_widen(axis::Axis)
should_widen = false
if axis[:scale] == :identity
for series in series_list(axis.sp)
if series.d[:seriestype] in (:scatter,) || series.d[:markershape] != :none
should_widen = true
end
end
end
should_widen
end
# using the axis extrema and limit overrides, return the min/max value for this axis
function axis_limits(axis::Axis, should_widen::Bool = default_should_widen(axis))
ex = axis[:extrema]
amin, amax = ex.emin, ex.emax
lims = axis[:lims]
if (isa(lims, Tuple) || isa(lims, AVec)) && length(lims) == 2
if isfinite(lims[1])
amin = lims[1]
end
if isfinite(lims[2])
amax = lims[2]
end
end
if amax <= amin && isfinite(amin)
amax = amin + 1.0
end
if should_widen
widen(amin, amax)
else
amin, amax
end
end
# -------------------------------------------------------------------------
# these methods track the discrete (categorical) values which correspond to axis continuous values (cv)
# whenever we have discrete values, we automatically set the ticks to match.
# we return (continuous_value, discrete_index)
function discrete_value!(axis::Axis, dv)
cv_idx = get(axis[:discrete_map], dv, -1)
# @show axis[:discrete_map], axis[:discrete_values], dv
if cv_idx == -1
ex = axis[:extrema]
cv = max(0.5, ex.emax + 1.0)
expand_extrema!(axis, cv)
push!(axis[:discrete_values], dv)
push!(axis[:continuous_values], cv)
cv_idx = length(axis[:discrete_values])
axis[:discrete_map][dv] = cv_idx
cv, cv_idx
else
cv = axis[:continuous_values][cv_idx]
cv, cv_idx
end
end
# continuous value... just pass back with axis negative index
function discrete_value!(axis::Axis, cv::Number)
cv, -1
end
# add the discrete value for each item. return the continuous values and the indices
function discrete_value!(axis::Axis, v::AVec)
n = length(v)
cvec = zeros(n)
discrete_indices = zeros(Int, n)
for i=1:n
cvec[i], discrete_indices[i] = discrete_value!(axis, v[i])
end
cvec, discrete_indices
end
# add the discrete value for each item. return the continuous values and the indices
function discrete_value!(axis::Axis, v::AMat)
n,m = size(v)
cmat = zeros(n,m)
discrete_indices = zeros(Int, n, m)
for i=1:n, j=1:m
cmat[i,j], discrete_indices[i,j] = discrete_value!(axis, v[i,j])
end
cmat, discrete_indices
end
function discrete_value!(axis::Axis, v::Surface)
map(Surface, discrete_value!(axis, v.surf))
end
# -------------------------------------------------------------------------
function pie_labels(sp::Subplot, series::Series)
d = series.d
if haskey(d,:x_discrete_indices)
dvals = sp.attr[:xaxis].d[:discrete_values]
[dvals[idx] for idx in d[:x_discrete_indices]]
else
d[:x]
end
end
# -------------------------------------------------------------------------
# compute the line segments which should be drawn for this axis
function axis_drawing_info(sp::Subplot)
xaxis, yaxis = sp[:xaxis], sp[:yaxis]
xmin, xmax = axis_limits(xaxis)
ymin, ymax = axis_limits(yaxis)
xticks = get_ticks(xaxis)
yticks = get_ticks(yaxis)
spine_segs = Segments(2)
grid_segs = Segments(2)
if !(xaxis[:ticks] in (nothing, false))
f = scalefunc(yaxis[:scale])
invf = invscalefunc(yaxis[:scale])
t1 = invf(f(ymin) + 0.015*(f(ymax)-f(ymin)))
t2 = invf(f(ymax) - 0.015*(f(ymax)-f(ymin)))
push!(spine_segs, (xmin,ymin), (xmax,ymin)) # bottom spine
push!(spine_segs, (xmin,ymax), (xmax,ymax)) # top spine
for xtick in xticks[1]
push!(spine_segs, (xtick, ymin), (xtick, t1)) # bottom tick
push!(grid_segs, (xtick, t1), (xtick, t2)) # vertical grid
push!(spine_segs, (xtick, ymax), (xtick, t2)) # top tick
end
end
if !(yaxis[:ticks] in (nothing, false))
f = scalefunc(xaxis[:scale])
invf = invscalefunc(xaxis[:scale])
t1 = invf(f(xmin) + 0.015*(f(xmax)-f(xmin)))
t2 = invf(f(xmax) - 0.015*(f(xmax)-f(xmin)))
push!(spine_segs, (xmin,ymin), (xmin,ymax)) # left spine
push!(spine_segs, (xmax,ymin), (xmax,ymax)) # right spine
for ytick in yticks[1]
push!(spine_segs, (xmin, ytick), (t1, ytick)) # left tick
push!(grid_segs, (t1, ytick), (t2, ytick)) # horizontal grid
push!(spine_segs, (xmax, ytick), (t2, ytick)) # right tick
end
end
xticks, yticks, spine_segs, grid_segs
end