Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework ticks computation for log scales #115

Merged
merged 3 commits into from
Jul 5, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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