Skip to content

Commit

Permalink
Merge pull request #115 from t-bltg/log
Browse files Browse the repository at this point in the history
Rework ticks computation for log scales
  • Loading branch information
t-bltg authored Jul 5, 2021
2 parents 4007370 + 97aef62 commit 1354566
Showing 1 changed file with 48 additions and 38 deletions.
86 changes: 48 additions & 38 deletions src/ticks.jl
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
const _logScales = [:ln, :log2, :log10]
const _logScaleBases = Dict(:ln => ℯ, :log2 => 2.0, :log10 => 10.0)

# NOTE: This file was moved from Gadfly.jl, and the original author is Daniel Jones (@dcjones)

# Find the smallest order of magnitude that is larger than xspan This is a
# little opaque because I want to avoid assuming the log function is defined
# over typeof(xspan)
function bounding_order_of_magnitude(xspan::DT) where DT
function bounding_order_of_magnitude(xspan::DT, base) where DT
one_dt = convert(DT, one(DT))

a = 1
step = 1
while xspan < 10.0^a * one_dt
while xspan < base^a * one_dt
a -= step
end

b = 1
step = 1
while xspan > 10.0^b * one_dt
while xspan > base^b * one_dt
b += step
end

while a + 1 < b
c = div(a + b, 2)
if xspan < 10.0^c * one_dt
if xspan < base^c * one_dt
b = c
else
a = c
Expand Down Expand Up @@ -131,78 +133,88 @@ function optimize_ticks(x_min::T, x_max::T; extend_ticks::Bool=false,
k_min::Int=2, k_max::Int=10, k_ideal::Int=5,
granularity_weight::Float64=1/4, simplicity_weight::Float64=1/6,
coverage_weight::Float64=1/3, niceness_weight::Float64=1/4,
strict_span=true, span_buffer = nothing) where T

strict_span=true, span_buffer=nothing, scale=nothing) where T
Qv = [(Float64(q[1]), Float64(q[2])) for q in Q]
optimize_ticks_typed(x_min, x_max, extend_ticks, Qv, k_min, k_max, k_ideal,
granularity_weight, simplicity_weight,
coverage_weight, niceness_weight, strict_span, span_buffer)
coverage_weight, niceness_weight, strict_span, span_buffer, scale)
end

function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks,
Q::Vector{Tuple{Float64,Float64}}, k_min,
k_max, k_ideal,
Q::Vector{Tuple{Float64,Float64}}, k_min, k_max, k_ideal,
granularity_weight::Float64, simplicity_weight::Float64,
coverage_weight::Float64, niceness_weight::Float64,
strict_span, span_buffer) where T
strict_span, span_buffer, scale) where T
one_t = convert(T, one(T))
if x_max - x_min < eps()*one_t
R = typeof(1.0 * one_t)
if x_max - x_min < eps() * one_t
R = typeof(1. * one_t)
return R[x_min], x_min - one_t, x_min + one_t
end

n = length(Q)
is_log_scale = scale _logScales
base = get(_logScaleBases, scale, 10.)

# generalizing "order of magnitude"
xspan = x_max - x_min
z = bounding_order_of_magnitude(xspan)
z = bounding_order_of_magnitude(xspan, base)

# find required significant digits for ticks with q*10^z spacing,
# find required significant digits for ticks with q*base^z spacing,
# for q values specified in Q
x_digits = bounding_order_of_magnitude(max(abs(x_min), abs(x_max)))
x_digits = bounding_order_of_magnitude(max(abs(x_min), abs(x_max)), base)
q_extra_digits = maximum(postdecimal_digits(q[1]) for q in Q)
sigdigits(z) = max(1, x_digits - z + q_extra_digits)

ib = Int(base)
round_base = (
isinteger(base) ?
v -> round(v, sigdigits=sigdigits(z), base=ib) :
v -> round(v, sigdigits=sigdigits(z))
)

high_score = -Inf
S_best = Array{typeof(1.0 * one_t)}(undef, 1)
S_best = Array{typeof(1. * one_t)}(undef, 1)
viewmin_best, viewmax_best = x_min, x_max


