From 7d1d05b40540feee2de06ccab18d60df05d9b35a Mon Sep 17 00:00:00 2001 From: yha Date: Mon, 4 May 2020 14:41:37 +0300 Subject: [PATCH 1/7] Dedicated `Ticks` type to simplify ticks calculation --- Project.toml | 3 +- src/ticks.jl | 280 ++++++++++++++++++++++------------------------- test/runtests.jl | 14 ++- 3 files changed, 145 insertions(+), 152 deletions(-) diff --git a/Project.toml b/Project.toml index d1c3c42..5f1ee2f 100644 --- a/Project.toml +++ b/Project.toml @@ -21,6 +21,7 @@ julia = "1.6" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [targets] -test = ["StableRNGs", "Statistics", "Test"] +test = ["StableRNGs", "Statistics", "Test", "Unitful"] diff --git a/src/ticks.jl b/src/ticks.jl index 7241e63..6010d09 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -6,14 +6,22 @@ const _logScaleBases = Dict(:ln => ℯ, :log2 => 2.0, :log10 => 10.0) # 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::T, base::T) where {T} +function bounding_order_of_magnitude(xspan::T) where {T} + one_dt = oneunit(T) + a = step = 1 - while xspan < base^a + while xspan < base^a * one_dt +function bounding_order_of_magnitude(xspan::DT) where DT + one_dt = oneunit(DT) + + a = 1 + step = 1 + while xspan < 10.0^a * one_dt a -= step end b = step = 1 - while xspan > base^b + while xspan > base^b * one_dt b += step end @@ -35,6 +43,13 @@ function postdecimal_digits(x::T) where {T} end return 0 end +struct Ticks{T} <: AbstractRange{T} + u::UnitRange{Int} + q::Int + z::Int + step::Float64 + Ticks{T}(u,q,z) where T = new(u, q, z, q * 10.0^z) +end fallback_ticks(x_min::T, x_max::T, k_min, k_max) where {T} = ( if k_min != 2 && isfinite(x_min) && isfinite(x_max) @@ -44,6 +59,34 @@ fallback_ticks(x_min::T, x_max::T, k_min, k_max) where {T} = ( end ) +Base.size(t::Ticks) = size(t.u) +Base.step(t::Ticks{T}) where T = t.step * oneunit(T) +Base.getindex(t::Ticks{T}, i::Integer) where T = round(t.u[i]*t.step; digits=max(-t.z,0)) * oneunit(T) +_ticks_str(t::Ticks) = "($(t.q*t.u))*10^$(t.z)" +Base.show(io::IO, t::Ticks{<:AbstractFloat}) = print(io, _ticks_str(t)) +Base.show(io::IO, t::Ticks{T}) where T = print(io, _ticks_str(t), " * ", oneunit(T)) + + +function restrict_ticks(t::Ticks{T}, from, to) where T + tickspan = step(t) + u_start = max(first(t.u), ceil(Int, from / tickspan)) + u_end = min(last(t.u), floor(Int, to / tickspan)) + t = Ticks{T}(u_start:u_end,t.q,t.z) + + # Fix possible floating-point errors (may occur in division above, or due + # rounding in (::Ticks)[::Int] when endpoints are near a round number) + while u_start <= u_end && t[1] < from + u_start += 1 + t = Ticks{T}(u_start:u_end,t.q,t.z) + end + while u_start <= u_end && t[end] > to + u_end -= 1 + t = Ticks{T}(u_start:u_end,t.q,t.z) + end + t +end + + # Empty catchall optimize_ticks() = Any[] @@ -136,65 +179,29 @@ and the variables here are: * `i`: index of `q` in `Q`. * `v`: 1 if label range includes 0, 0 otherwise. """ -function optimize_ticks( - x_min::T, - x_max::T; - extend_ticks::Bool = false, - Q = [(1.0, 1.0), (5.0, 0.9), (2.0, 0.7), (2.5, 0.5), (3.0, 0.2)], - k_min::Integer = 2, - k_max::Integer = 10, - k_ideal::Integer = 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, - scale = nothing, -) where {T} - F = float(T) - if x_max - x_min < eps(F) - return fallback_ticks(x_min, x_max, k_min, k_max) - end +function optimize_ticks(x_min::T, x_max::T; extend_ticks::Bool=false, + Q=[(10,1.0), (50, 0.9), (20, 0.7), (25, 0.5), (30, 0.2)], + 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 - Qv = F[q[1] for q in Q] - Qs = F[q[2] for q in Q] - - base_float = F(get(_logScaleBases, scale, 10.0)) - base = isinteger(base_float) ? Int(base_float) : 10 - is_log_scale = scale ∈ _logScales - - for i in 1:2 - sspan = i == 1 ? strict_span : false - high_score, best, min_best, max_best = optimize_ticks_typed( - F(x_min), - F(x_max), - extend_ticks, - Qv, - Qs, - k_min, - k_max, - k_ideal, - F(granularity_weight), - F(simplicity_weight), - F(coverage_weight), - F(niceness_weight), - sspan, - span_buffer, - is_log_scale, - base_float, - base, - ) + Qv = [(Int(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) +end - if isinf(high_score) - if sspan - @warn "No strict ticks found" - else - return fallback_ticks(x_min, x_max, k_min, k_max) - end - else - return best, min_best, max_best - end +function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks, + Q::Vector{Tuple{Int,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 + one_t = oneunit(T) + if x_max - x_min < eps()*one_t + R = typeof(1.0 * one_t) + return R[x_min], x_min - one_t, x_min + one_t end end @@ -220,82 +227,45 @@ function optimize_ticks_typed( xspan = x_max - x_min # generalizing "order of magnitude" - z = bounding_order_of_magnitude(xspan, base_float) - - # find required significant digits for ticks with q * base^z spacing, - # for q values specified in Qv - num_digits = ( - bounding_order_of_magnitude(max(abs(x_min), abs(x_max)), base_float) + - maximum(postdecimal_digits(q) for q in Qv) - ) + xspan = x_max - x_min + z = bounding_order_of_magnitude(xspan / minimum(q[1] for q in Q)) - viewmin_best, viewmax_best = x_min, x_max high_score = -Inf + best_ticks = nothing + + max_q_exponent = ceil(Int,log10(maximum(q[1] for q in Q))) + while 2k_max * 10.0^(z+max_q_exponent) * one_t > xspan + for (ik, k) in enumerate(k_min:2k_max) + for (q, qscore) in Q + stp = q*10.0^z + if stp < eps() + continue + end - S_best = Vector{F}(undef, k_max) - len_S_best = length(S_best) - - S = Vector{F}(undef, (extend_ticks ? 4 : 2) * k_max) - - @inbounds begin - while 2k_max * base_float^(z + 1) > xspan - sigdigits = max(1, num_digits - z) - for k in k_min:(2k_max) - for (q, qscore) in zip(Qv, Qs) - tickspan = q * base_float^z - tickspan < eps(F) && continue - span = (k - 1) * tickspan - span < xspan && continue - - r_float = (x_max - span) / tickspan - isfinite(r_float) || continue - r = ceil(Int, r_float) - - # try to favor integer exponents for log scales - (nice_scale = !is_log_scale || isinteger(tickspan)) || (qscore = F(0)) - - while r * tickspan <= x_min - # Filter or expand ticks - if extend_ticks - for i in 0:(3k - 1) - S[i + 1] = (r + i - k) * tickspan - end - imin = k + 1 - imax = 2k - else - for i in 0:(k - 1) - S[i + 1] = (r + i) * tickspan - end - imin = 1 - imax = k - end - # round only those values that end up as viewmin and viewmax to save computation time - S[imin] = - viewmin = round(S[imin], sigdigits = sigdigits, base = base) - S[imax] = - viewmax = round(S[imax], sigdigits = sigdigits, base = base) - - if strict_span - viewmin = max(viewmin, x_min) - viewmax = min(viewmax, x_max) - buf = something(span_buffer, 0) * (viewmax - viewmin) - - # filter the S array while reusing its own memory to do so - # this works because S is sorted, and we will only overwrite - # values that are not needed anymore going forward in the loop - - # we do this because it saves allocations and leaves S type stable - counter = 0 - for i in 1:imax - if (viewmin - buf) <= S[i] <= (viewmax + buf) - counter += 1 - S[counter] = S[i] - end - end - len = counter - else - len = imax + tickspan = stp * one_t + span = (k - 1) * tickspan + if span < xspan + continue + end + + r = ceil(Int64, (x_max - span) / tickspan) + + while r*tickspan <= x_min + u = extend_ticks ? (r-k:r+2k-1) : (r:r+k-1) + ticks = Ticks{T}(u, q, z) + + if strict_span + viewmin = max(r*tickspan, x_min) + viewmax = min((r+k-1)*tickspan, x_max) + buf = something(span_buffer, 0) * (viewmax - viewmin) + + ticks = restrict_ticks(ticks,viewmin-buf,viewmax+buf) + if length(ticks) < k_min + r += 1 + continue end + end + nticks = length(ticks) # evaluate quality of ticks has_zero = r <= 0 && abs(r) < k @@ -306,16 +276,14 @@ function optimize_ticks_typed( # granularity g = 0 < len < 2k_ideal ? 1 - abs(len - k_ideal) / k_ideal : F(0) - # coverage - c = if len > 1 - effective_span = (len - 1) * tickspan - 1.5xspan / effective_span - else - F(0) - end + # granularity + g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 - score = - granularity_weight * g + + # coverage + effective_span = (nticks-1) * tickspan + c = 1.5 * xspan/effective_span + + score = granularity_weight * g + simplicity_weight * s + coverage_weight * c + niceness_weight * qscore @@ -328,20 +296,34 @@ function optimize_ticks_typed( score -= 1000 end - if score > high_score && (k_min <= len <= k_max) - viewmin_best, viewmax_best = viewmin, viewmax - high_score, len_S_best = score, len - copyto!(S_best, view(S, 1:len)) - end - r += 1 + if score > high_score && (k_min <= nticks <= k_max) + best_ticks = ticks + high_score = score end end end z -= 1 end end - resize!(S_best, len_S_best) - return high_score, S_best, viewmin_best, viewmax_best + + if best_ticks === nothing + 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, + granularity_weight, simplicity_weight, + coverage_weight, niceness_weight, + false, span_buffer) + else + R = typeof(1.0 * one_t) + return R[x_min], x_min - one_t, x_min + one_t + end + end + + viewmin = min(first(best_ticks), x_min) + viewmax = max(last(best_ticks), x_max) + return (best_ticks, viewmin, viewmax) end optimize_ticks( diff --git a/test/runtests.jl b/test/runtests.jl index 3e47fbe..1254292 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,7 @@ using PlotUtils using Test using Statistics: mean using Dates +using Unitful using Random using StableRNGs @@ -112,9 +113,15 @@ end x, y = minmax(x, y) ticks, = optimize_ticks(x, y) @test issorted(ticks) - @test all(x .<= ticks .<= y) + @test allunique(ticks) + if (x,y) ∈ [(1.0,1.0+eps()), (1.0-eps(),1.0)] # known failures + @test_broken all(x .<= ticks .<= y) + else + @test all(x .<= ticks .<= y) + end # Fails: - # @test allunique(ticks) + #@test is_uniformly_spaced(ticks) + end end @@ -142,6 +149,9 @@ end end end + km = Unitful.km + @test optimize_ticks(2km, 5km) == optimize_ticks(2, 5) .* 1km + @testset "types" begin for T in (Int32, Int64, Float16, Float32, Float64) x, y = T(1), T(10) From bcff7d0286664e5cfcf95e2c285499dfc8f20f87 Mon Sep 17 00:00:00 2001 From: yha Date: Mon, 4 May 2020 14:59:16 +0300 Subject: [PATCH 2/7] Simpler rounding --- src/ticks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ticks.jl b/src/ticks.jl index 6010d09..8b53ca0 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -61,7 +61,7 @@ fallback_ticks(x_min::T, x_max::T, k_min, k_max) where {T} = ( Base.size(t::Ticks) = size(t.u) Base.step(t::Ticks{T}) where T = t.step * oneunit(T) -Base.getindex(t::Ticks{T}, i::Integer) where T = round(t.u[i]*t.step; digits=max(-t.z,0)) * oneunit(T) +Base.getindex(t::Ticks{T}, i::Integer) where T = round(t.u[i]*t.step; digits=-t.z) * oneunit(T) _ticks_str(t::Ticks) = "($(t.q*t.u))*10^$(t.z)" Base.show(io::IO, t::Ticks{<:AbstractFloat}) = print(io, _ticks_str(t)) Base.show(io::IO, t::Ticks{T}) where T = print(io, _ticks_str(t), " * ", oneunit(T)) From e8e6463a8a73a1644f8f7723311d70756ba3b45e Mon Sep 17 00:00:00 2001 From: yha Date: Mon, 4 May 2020 21:22:20 +0300 Subject: [PATCH 3/7] Fix for 32-bit --- src/ticks.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ticks.jl b/src/ticks.jl index 8b53ca0..1e5fa81 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -44,7 +44,7 @@ function postdecimal_digits(x::T) where {T} return 0 end struct Ticks{T} <: AbstractRange{T} - u::UnitRange{Int} + u::UnitRange{Int64} q::Int z::Int step::Float64 @@ -69,8 +69,8 @@ Base.show(io::IO, t::Ticks{T}) where T = print(io, _ticks_str(t), " * ", oneunit function restrict_ticks(t::Ticks{T}, from, to) where T tickspan = step(t) - u_start = max(first(t.u), ceil(Int, from / tickspan)) - u_end = min(last(t.u), floor(Int, to / tickspan)) + u_start = max(first(t.u), ceil(Int64, from / tickspan)) + u_end = min(last(t.u), floor(Int64, to / tickspan)) t = Ticks{T}(u_start:u_end,t.q,t.z) # Fix possible floating-point errors (may occur in division above, or due From 5effa77c21d728f872cff6b6185449e6b0d7fc92 Mon Sep 17 00:00:00 2001 From: Simon Christ Date: Thu, 23 Jun 2022 16:18:49 +0200 Subject: [PATCH 4/7] format files --- src/ticks.jl | 170 ++++++++++++++++++++++++++++------------------- test/runtests.jl | 2 +- 2 files changed, 102 insertions(+), 70 deletions(-) diff --git a/src/ticks.jl b/src/ticks.jl index 1e5fa81..7b5bc57 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -11,12 +11,6 @@ function bounding_order_of_magnitude(xspan::T) where {T} a = step = 1 while xspan < base^a * one_dt -function bounding_order_of_magnitude(xspan::DT) where DT - one_dt = oneunit(DT) - - a = 1 - step = 1 - while xspan < 10.0^a * one_dt a -= step end @@ -48,7 +42,7 @@ struct Ticks{T} <: AbstractRange{T} q::Int z::Int step::Float64 - Ticks{T}(u,q,z) where T = new(u, q, z, q * 10.0^z) + Ticks{T}(u, q, z) where {T} = new(u, q, z, q * 10.0^z) end fallback_ticks(x_min::T, x_max::T, k_min, k_max) where {T} = ( @@ -60,33 +54,32 @@ fallback_ticks(x_min::T, x_max::T, k_min, k_max) where {T} = ( ) Base.size(t::Ticks) = size(t.u) -Base.step(t::Ticks{T}) where T = t.step * oneunit(T) -Base.getindex(t::Ticks{T}, i::Integer) where T = round(t.u[i]*t.step; digits=-t.z) * oneunit(T) +Base.step(t::Ticks{T}) where {T} = t.step * oneunit(T) +Base.getindex(t::Ticks{T}, i::Integer) where {T} = + round(t.u[i] * t.step; digits = -t.z) * oneunit(T) _ticks_str(t::Ticks) = "($(t.q*t.u))*10^$(t.z)" Base.show(io::IO, t::Ticks{<:AbstractFloat}) = print(io, _ticks_str(t)) -Base.show(io::IO, t::Ticks{T}) where T = print(io, _ticks_str(t), " * ", oneunit(T)) - +Base.show(io::IO, t::Ticks{T}) where {T} = print(io, _ticks_str(t), " * ", oneunit(T)) -function restrict_ticks(t::Ticks{T}, from, to) where T +function restrict_ticks(t::Ticks{T}, from, to) where {T} tickspan = step(t) u_start = max(first(t.u), ceil(Int64, from / tickspan)) u_end = min(last(t.u), floor(Int64, to / tickspan)) - t = Ticks{T}(u_start:u_end,t.q,t.z) + t = Ticks{T}(u_start:u_end, t.q, t.z) # Fix possible floating-point errors (may occur in division above, or due # rounding in (::Ticks)[::Int] when endpoints are near a round number) while u_start <= u_end && t[1] < from u_start += 1 - t = Ticks{T}(u_start:u_end,t.q,t.z) + t = Ticks{T}(u_start:u_end, t.q, t.z) end while u_start <= u_end && t[end] > to u_end -= 1 - t = Ticks{T}(u_start:u_end,t.q,t.z) + t = Ticks{T}(u_start:u_end, t.q, t.z) end t end - # Empty catchall optimize_ticks() = Any[] @@ -179,27 +172,56 @@ and the variables here are: * `i`: index of `q` in `Q`. * `v`: 1 if label range includes 0, 0 otherwise. """ -function optimize_ticks(x_min::T, x_max::T; extend_ticks::Bool=false, - Q=[(10,1.0), (50, 0.9), (20, 0.7), (25, 0.5), (30, 0.2)], - 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 - +function optimize_ticks( + x_min::T, + x_max::T; + extend_ticks::Bool = false, + Q = [(10, 1.0), (50, 0.9), (20, 0.7), (25, 0.5), (30, 0.2)], + 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} Qv = [(Int(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) + 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, + ) end -function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks, - Q::Vector{Tuple{Int,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 +function optimize_ticks_typed( + x_min::T, + x_max::T, + extend_ticks, + Q::Vector{Tuple{Int,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} one_t = oneunit(T) - if x_max - x_min < eps()*one_t + if x_max - x_min < eps() * one_t R = typeof(1.0 * one_t) return R[x_min], x_min - one_t, x_min + one_t end @@ -233,11 +255,11 @@ function optimize_ticks_typed( high_score = -Inf best_ticks = nothing - max_q_exponent = ceil(Int,log10(maximum(q[1] for q in Q))) - while 2k_max * 10.0^(z+max_q_exponent) * one_t > xspan - for (ik, k) in enumerate(k_min:2k_max) + max_q_exponent = ceil(Int, log10(maximum(q[1] for q in Q))) + while 2k_max * 10.0^(z + max_q_exponent) * one_t > xspan + for (ik, k) in enumerate(k_min:(2k_max)) for (q, qscore) in Q - stp = q*10.0^z + stp = q * 10.0^z if stp < eps() continue end @@ -250,16 +272,16 @@ function optimize_ticks_typed( r = ceil(Int64, (x_max - span) / tickspan) - while r*tickspan <= x_min - u = extend_ticks ? (r-k:r+2k-1) : (r:r+k-1) + while r * tickspan <= x_min + u = extend_ticks ? ((r - k):(r + 2k - 1)) : (r:(r + k - 1)) ticks = Ticks{T}(u, q, z) if strict_span - viewmin = max(r*tickspan, x_min) - viewmax = min((r+k-1)*tickspan, x_max) + viewmin = max(r * tickspan, x_min) + viewmax = min((r + k - 1) * tickspan, x_max) buf = something(span_buffer, 0) * (viewmax - viewmin) - ticks = restrict_ticks(ticks,viewmin-buf,viewmax+buf) + ticks = restrict_ticks(ticks, viewmin - buf, viewmax + buf) if length(ticks) < k_min r += 1 continue @@ -267,34 +289,35 @@ function optimize_ticks_typed( end nticks = length(ticks) - # evaluate quality of ticks - has_zero = r <= 0 && abs(r) < k + # evaluate quality of ticks + has_zero = r <= 0 && abs(r) < k - # simplicity - s = has_zero && nice_scale ? 1 : 0 + # simplicity + s = has_zero && nice_scale ? 1 : 0 - # granularity - g = 0 < len < 2k_ideal ? 1 - abs(len - k_ideal) / k_ideal : F(0) + # granularity + g = 0 < len < 2k_ideal ? 1 - abs(len - k_ideal) / k_ideal : F(0) # granularity g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 # coverage - effective_span = (nticks-1) * tickspan - c = 1.5 * xspan/effective_span - - score = granularity_weight * g + - simplicity_weight * s + - coverage_weight * c + - niceness_weight * qscore - - # strict limits on coverage - if strict_span && span > xspan - score -= 10000 - end - if span >= 2xspan - score -= 1000 - end + effective_span = (nticks - 1) * tickspan + c = 1.5 * xspan / effective_span + + score = + granularity_weight * g + + simplicity_weight * s + + coverage_weight * c + + niceness_weight * qscore + + # strict limits on coverage + if strict_span && span > xspan + score -= 10000 + end + if span >= 2xspan + score -= 1000 + end if score > high_score && (k_min <= nticks <= k_max) best_ticks = ticks @@ -309,12 +332,21 @@ function optimize_ticks_typed( if best_ticks === nothing 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, - granularity_weight, simplicity_weight, - coverage_weight, niceness_weight, - false, span_buffer) + 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, + ) else R = typeof(1.0 * one_t) return R[x_min], x_min - one_t, x_min + one_t diff --git a/test/runtests.jl b/test/runtests.jl index 1254292..c4e4f3c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -114,7 +114,7 @@ end ticks, = optimize_ticks(x, y) @test issorted(ticks) @test allunique(ticks) - if (x,y) ∈ [(1.0,1.0+eps()), (1.0-eps(),1.0)] # known failures + if (x, y) ∈ [(1.0, 1.0 + eps()), (1.0 - eps(), 1.0)] # known failures @test_broken all(x .<= ticks .<= y) else @test all(x .<= ticks .<= y) From 29a8608c4ffe9f169ba89ccea9a8047a1dfdae94 Mon Sep 17 00:00:00 2001 From: Simon Christ Date: Thu, 23 Jun 2022 17:36:34 +0200 Subject: [PATCH 5/7] some fixes --- src/ticks.jl | 66 +++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/ticks.jl b/src/ticks.jl index 7b5bc57..88a38c6 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -6,7 +6,7 @@ const _logScaleBases = Dict(:ln => ℯ, :log2 => 2.0, :log10 => 10.0) # 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::T) where {T} +function bounding_order_of_magnitude(xspan::T, base::T) where {T} one_dt = oneunit(T) a = step = 1 @@ -21,7 +21,7 @@ function bounding_order_of_magnitude(xspan::T) where {T} while a + 1 < b c = div(a + b, 2) - if xspan < base^c + if xspan < base^c * one_dt b = c else a = c @@ -41,8 +41,9 @@ struct Ticks{T} <: AbstractRange{T} u::UnitRange{Int64} q::Int z::Int + base::T step::Float64 - Ticks{T}(u, q, z) where {T} = new(u, q, z, q * 10.0^z) + Ticks{T}(u, q, z; base = 10.0) where {T} = new(u, q, z, base, q * float(base)^z) end fallback_ticks(x_min::T, x_max::T, k_min, k_max) where {T} = ( @@ -54,10 +55,11 @@ fallback_ticks(x_min::T, x_max::T, k_min, k_max) where {T} = ( ) Base.size(t::Ticks) = size(t.u) +Base.length(t::Ticks) = Base.length(t.u) Base.step(t::Ticks{T}) where {T} = t.step * oneunit(T) Base.getindex(t::Ticks{T}, i::Integer) where {T} = round(t.u[i] * t.step; digits = -t.z) * oneunit(T) -_ticks_str(t::Ticks) = "($(t.q*t.u))*10^$(t.z)" +_ticks_str(t::Ticks) = "($(t.q*t.u))*$(t.base)^$(t.z)" Base.show(io::IO, t::Ticks{<:AbstractFloat}) = print(io, _ticks_str(t)) Base.show(io::IO, t::Ticks{T}) where {T} = print(io, _ticks_str(t), " * ", oneunit(T)) @@ -186,8 +188,14 @@ function optimize_ticks( niceness_weight::Float64 = 1 / 4, strict_span = true, span_buffer = nothing, + scale = nothing, ) where {T} Qv = [(Int(q[1]), Float64(q[2])) for q in Q] + + base_float = Float64(get(_logScaleBases, scale, 10.0)) + base = isinteger(base_float) ? Int(base_float) : 10 + is_log_scale = scale ∈ _logScales + optimize_ticks_typed( x_min, x_max, @@ -202,6 +210,9 @@ function optimize_ticks( niceness_weight, strict_span, span_buffer, + is_log_scale, + base_float, + base, ) end @@ -219,47 +230,30 @@ function optimize_ticks_typed( niceness_weight::Float64, strict_span, span_buffer, + is_log_scale, + base_float::Float64, + base::Integer, ) where {T} one_t = oneunit(T) - if x_max - x_min < eps() * one_t + + xspan = x_max - x_min + if xspan < eps(T) * one_t R = typeof(1.0 * one_t) return R[x_min], x_min - one_t, x_min + one_t end -end - -function optimize_ticks_typed( - x_min::F, - x_max::F, - extend_ticks, - Qv, - Qs, - k_min, - k_max, - k_ideal, - granularity_weight::F, - simplicity_weight::F, - coverage_weight::F, - niceness_weight::F, - strict_span, - span_buffer, - is_log_scale, - base_float::F, - base::Integer, -) where {F<:AbstractFloat} - xspan = x_max - x_min # generalizing "order of magnitude" - xspan = x_max - x_min - z = bounding_order_of_magnitude(xspan / minimum(q[1] for q in Q)) + z = bounding_order_of_magnitude(xspan / minimum(q[1] for q in Q), base_float) high_score = -Inf best_ticks = nothing - max_q_exponent = ceil(Int, log10(maximum(q[1] for q in Q))) - while 2k_max * 10.0^(z + max_q_exponent) * one_t > xspan + max_q_exponent = ceil(Int, log(base, maximum(q[1] for q in Q))) + count = 0 + while 2k_max * base_float^z + max_q_exponent * one_t > xspan for (ik, k) in enumerate(k_min:(2k_max)) for (q, qscore) in Q - stp = q * 10.0^z + stp = q * base_float^z if stp < eps() continue end @@ -273,8 +267,12 @@ function optimize_ticks_typed( r = ceil(Int64, (x_max - span) / tickspan) while r * tickspan <= x_min + @show r * tickspan + @show x_min + count += 1 + count > 100_000 && error("Potential infinite loop in `optimize_ticks_typed`.") u = extend_ticks ? ((r - k):(r + 2k - 1)) : (r:(r + k - 1)) - ticks = Ticks{T}(u, q, z) + ticks = Ticks{T}(u, q, z; base) if strict_span viewmin = max(r * tickspan, x_min) @@ -296,7 +294,7 @@ function optimize_ticks_typed( s = has_zero && nice_scale ? 1 : 0 # granularity - g = 0 < len < 2k_ideal ? 1 - abs(len - k_ideal) / k_ideal : F(0) + g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 # granularity g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 From 62f08ae9e7a5f42eb39affe85fdb285a4c7eb82b Mon Sep 17 00:00:00 2001 From: Simon Christ Date: Fri, 24 Jun 2022 11:10:24 +0200 Subject: [PATCH 6/7] minimal running version --- src/ticks.jl | 206 ++++++++++++++++++++++++++++----------------------- 1 file changed, 113 insertions(+), 93 deletions(-) diff --git a/src/ticks.jl b/src/ticks.jl index 88a38c6..246b745 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -179,9 +179,9 @@ function optimize_ticks( x_max::T; extend_ticks::Bool = false, Q = [(10, 1.0), (50, 0.9), (20, 0.7), (25, 0.5), (30, 0.2)], - k_min::Int = 2, - k_max::Int = 10, - k_ideal::Int = 5, + k_min::Integer = 2, + k_max::Integer = 10, + k_ideal::Integer = 5, granularity_weight::Float64 = 1 / 4, simplicity_weight::Float64 = 1 / 6, coverage_weight::Float64 = 1 / 3, @@ -190,140 +190,160 @@ function optimize_ticks( span_buffer = nothing, scale = nothing, ) where {T} - Qv = [(Int(q[1]), Float64(q[2])) for q in Q] + F = float(T) + if x_max - x_min < eps(F) + return fallback_ticks(x_min, x_max, k_min, k_max) + end + Qv = Int.(getindex.(Q, 1)) + Qs = F.(getindex.(Q, 2)) - base_float = Float64(get(_logScaleBases, scale, 10.0)) + base_float = F(get(_logScaleBases, scale, 10.0)) base = isinteger(base_float) ? Int(base_float) : 10 is_log_scale = scale ∈ _logScales - optimize_ticks_typed( - x_min, - x_max, + for i in 1:2 + sspan = (i == 1) ? strict_span : false + high_score, best, min_best, max_best = optimize_ticks_typed( + F(x_min), + F(x_max), extend_ticks, Qv, + Qs, k_min, k_max, k_ideal, - granularity_weight, - simplicity_weight, - coverage_weight, - niceness_weight, - strict_span, + F(granularity_weight), + F(simplicity_weight), + F(coverage_weight), + F(niceness_weight), + sspan, span_buffer, is_log_scale, base_float, base, - ) + ) + + if isinf(high_score) + if sspan + @warn "No strict ticks found" + else + return fallback_ticks(x_min, x_max, k_min, k_max) + end + else + return best, min_best, max_best + end + end end function optimize_ticks_typed( - x_min::T, - x_max::T, + x_min::F, + x_max::F, extend_ticks, - Q::Vector{Tuple{Int,Float64}}, + Qv::Vector{Int}, + Qs::Vector{F}, k_min, k_max, k_ideal, - granularity_weight::Float64, - simplicity_weight::Float64, - coverage_weight::Float64, - niceness_weight::Float64, + granularity_weight::F, + simplicity_weight::F, + coverage_weight::F, + niceness_weight::F, strict_span, span_buffer, is_log_scale, - base_float::Float64, + base_float::F, base::Integer, -) where {T} - one_t = oneunit(T) +) where {F} + one_t = oneunit(eltype(Qv)) xspan = x_max - x_min - if xspan < eps(T) * one_t - R = typeof(1.0 * one_t) - return R[x_min], x_min - one_t, x_min + one_t - end # generalizing "order of magnitude" - z = bounding_order_of_magnitude(xspan / minimum(q[1] for q in Q), base_float) + z = bounding_order_of_magnitude(xspan / minimum(Qv), base_float) high_score = -Inf best_ticks = nothing - max_q_exponent = ceil(Int, log(base, maximum(q[1] for q in Q))) - count = 0 - while 2k_max * base_float^z + max_q_exponent * one_t > xspan - for (ik, k) in enumerate(k_min:(2k_max)) - for (q, qscore) in Q - stp = q * base_float^z - if stp < eps() - continue - end - - tickspan = stp * one_t - span = (k - 1) * tickspan - if span < xspan - continue - end + max_q_exponent = ceil(Int, log(base, maximum(Qv))) - r = ceil(Int64, (x_max - span) / tickspan) - - while r * tickspan <= x_min - @show r * tickspan - @show x_min - count += 1 - count > 100_000 && error("Potential infinite loop in `optimize_ticks_typed`.") - u = extend_ticks ? ((r - k):(r + 2k - 1)) : (r:(r + k - 1)) - ticks = Ticks{T}(u, q, z; base) - - if strict_span - viewmin = max(r * tickspan, x_min) - viewmax = min((r + k - 1) * tickspan, x_max) - buf = something(span_buffer, 0) * (viewmax - viewmin) - - ticks = restrict_ticks(ticks, viewmin - buf, viewmax + buf) - if length(ticks) < k_min - r += 1 - continue + count = 0 + @inbounds begin + while 2k_max * base_float^(z + max_q_exponent) * one_t > xspan + for k in k_min:(2k_max) + for (q, qscore) in zip(Qv, Qs) + stp = q * base_float^z + stp < eps(F) && continue + + + tickspan = stp * one_t + span = (k - 1) * tickspan + span < xspan && continue + + r_float = (x_max - span) / stp + isfinite(r_float) || continue + r = ceil(Int, r_float) * one_t + + # try to favor integer exponents for log scales + (nice_scale = !is_log_scale || isinteger(tickspan)) || (qscore = F(0)) + + while r * tickspan <= x_min + count += 1 + count > 100_000 && error("Potential infinite loop in `optimize_ticks_typed`.") + u = extend_ticks ? ((r - k):(r + 2k - 1)) : (r:(r + k - 1)) + ticks = Ticks{F}(u, q, z; base) + + if strict_span + viewmin = max(r * tickspan, x_min) + viewmax = min((r + k - 1) * tickspan, x_max) + buf = something(span_buffer, 0) * (viewmax - viewmin) + + ticks = restrict_ticks(ticks, viewmin - buf, viewmax + buf) + if length(ticks) < k_min + r += 1 + continue + end end - end - nticks = length(ticks) + nticks = length(ticks) - # evaluate quality of ticks - has_zero = r <= 0 && abs(r) < k + # evaluate quality of ticks + has_zero = r <= 0 && abs(r) < k - # simplicity - s = has_zero && nice_scale ? 1 : 0 + # simplicity + s = has_zero && nice_scale ? 1 : 0 - # granularity - g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 + # granularity + g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 - # granularity - g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 + # granularity + g = 0 < nticks < 2k_ideal ? 1 - abs(nticks - k_ideal) / k_ideal : 0.0 - # coverage - effective_span = (nticks - 1) * tickspan - c = 1.5 * xspan / effective_span + # coverage + effective_span = (nticks - 1) * tickspan + c = 1.5 * xspan / effective_span - score = - granularity_weight * g + - simplicity_weight * s + - coverage_weight * c + - niceness_weight * qscore + score = + granularity_weight * g + + simplicity_weight * s + + coverage_weight * c + + niceness_weight * qscore - # strict limits on coverage - if strict_span && span > xspan - score -= 10000 - end - if span >= 2xspan - score -= 1000 - end + # strict limits on coverage + if strict_span && span > xspan + score -= 10000 + end + if span >= 2xspan + score -= 1000 + end - if score > high_score && (k_min <= nticks <= k_max) - best_ticks = ticks - high_score = score + if score > high_score && (k_min <= nticks <= k_max) + best_ticks = ticks + high_score = score + end + r += 1 end end + z -= 1 end - z -= 1 end end @@ -353,7 +373,7 @@ function optimize_ticks_typed( viewmin = min(first(best_ticks), x_min) viewmax = max(last(best_ticks), x_max) - return (best_ticks, viewmin, viewmax) + return high_score, best_ticks, viewmin, viewmax end optimize_ticks( From 1133dd6956cdb9bda76e411298783ef830ce1f0e Mon Sep 17 00:00:00 2001 From: Simon Christ Date: Fri, 24 Jun 2022 12:11:32 +0200 Subject: [PATCH 7/7] fixed errors --- src/ticks.jl | 63 +++++++++++++++++------------------------------- test/runtests.jl | 14 +++++------ 2 files changed, 29 insertions(+), 48 deletions(-) diff --git a/src/ticks.jl b/src/ticks.jl index 246b745..0fc091c 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -6,7 +6,7 @@ const _logScaleBases = Dict(:ln => ℯ, :log2 => 2.0, :log10 => 10.0) # 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::T, base::T) where {T} +function bounding_order_of_magnitude(xspan::T, base::F) where {T,F<:AbstractFloat} one_dt = oneunit(T) a = step = 1 @@ -41,7 +41,7 @@ struct Ticks{T} <: AbstractRange{T} u::UnitRange{Int64} q::Int z::Int - base::T + base::Float64 step::Float64 Ticks{T}(u, q, z; base = 10.0) where {T} = new(u, q, z, base, q * float(base)^z) end @@ -190,8 +190,8 @@ function optimize_ticks( span_buffer = nothing, scale = nothing, ) where {T} - F = float(T) - if x_max - x_min < eps(F) + F = typeof(float(one(T))) + if x_max - x_min < eps(F) * oneunit(T) return fallback_ticks(x_min, x_max, k_min, k_max) end Qv = Int.(getindex.(Q, 1)) @@ -204,8 +204,8 @@ function optimize_ticks( for i in 1:2 sspan = (i == 1) ? strict_span : false high_score, best, min_best, max_best = optimize_ticks_typed( - F(x_min), - F(x_max), + float(x_min), + float(x_max), extend_ticks, Qv, Qs, @@ -240,21 +240,21 @@ function optimize_ticks_typed( x_max::F, extend_ticks, Qv::Vector{Int}, - Qs::Vector{F}, + Qs::Vector{F1}, k_min, k_max, k_ideal, - granularity_weight::F, - simplicity_weight::F, - coverage_weight::F, - niceness_weight::F, + granularity_weight::F1, + simplicity_weight::F1, + coverage_weight::F1, + niceness_weight::F1, strict_span, span_buffer, is_log_scale, - base_float::F, + base_float::F1, base::Integer, -) where {F} - one_t = oneunit(eltype(Qv)) +) where {F, F1 <: AbstractFloat} + one_t = oneunit(F) xspan = x_max - x_min @@ -279,9 +279,9 @@ function optimize_ticks_typed( span = (k - 1) * tickspan span < xspan && continue - r_float = (x_max - span) / stp + r_float = (x_max - span) / tickspan isfinite(r_float) || continue - r = ceil(Int, r_float) * one_t + r = ceil(Int64, r_float) # try to favor integer exponents for log scales (nice_scale = !is_log_scale || isinteger(tickspan)) || (qscore = F(0)) @@ -347,32 +347,13 @@ function optimize_ticks_typed( end end - if best_ticks === nothing - 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, - granularity_weight, - simplicity_weight, - coverage_weight, - niceness_weight, - false, - span_buffer, - ) - else - R = typeof(1.0 * one_t) - return R[x_min], x_min - one_t, x_min + one_t - end + if best_ticks !== nothing + viewmin = min(first(best_ticks), x_min) + viewmax = max(last(best_ticks), x_max) + else + viewmin = x_min + viewmax = x_max end - - viewmin = min(first(best_ticks), x_min) - viewmax = max(last(best_ticks), x_max) return high_score, best_ticks, viewmin, viewmax end diff --git a/test/runtests.jl b/test/runtests.jl index c4e4f3c..4580629 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -111,7 +111,7 @@ end @testset "small range $x, $(i)ϵ" for x in exp10.(-12:12), i in -5:5 y = x + i * eps(x) x, y = minmax(x, y) - ticks, = optimize_ticks(x, y) + ticks, _ = optimize_ticks(x, y) @test issorted(ticks) @test allunique(ticks) if (x, y) ∈ [(1.0, 1.0 + eps()), (1.0 - eps(), 1.0)] # known failures @@ -144,7 +144,7 @@ end y0 = 10^n x0 = y0 - 1 x, y = (x0, y0) .* 10.0^i - ticks, = optimize_ticks(x, y) + ticks, _ = optimize_ticks(x, y) test_ticks(x, y, ticks) end end @@ -155,7 +155,7 @@ end @testset "types" begin for T in (Int32, Int64, Float16, Float32, Float64) x, y = T(1), T(10) - ticks, = optimize_ticks(x, y) + ticks, _ = optimize_ticks(x, y) @test eltype(ticks) <: AbstractFloat @test eltype(ticks) == (T <: AbstractFloat ? T : float(T)) test_ticks(x, y, ticks) @@ -195,18 +195,18 @@ end @testset "PlotUtils.jl/issues/129" begin # invalid float input let x = NaN, y = 1.0 - ticks, = @test_logs warn_ticks optimize_ticks(x, y) + ticks, _ = @test_logs warn_ticks optimize_ticks(x, y) @test isnan(ticks[1]) @test ticks[2] === y - ticks, = @test_logs warn_ticks optimize_ticks(x, y, k_min = 5) + ticks, _ = @test_logs warn_ticks optimize_ticks(x, y, k_min = 5) @test isnan(ticks[1]) @test ticks[2] === y end let x = 0.0f0, y = Inf32 - ticks, = @test_logs warn_ticks optimize_ticks(x, y) + ticks, _ = @test_logs warn_ticks optimize_ticks(x, y) @test ticks[1] === x @test isinf(ticks[2]) - ticks, = @test_logs warn_ticks optimize_ticks(x, y, k_min = 5) + ticks, _ = @test_logs warn_ticks optimize_ticks(x, y, k_min = 5) @test ticks[1] === x @test isinf(ticks[2]) end