diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..a580c42 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,12 @@ +always_for_in = true +import_to_using = false +align_pair_arrow = true +align_assignment = true +align_conditional = true +always_use_return = false +conditional_to_if = false +whitespace_in_kwargs = true +remove_extra_newlines = true +whitespace_ops_in_indices = true +short_to_long_function_def = false +annotate_untyped_fields_with_any = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 852fc04..24771d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,16 +19,18 @@ jobs: fail-fast: false matrix: version: - - '1.0' - - '1' - - 'nightly' + - '1.6' # LTS + - '1.7' # latest stable os: - ubuntu-latest - windows-latest - macos-latest arch: - x64 - - x86 + # - x86 + include: + - version: 'nightly' + os: ubuntu-latest steps: @@ -55,7 +57,7 @@ jobs: # Run tests - name: Run Graphical test - run: julia --project -e 'using Pkg; Pkg.test(coverage=true);' + run: julia --project -e 'using Pkg; Pkg.test(coverage=true)' # - name: Codecov # uses: julia-actions/julia-uploadcodecov@latest diff --git a/.github/workflows/format_pr.yml b/.github/workflows/format_pr.yml new file mode 100644 index 0000000..f547d1d --- /dev/null +++ b/.github/workflows/format_pr.yml @@ -0,0 +1,35 @@ +name: format-pr + +on: + schedule: + - cron: '0 0 * * SUN' + +jobs: + build: + if: "!contains(github.event.head_commit.message, '[skip ci]')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install JuliaFormatter and format + run: | + julia -e 'using Pkg; pkg"add JuliaFormatter"' + julia -e 'using JuliaFormatter; [format(["src", "test"]) for _ in 1:2]' + git diff --exit-code + + - name: Create Pull Request + if: ${{ failure() }} + id: cpr + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Format .jl files [skip ci]" + title: 'Automatic JuliaFormatter.jl run' + branch: auto-juliaformatter-pr + delete-branch: true + labels: formatting, automated pr, no changelog + + - name: Check outputs + if: ${{ failure() }} + run: | + echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" diff --git a/Project.toml b/Project.toml index 24cd7b4..33decb0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "PlotUtils" uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" -version = "1.0.15" +version = "1.1.0" [deps] ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" @@ -15,7 +15,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" ColorSchemes = "3.8" Colors = "0.12" Reexport = "0.2, 1.0" -julia = "1" +julia = "1.6" [extras] StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" diff --git a/REQUIRE b/REQUIRE deleted file mode 100644 index 85ec78b..0000000 --- a/REQUIRE +++ /dev/null @@ -1,3 +0,0 @@ -julia 0.7-alpha -Reexport -Colors diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 5150cd5..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,43 +0,0 @@ -environment: - matrix: - - julia_version: 1.0 - - julia_version: 1 - - julia_version: nightly - -platform: - - x86 # 32-bit - - x64 # 64-bit - -# # Uncomment the following lines to allow failures on nightly julia -# # (tests will run but not make your overall status red) -matrix: - allow_failures: - - julia_version: nightly - -branches: - only: - - master - - /release-.*/ - -notifications: - - provider: Email - on_build_success: false - on_build_failure: false - on_build_status_changed: false - -install: - - ps: iex ((new-object net.webclient).DownloadString("https://raw.githubusercontent.com/JuliaCI/Appveyor.jl/version-1/bin/install.ps1")) - -build_script: - - echo "%JL_BUILD_SCRIPT%" - - C:\julia\bin\julia -e "%JL_BUILD_SCRIPT%" - -test_script: - - echo "%JL_TEST_SCRIPT%" - - C:\julia\bin\julia -e "%JL_TEST_SCRIPT%" - -# # Uncomment to support code coverage upload. Should only be enabled for packages -# # which would have coverage gaps without running on Windows -# on_success: -# - echo "%JL_CODECOV_SCRIPT%" -# - C:\julia\bin\julia -e "%JL_CODECOV_SCRIPT%" diff --git a/src/color_utils.jl b/src/color_utils.jl index bade182..64a30d1 100644 --- a/src/color_utils.jl +++ b/src/color_utils.jl @@ -9,10 +9,7 @@ const _lightness_darkbg = [80.0] const _lightness_lightbg = [60.0] const _lch_c_const = [60] -function adjust_lch(color, l, c) - lch = convert(LCHab, color) - convert(RGBA{Float64}, LCHab(l, c, lch.h)) -end +adjust_lch(color, l, c) = convert(RGBA{Float64}, LCHab(l, c, convert(LCHab, color).h)) function lightness_from_background(bgcolor) bglight = convert(LCHab, bgcolor).l @@ -82,9 +79,7 @@ function darken(c, v=0.1) b = max(0, min(rgba.b - v, 1)) RGBA(r,g,b,rgba.alpha) end -function lighten(c, v=0.3) - darken(c, -v) -end +lighten(c, v=0.3) = darken(c, -v) # borrowed from http://stackoverflow.com/a/1855903: lightness_level(c::Colorant) = 0.299 * red(c) + 0.587 * green(c) + 0.114 * blue(c) @@ -93,16 +88,12 @@ isdark(c::Colorant) = lightness_level(c) < 0.5 islight(c::Colorant) = !isdark(c) -function Base.convert(::Type{RGB}, h::Unsigned) - mask = 0x0000FF - RGB([(x & mask) / 0xFF for x in (h >> 16, h >> 8, h)]...) -end +Base.convert(::Type{RGB}, h::Unsigned) = RGB([(x & 0x0000FF) / 0xFF for x in (h >> 16, h >> 8, h)]...) make255(x) = round(Int, 255 * x) -function rgb_string(c::Colorant) +rgb_string(c::Colorant) = @sprintf("rgb(%d, %d, %d)", make255(red(c)), make255(green(c)), make255(blue(c))) -end function rgba_string(c::Colorant) @sprintf( diff --git a/src/ticks.jl b/src/ticks.jl index 3a804de..ab863ae 100644 --- a/src/ticks.jl +++ b/src/ticks.jl @@ -6,24 +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::DT, base) where DT - one_dt = convert(DT, one(DT)) - +function bounding_order_of_magnitude(xspan::T, base) where T a = 1 step = 1 - while xspan < base^a * one_dt + while xspan < T(base^a) a -= step end b = 1 step = 1 - while xspan > base^b * one_dt + while xspan > T(base^b) b += step end while a + 1 < b c = div(a + b, 2) - if xspan < base^c * one_dt + if xspan < T(base^c) b = c else a = c @@ -36,6 +34,8 @@ end const float_digit_range = floor(Int,log10(floatmin())):ceil(Int,log10(floatmax())) postdecimal_digits(x) = first(i for i in float_digit_range if x==floor(x; digits=i)) +fallback_ticks(x_min::T, x_max::T, k_min, k_max) where T = collect(T, range(x_min, x_max; length=k_min)), x_min, x_max + # Empty catchall optimize_ticks() = Any[] @@ -129,12 +129,12 @@ and the variables here are: * `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)], + Q=[(1., 1.), (5., .9), (2., .7), (2.5, .5), (3., .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, 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, @@ -146,21 +146,20 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks, granularity_weight::Float64, simplicity_weight::Float64, coverage_weight::Float64, niceness_weight::Float64, strict_span, span_buffer, scale) where T - one_t = convert(T, 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 + + F = float(T) + if (xspan = x_max - x_min) < eps(F) + return fallback_ticks(x_min, x_max, k_min, k_max) end n = length(Q) is_log_scale = scale ∈ _logScales - base = get(_logScaleBases, scale, 10.) + base = F(get(_logScaleBases, scale, 10.)) # generalizing "order of magnitude" - xspan = x_max - x_min z = bounding_order_of_magnitude(xspan, base) - # find required significant digits for ticks with q*base^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)), base) q_extra_digits = maximum(postdecimal_digits(q[1]) for q in Q) @@ -174,49 +173,47 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks, ) high_score = -Inf - S_best = Array{typeof(1. * one_t)}(undef, 1) + S_best = Array{F}(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. * one_t)}(undef, Int(3k)) for k in k_min:2k_max] - else - [Array{typeof(1. * one_t)}(undef, k) for k in k_min:2k_max] - end + prealloc_Ss = [Array{F}(undef, extend_ticks ? Int(3k) : k) for k in k_min:2k_max] - while 2k_max * base^(z + 1) * one_t > xspan + while 2k_max * base^(z + 1) > xspan for (ik, k) in enumerate(k_min:2k_max) for (q, qscore) in Q - tickspan = q * base^z * one_t + tickspan = q * base^z + tickspan < eps(F) && continue span = (k - 1) * tickspan span < xspan && continue - stp = q*base^z - stp < eps() && continue + r = (x_max - span) / tickspan + isfinite(r) || continue + r = ceil(Int, r) - if is_log_scale && !isinteger(stp) - continue # prefer integer exponents for log scales + if is_log_scale && !isinteger(tickspan) + # try to favor integer exponents for log scales + nice_scale = false + qscore = 0 + else + nice_scale = true end - r = (x_max - span) / (stp * one_t) - isfinite(r) || continue - r = ceil(Int64, r) - while r*stp * one_t <= x_min + while r * tickspan <= x_min + S = prealloc_Ss[ik] # Filter or expand ticks if extend_ticks - S = prealloc_Ss[ik] 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_base(S[k + 1]) - S[2 * k] = round_base(S[2 * k]) - viewmin, viewmax = S[k + 1], S[2 * k] + S[2k] = round_base(S[2k]) + viewmin, viewmax = S[k + 1], S[2k] else - S = prealloc_Ss[ik] for i in 0:(k - 1) S[i+1] = (r + i) * tickspan end @@ -247,18 +244,17 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks, end # evaluate quality of ticks - has_zero = r <= 0 && abs(r) < k # simplicity - s = has_zero ? 1 : 0 + s = has_zero && nice_scale ? 1 : 0 # granularity g = 0 < length(S) < 2k_ideal ? 1 - abs(length(S) - k_ideal) / k_ideal : 0 # coverage effective_span = (length(S) - 1) * tickspan - c = 1.5xspan / effective_span + c = abs(effective_span) > eps(F) ? 1.5xspan / effective_span : 0 score = granularity_weight * g + simplicity_weight * s + @@ -291,14 +287,13 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks, if isinf(high_score) if strict_span - @warn("No strict ticks found") + @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, scale) else - R = typeof(1. * one_t) - return R[x_min], x_min - one_t, x_min + one_t + return fallback_ticks(x_min, x_max, k_min, k_max) end end @@ -306,14 +301,12 @@ function optimize_ticks_typed(x_min::T, x_max::T, extend_ticks, end -function optimize_ticks(x_min::Date, x_max::Date; extend_ticks::Bool=false, +optimize_ticks(x_min::Date, x_max::Date; extend_ticks::Bool=false, k_min=nothing, k_max=nothing, scale=:auto, granularity_weight=nothing, simplicity_weight=nothing, coverage_weight=nothing, niceness_weight=nothing, - strict_span=true, span_buffer = nothing) - return optimize_ticks(convert(DateTime, x_min), convert(DateTime, x_max), - extend_ticks=extend_ticks, scale=scale) -end + strict_span=true, span_buffer = nothing) = +optimize_ticks(convert(DateTime, x_min), convert(DateTime, x_max), extend_ticks=extend_ticks, scale=scale) function optimize_ticks(x_min::DateTime, x_max::DateTime; extend_ticks::Bool=false, @@ -369,27 +362,27 @@ function optimize_ticks(x_min::DateTime, x_max::DateTime; extend_ticks::Bool=fal end # round x_min down - if scale === Day(1) - first_tick = DateTime(year(x_min), month(x_min), day(x_min)) + first_tick = if scale === Day(1) + DateTime(year(x_min), month(x_min), day(x_min)) elseif scale === Hour(1) - first_tick = DateTime(year(x_min), month(x_min), day(x_min), + DateTime(year(x_min), month(x_min), day(x_min), hour(x_min)) elseif scale === Minute(1) - first_tick = DateTime(year(x_min), month(x_min), day(x_min), + DateTime(year(x_min), month(x_min), day(x_min), hour(x_min), minute(x_min)) elseif scale === Second(1) - first_tick = DateTime(year(x_min), month(x_min), day(x_min), + DateTime(year(x_min), month(x_min), day(x_min), hour(x_min), minute(x_min), second(x_min)) elseif scale === Millisecond(100) - first_tick = DateTime(year(x_min), month(x_min), day(x_min), + DateTime(year(x_min), month(x_min), day(x_min), hour(x_min), minute(x_min), second(x_min), millisecond(x_min) % 100) elseif scale === Millisecond(10) - first_tick = DateTime(year(x_min), month(x_min), day(x_min), + DateTime(year(x_min), month(x_min), day(x_min), hour(x_min), minute(x_min), second(x_min), millisecond(x_min) % 10) else - first_tick = x_min + x_min end push!(ticks, first_tick) @@ -422,27 +415,20 @@ end # Generate ticks suitable for multiple scales. -function multilevel_ticks(viewmin::T, viewmax::T; - scales=[0.5, 5.0, 10.0]) where T - +function multilevel_ticks(viewmin::T, viewmax::T; scales=[0.5, 5.0, 10.0]) where T ticks = Dict() for scale in scales ticks[scale] = optimize_ticks(viewmin, viewmax, - k_min=max(1, round(Int, 2*scale)), - k_max=max(3, round(Int, 10*scale)), - k_ideal=max(2, round(Int, 15*scale)))[1] + k_min=max(1, round(Int, 2scale)), + k_max=max(3, round(Int, 10scale)), + k_ideal=max(2, round(Int, 15scale)))[1] end - return ticks end -function multilevel_ticks(viewmin::Date, viewmax::Date; - scales=[:year, :month, :day]) - return multilevel_ticks(convert(DateTime, viewmin), - convert(DateTime, viewmax), - scales=scales) -end +multilevel_ticks(viewmin::Date, viewmax::Date; scales=[:year, :month, :day]) = + multilevel_ticks(convert(DateTime, viewmin), convert(DateTime, viewmax), scales=scales) function multilevel_ticks(viewmin::DateTime, viewmax::DateTime; @@ -451,12 +437,12 @@ function multilevel_ticks(viewmin::DateTime, viewmax::DateTime; span = convert(Float64, Dates.toms(viewmax - viewmin)) ticks = Dict() for scale in scales - if scale == :year - s = span / Dates.toms(Day(360)) + s = if scale == :year + span / Dates.toms(Day(360)) elseif scale == :month - s = span / Dates.toms(Day(90)) + span / Dates.toms(Day(90)) else - s = span / Dates.toms(Day(1)) + span / Dates.toms(Day(1)) end ticks[s/20] = optimize_ticks(viewmin, viewmax, scale=scale)[1] @@ -469,14 +455,14 @@ end # Choose "round" (full seconds/minutes/hours/days/months/years) DateTime ticks # between x_min and x_max: function optimize_datetime_ticks(a_min, a_max; k_min = 2, k_max = 4) - x_min = DateTime(Dates.UTM(Int64(round(a_min)))) - x_max = DateTime(Dates.UTM(Int64(round(a_max)))) + x_min = DateTime(Dates.UTM(Int(round(a_min)))) + x_max = DateTime(Dates.UTM(Int(round(a_max)))) Δt = x_max - x_min - if Δt > Dates.Day(365 * k_min) + if Δt > Dates.Day(365k_min) P = Dates.Year steplength = Δt / (k_max * Dates.Millisecond(Dates.Day(365))) - elseif Δt > Dates.Day(30 * k_min) + elseif Δt > Dates.Day(30k_min) P = Dates.Month steplength = Δt / (k_max * Dates.Millisecond(Dates.Day(30))) elseif Δt > Dates.Day(k_min) diff --git a/test/runtests.jl b/test/runtests.jl index c9d0e35..d81d4eb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,8 +7,6 @@ using StableRNGs rng = StableRNG(42) -# TODO: real tests - # ---------------------- # colors @@ -89,18 +87,30 @@ function is_uniformly_spaced(v; tol = 1e-6) maximum(dv) - minimum(dv) < tol * mean(abs.(dv)) end +function test_ticks(x, y, ticks) + @test issorted(ticks) + @test all(x .<= ticks .<= y) + if x < y + @test length(ticks) >= 2 + @test is_uniformly_spaced(ticks) + end +end + @testset "ticks" begin - @test optimize_ticks(-1, 2) == ([-1.0,0.0,1.0,2.0], -1.0, 2.0) - dt1, dt2 = Dates.value(DateTime(2000)), Dates.value(DateTime(2100)) - @test optimize_datetime_ticks(dt1, dt2) == ( - [63113990400000, 63902908800000, 64691827200000, 65480745600000], - ["2001-01-01", "2026-01-01", "2051-01-01", "2076-01-01"]) + @test optimize_ticks(-1, 2) == ([-1., 0., 1., 2.], -1., 2.) + + @testset "dates" begin + dt1, dt2 = Dates.value(DateTime(2000)), Dates.value(DateTime(2100)) + @test optimize_datetime_ticks(dt1, dt2) == ( + [63113990400000, 63902908800000, 64691827200000, 65480745600000], + ["2001-01-01", "2026-01-01", "2051-01-01", "2076-01-01"]) + end @testset "small range" begin @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 = PlotUtils.optimize_ticks(x, y)[1] + ticks, = PlotUtils.optimize_ticks(x, y) @test issorted(ticks) @test all(x .<= ticks .<= y) # Fails: @@ -108,18 +118,9 @@ end end end - function test_ticks(x, y, ticks) - @test issorted(ticks) - @test all(x .<= ticks .<= y) - if x < y - @test length(ticks) >= 2 - @test is_uniformly_spaced(ticks) - end - end - @testset "fixed ranges" begin @testset "fixed range $x..$y" for (x, y) in [(2, 14),(14, 25),(16, 36),(57, 69)] - test_ticks(x, y, optimize_ticks(x, y)[1]) + test_ticks(+x, +y, optimize_ticks(+x, +y)[1]) test_ticks(-y, -x, optimize_ticks(-y, -x)[1]) end end @@ -131,25 +132,52 @@ end end end - @testset "PlotUtils.jl/issues/86" begin - let x = -1.0, y = 13.0 - test_ticks(x, y, optimize_ticks(x, y, k_min = 4, k_max = 8)[1]) - end - end - @testset "digits" begin @testset "digits $((10^n) - 1)*10^$i" for n in 1:9, i in -9:9 y0 = 10^n x0 = y0 - 1 x, y = (x0, y0) .* 10.0^i - ticks = optimize_ticks(x, y)[1] + ticks, = optimize_ticks(x, y) test_ticks(x, y, ticks) end end - @testset "Plots.jl/issues/3859" begin - x, y = extrema([-1.7055509600077687e307, -1.3055509600077687e307, -1.e300]) - test_ticks(x, y, optimize_ticks(x, y, k_min = 4, k_max = 8)[1]) + @testset "types" begin + for T in (Int32, Int64, Float16, Float32, Float64) + x, y = T(1), T(10) + ticks, = optimize_ticks(x, y) + @test eltype(ticks) <: AbstractFloat + @test eltype(ticks) == (T <: AbstractFloat ? T : float(T)) + test_ticks(x, y, ticks) + end + end + + @testset "issues" begin + @testset "PlotUtils.jl/issues/86" begin + let x = -1.0, y = 13.0 + test_ticks(x, y, optimize_ticks(x, y, k_min = 4, k_max = 8)[1]) + end + end + + @testset "Plots.jl/issues/3859" begin + x, y = extrema([-1.7055509600077687e307, -1.3055509600077687e307, -1.e300]) + test_ticks(x, y, optimize_ticks(x, y, k_min = 4, k_max = 8)[1]) + end + + @testset "PlotUtils.jl/issues/114" begin + let x = -.1eps(), y = .1eps() + test_ticks(x, y, optimize_ticks(x, y)[1]) + end + end + + @testset "PlotUtils.jl/issues/116" begin + let x = 4.5, y = 5.5 + test_ticks(x, y, optimize_ticks(x, y, scale = :log10)[1]) + end + let x = 2.5, y = 3.5 + test_ticks(x, y, optimize_ticks(x, y, scale = :log2)[1]) + end + end end end