# we preallocate arrays that hold all required S arrays for every given
# the k parameter, so we don't have to create them again and again, which
# saves many allocations
prealloc_Ss = if extend_ticks
[Array{typeof(1.0 * one_t)}(undef, Int(3 * k)) for k in k_min:2k_max]
[Array{typeof(1. * one_t)}(undef, Int(3k)) for k in k_min:2k_max]
else
[Array{typeof(1.0 * one_t)}(undef, k) for k in k_min:2k_max]
[Array{typeof(1. * one_t)}(undef, k) for k in k_min:2k_max]
end

while 2k_max * 10.0^(z+1) * one_t > xspan
while 2k_max * base^(z + 1) * one_t > xspan
for (ik, k) in enumerate(k_min:2k_max)
for (q, qscore) in Q
tickspan = q * 10.0^z * one_t
tickspan = q * base^z * one_t
span = (k - 1) * tickspan
if span < xspan
continue
end

stp = q*10.0^z
stp = q*base^z
if stp < eps()
continue
end
if is_log_scale && !isinteger(stp)
continue # prefer integer exponents for log scales
end
r = ceil(Int64, (x_max - span) / (stp * one_t))

while r*stp * one_t <= x_min
# Filter or expand ticks
if extend_ticks
S = prealloc_Ss[ik]
for i in 0:(3*k - 1)
for i in 0:(3k - 1)
S[i+1] = (r + i - k) * tickspan
end
# round only those values that end up as viewmin and viewmax
# to save computation time
S[k + 1] = round(S[k + 1], sigdigits = sigdigits(z))
S[2 * k] = round(S[2 * k], sigdigits = sigdigits(z))
S[k + 1] = round_base(S[k + 1])
S[2 * k] = round_base(S[2 * k])
viewmin, viewmax = S[k + 1], S[2 * k]
else
S = prealloc_Ss[ik]
Expand All @@ -211,8 +223,8 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks,
end
# round only those values that end up as viewmin and viewmax
# to save computation time
S[1] = round(S[1], sigdigits = sigdigits(z))
S[k] = round(S[k], sigdigits = sigdigits(z))
S[1] = round_base(S[1])
S[k] = round_base(S[k])
viewmin, viewmax = S[1], S[k]
end
if strict_span
Expand Down Expand Up @@ -240,14 +252,14 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks,
has_zero = r <= 0 && abs(r) < k

# simplicity
s = has_zero ? 1.0 : 0.0
s = has_zero ? 1 : 0

# granularity
g = 0 < length(S) < 2k_ideal ? 1 - abs(length(S) - k_ideal) / k_ideal : 0.0
g = 0 < length(S) < 2k_ideal ? 1 - abs(length(S) - k_ideal) / k_ideal : 0

# coverage
effective_span = (length(S)-1) * tickspan
c = 1.5 * xspan/effective_span
effective_span = (length(S) - 1) * tickspan
c = 1.5xspan / effective_span

score = granularity_weight * g +
simplicity_weight * s +
Expand All @@ -258,7 +270,7 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks,
if strict_span && span > xspan
score -= 10000
end
if span >= 2.0*xspan
if span >= 2xspan
score -= 1000
end

Expand All @@ -268,7 +280,7 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks,
# could otherwise be mutated in the next runs
S = collect(S)
end
(S_best, viewmin_best, viewmax_best) = (S, viewmin, viewmax)
S_best, viewmin_best, viewmax_best = S, viewmin, viewmax
high_score = score
end
r += 1
Expand All @@ -281,14 +293,12 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks,
if isinf(high_score)
if strict_span
@warn("No strict ticks found")
return optimize_ticks_typed(x_min, x_max, extend_ticks,
Q, k_min,
k_max, k_ideal,
return optimize_ticks_typed(x_min, x_max, extend_ticks, Q, k_min, k_max, k_ideal,
granularity_weight, simplicity_weight,
coverage_weight, niceness_weight,
false, span_buffer)
false, span_buffer, scale)
else
R = typeof(1.0 * one_t)
R = typeof(1. * one_t)
return R[x_min], x_min - one_t, x_min + one_t
end
end
Expand Down

0 comments on commit 1354566

Please sign in to comment.