diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index be671c6c2..279235bd0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -40,9 +40,16 @@ jobs: - name: Install binary dependencies run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - name: Install Julia dependencies - run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.add(url = "https://github.com/JuliaDocs/Documenter.jl", rev = "master"); Pkg.instantiate()' + run: > + DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' + julia --project=docs -e + 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); + Pkg.add(url = "https://github.com/JuliaDocs/Documenter.jl", rev = "master"); + Pkg.instantiate()' - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} - run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs --color=yes docs/make.jl + run: > + DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' + julia --project=docs --color=yes docs/make.jl diff --git a/docs/src/makielayout/axis.md b/docs/src/makielayout/axis.md index 9254f3ebb..60fc6d410 100755 --- a/docs/src/makielayout/axis.md +++ b/docs/src/makielayout/axis.md @@ -273,6 +273,50 @@ hideydecorations!(ax3, ticks = false) f ``` +## Log scales and other axis scales + +The two attributes `xscale` and `yscale`, which by default are set to `identity`, can be used to project the data in a nonlinear way, in addition to the linear zoom that the limits provide. + +Take care that the axis limits always stay inside the limits appropriate for the chosen scaling function, for example, `log` functions fail for values `x <= 0`, `sqrt` for `x < 0`, etc. + +```@example +using CairoMakie + +data = sort(10.0 .^ randn(100)) + +f = Figure(resolution = (1000, 1000), fontsize = 14) + +for (i, scale) in enumerate([identity, log10, log2, log, sqrt]) + + row, col = fldmod1(i, 2) + Axis(f[row, col], yscale = scale, title = string(scale), + yminorticksvisible = true, yminorgridvisible = true, + yminorticks = IntervalsBetween(8)) + + lines!(data, color = :blue) + +end + +f +``` + +Some plotting functions, like barplots or density plots, have offset parameters which are usually zero, which you have to set to some non-zero value explicitly so they work in `log` axes. + +```@example +using CairoMakie + +processors = ["VAX-11/780", "Sun-4/260", "PowerPC 604", + "Alpha 21164", "Intel Pentium III", "Intel Xeon"] +relative_speeds = [1, 9, 117, 280, 1779, 6505] + +barplot(relative_speeds, fillto = 0.5, + axis = (yscale = log10, ylabel ="relative speed", + xticks = (1:6, processors), xticklabelrotation = pi/8)) + +ylims!(0.5, 10000) +current_figure() +``` + ## Controlling Axis aspect ratios If you're plotting images, you might want to force a specific aspect ratio diff --git a/src/layouting/data_limits.jl b/src/layouting/data_limits.jl index 1a443352d..bad06fd3c 100644 --- a/src/layouting/data_limits.jl +++ b/src/layouting/data_limits.jl @@ -14,15 +14,15 @@ Data limits calculate a minimal boundingbox from the data points in a plot. This doesn't include any transformations, markers etc. """ function atomic_limits(x::Atomic{<: Tuple{Arg1}}) where Arg1 - return xyz_boundingbox(transform_func(x), to_value(x[1])) + return xyz_boundingbox(identity, to_value(x[1])) end function atomic_limits(x::Atomic{<: Tuple{X, Y, Z}}) where {X, Y, Z} - return xyz_boundingbox(transform_func(x), to_value.(x[1:3])...) + return xyz_boundingbox(identity, to_value.(x[1:3])...) end function atomic_limits(x::Atomic{<: Tuple{X, Y}}) where {X, Y} - return xyz_boundingbox(transform_func(x), to_value.(x[1:2])...) + return xyz_boundingbox(identity, to_value.(x[1:2])...) end _isfinite(x) = isfinite(x) @@ -91,13 +91,13 @@ end const ImageLike{Arg} = Union{Heatmap{Arg}, Image{Arg}} function data_limits(x::ImageLike{<: Tuple{X, Y, Z}}) where {X, Y, Z} - xyz_boundingbox(transform_func(x), to_value.((x[1], x[2]))...) + xyz_boundingbox(identity, to_value.((x[1], x[2]))...) end function data_limits(x::Volume) _to_interval(r) = ((lo, hi) = extrema(r); lo..hi) axes = (x[1], x[2], x[3]) - xyz_boundingbox(transform_func(x), _to_interval.(to_value.(axes))...) + xyz_boundingbox(identity, _to_interval.(to_value.(axes))...) end function text_limits(x::VecTypes) diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index dc670123c..8cf072889 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -209,16 +209,37 @@ transform_func_obs(x) = transformation(x).transform_func apply_transform(f, data) Apply the data transform func to the data """ -apply_transform(f::typeof(identity), position::Number) = position -apply_transform(f::typeof(identity), positions::AbstractArray) = positions -apply_transform(f::typeof(identity), positions::AbstractVector) = positions -apply_transform(f::typeof(identity), position::VecTypes) = position +apply_transform(f::typeof(identity), x) = x +# these are all ambiguity fixes +apply_transform(f::typeof(identity), x::AbstractArray) = x +apply_transform(f::typeof(identity), x::VecTypes) = x +apply_transform(f::typeof(identity), x::Number) = x +apply_transform(f::typeof(identity), x::ClosedInterval) = x + +apply_transform(f::NTuple{2, typeof(identity)}, x) = x +apply_transform(f::NTuple{2, typeof(identity)}, x::AbstractArray) = x +apply_transform(f::NTuple{2, typeof(identity)}, x::VecTypes) = x +apply_transform(f::NTuple{2, typeof(identity)}, x::Number) = x +apply_transform(f::NTuple{2, typeof(identity)}, x::ClosedInterval) = x + +apply_transform(f::NTuple{3, typeof(identity)}, x) = x +apply_transform(f::NTuple{3, typeof(identity)}, x::AbstractArray) = x +apply_transform(f::NTuple{3, typeof(identity)}, x::VecTypes) = x +apply_transform(f::NTuple{3, typeof(identity)}, x::Number) = x +apply_transform(f::NTuple{3, typeof(identity)}, x::ClosedInterval) = x + struct PointTrans{N, F} f::F + function PointTrans{N}(f::F) where {N, F} + if !hasmethod(f, Tuple{Point{N}}) + error("PointTrans with parameter N = $N must be applicable to an argument of type Point{$N}.") + end + new{N, F}(f) + end end -PointTrans{N}(func::F) where {N, F} = PointTrans{N, F}(func) +# PointTrans{N}(func::F) where {N, F} = PointTrans{N, F}(func) Base.broadcastable(x::PointTrans) = (x,) function apply_transform(f::PointTrans{N}, point::Point{N}) where N @@ -236,25 +257,38 @@ function apply_transform(f::PointTrans{N1}, point::Point{N2}) where {N1, N2} end end - - function apply_transform(f, data::AbstractArray) - return map(point-> apply_transform(f, point), data) + map(point-> apply_transform(f, point), data) end -function apply_transform(f::NTuple{N, Any}, point::VecTypes{N}) where {N, T} - return Point{N, Float32}(ntuple(i-> apply_transform(f[i], point[i]), N)) +function apply_transform(f::Tuple{Any, Any}, point::VecTypes{2}) + Point2{Float32}( + f[1](point[1]), + f[2](point[2]), + ) end +# ambiguity fix +apply_transform(f::NTuple{2, typeof(identity)}, point::VecTypes{2}) = point -apply_transform(f, number::Number) = f(number) -function apply_transform(f::Union{typeof(log), typeof(log10), typeof(log2)}, number::Number) - if number <= 0.0 - return 0.0 - else - return f(number) - end +function apply_transform(f::Tuple{Any, Any}, point::VecTypes{3}) + apply_transform((f..., identity), point) end +# ambiguity fix +apply_transform(f::NTuple{2, typeof(identity)}, point::VecTypes{3}) = point + +function apply_transform(f::Tuple{Any, Any, Any}, point::VecTypes{3}) + Point3{Float32}( + f[1](point[1]), + f[2](point[2]), + f[3](point[3]), + ) +end +# ambiguity fix +apply_transform(f::NTuple{3, typeof(identity)}, point::VecTypes{3}) = point + + +apply_transform(f, number::Number) = f(number) function apply_transform(f::Observable, data::Observable) return lift((f, d)-> apply_transform(f, d), f, data) @@ -265,3 +299,23 @@ function apply_transform(f, itr::ClosedInterval) mini, maxi = extrema(itr) return apply_transform(f, mini) .. apply_transform(f, maxi) end + + +function apply_transform(f, r::Rect) + mi = minimum(r) + ma = maximum(r) + mi_t = apply_transform(f, mi) + ma_t = apply_transform(f, ma) + Rect(Vec(mi_t), Vec(ma_t .- mi_t)) +end +# ambiguity fix +apply_transform(f::typeof(identity), r::Rect) = r +apply_transform(f::NTuple{2, typeof(identity)}, r::Rect) = r +apply_transform(f::NTuple{3, typeof(identity)}, r::Rect) = r + +inverse_transform(::typeof(identity)) = identity +inverse_transform(::typeof(log10)) = exp10 +inverse_transform(::typeof(log)) = exp +inverse_transform(::typeof(log2)) = exp2 +inverse_transform(::typeof(sqrt)) = x -> x ^ 2 +inverse_transform(F::Tuple) = map(inverse_transform, F) \ No newline at end of file diff --git a/src/makielayout/defaultattributes.jl b/src/makielayout/defaultattributes.jl index 128a976b8..5b2fac192 100644 --- a/src/makielayout/defaultattributes.jl +++ b/src/makielayout/defaultattributes.jl @@ -184,11 +184,11 @@ function default_attributes(::Type{Axis}, scene) "The relative margins added to the autolimits in y direction." yautolimitmargin = (0.05f0, 0.05f0) "The xticks." - xticks = LinearTicks(4) + xticks = AbstractPlotting.automatic "Format for xticks." xtickformat = AbstractPlotting.automatic "The yticks." - yticks = LinearTicks(4) + yticks = AbstractPlotting.automatic "Format for yticks." ytickformat = AbstractPlotting.automatic "The button for panning." @@ -215,8 +215,6 @@ function default_attributes(::Type{Axis}, scene) flip_ylabel = false "Constrains the data aspect ratio (`nothing` leaves the ratio unconstrained)." autolimitaspect = nothing - "The limits that the axis tries to set given other constraints like aspect. Don't set this directly, use `xlims!`, `ylims!` or `limits!` instead." - targetlimits = BBox(0, 100, 0, 100) "The limits that the user has manually set. They are reinstated when calling `reset_limits!` and are set to nothing by `autolimits!`. Can be either a tuple (xlow, xhigh, ylow, high) or a tuple (nothing_or_xlims, nothing_or_ylims). Are set by `xlims!`, `ylims!` and `limits!`." limits = (nothing, nothing) "The align mode of the axis in its parent GridLayout." @@ -249,6 +247,10 @@ function default_attributes(::Type{Axis}, scene) yminortickcolor = :black "The tick locator for the y minor ticks" yminorticks = IntervalsBetween(2) + "The x axis scale" + xscale = identity + "The y axis scale" + yscale = identity end (attributes = attrs, documentation = docdict, defaults = defaultdict) @@ -291,7 +293,7 @@ function default_attributes(::Type{Colorbar}, scene) "Controls if the tick marks are visible." ticksvisible = true "The ticks." - ticks = LinearTicks(4) + ticks = AbstractPlotting.automatic "Format for ticks." tickformat = AbstractPlotting.automatic "The space reserved for the tick labels." @@ -336,10 +338,10 @@ function default_attributes(::Type{Colorbar}, scene) flipaxis = true "Flips the colorbar label if the axis is vertical." flip_vertical_label = false - "The width setting of the colorbar." - width = nothing + "The width setting of the colorbar. Use `size` to set width or height relative to colorbar orientation instead." + width = AbstractPlotting.automatic "The height setting of the colorbar." - height = nothing + height = AbstractPlotting.automatic "Controls if the parent layout can adjust to this element's width" tellwidth = true "Controls if the parent layout can adjust to this element's height" @@ -368,6 +370,10 @@ function default_attributes(::Type{Colorbar}, scene) minortickcolor = :black "The tick locator for the minor ticks" minorticks = IntervalsBetween(5) + "The axis scale" + scale = identity + "The width or height of the colorbar, depending on if it's vertical or horizontal, unless overridden by `width` / `height`" + size = 20 end (attributes = attrs, documentation = docdict, defaults = defaultdict) end @@ -570,6 +576,7 @@ function default_attributes(::Type{LineAxis}) minortickwidth = 1f0, minortickcolor = :black, minorticks = AbstractPlotting.automatic, + scale = identity, ) end diff --git a/src/makielayout/interactions.jl b/src/makielayout/interactions.jl index 6cdca0c9f..e223dd06c 100644 --- a/src/makielayout/interactions.jl +++ b/src/makielayout/interactions.jl @@ -140,9 +140,18 @@ end function process_interaction(r::RectangleZoom, event::MouseEvent, ax::Axis) + # TODO: actually, the data from the mouse event should be transformed already + # but the problem is that these mouse events are generated all the time + # and outside of log axes, you would quickly run into domain errors + transf = AbstractPlotting.transform_func(ax) + inv_transf = AbstractPlotting.inverse_transform(transf) + if event.type === MouseEventTypes.leftdragstart - r.from = event.prev_data - r.to = event.data + data = AbstractPlotting.apply_transform(inv_transf, event.data) + prev_data = AbstractPlotting.apply_transform(inv_transf, event.prev_data) + + r.from = prev_data + r.to = data r.rectnode[] = _chosen_limits(r, ax) selection_vertices = lift(_selection_vertices, ax.finallimits, r.rectnode) @@ -161,7 +170,11 @@ function process_interaction(r::RectangleZoom, event::MouseEvent, ax::Axis) r.active = true elseif event.type === MouseEventTypes.leftdrag - r.to = event.data + # clamp mouse data to shown limits + rect = AbstractPlotting.apply_transform(transf, ax.finallimits[]) + data = AbstractPlotting.apply_transform(inv_transf, rectclamp(event.data, rect)) + + r.to = data r.rectnode[] = _chosen_limits(r, ax) elseif event.type === MouseEventTypes.leftdragstop @@ -182,6 +195,12 @@ function process_interaction(r::RectangleZoom, event::MouseEvent, ax::Axis) return nothing end +function rectclamp(p::Point, r::Rect) + map(p, minimum(r), maximum(r)) do pp, mi, ma + clamp(pp, mi, ma) + end |> Point +end + function process_interaction(r::RectangleZoom, event::KeysEvent, ax::Axis) r.restrict_y = Keyboard.x in event.keys r.restrict_x = Keyboard.y in event.keys @@ -248,11 +267,17 @@ function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::Axis) # now to 0..1 0.5 .+ 0.5 - xorigin = tlimits[].origin[1] - yorigin = tlimits[].origin[2] + xscale = ax.xscale[] + yscale = ax.yscale[] - xwidth = tlimits[].widths[1] - ywidth = tlimits[].widths[2] + transf = (xscale, yscale) + tlimits_trans = AbstractPlotting.apply_transform(transf, tlimits[]) + + xorigin = tlimits_trans.origin[1] + yorigin = tlimits_trans.origin[2] + + xwidth = tlimits_trans.widths[1] + ywidth = tlimits_trans.widths[2] newxwidth = xzoomlock[] ? xwidth : xwidth * z newywidth = yzoomlock[] ? ywidth : ywidth * z @@ -262,7 +287,7 @@ function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::Axis) timed_ticklabelspace_reset(ax, s.reset_timer, s.prev_xticklabelspace, s.prev_yticklabelspace, s.reset_delay) - tlimits[] = if ispressed(scene, xzoomkey[]) + newrect_trans = if ispressed(scene, xzoomkey[]) FRect(newxorigin, yorigin, newxwidth, ywidth) elseif ispressed(scene, yzoomkey[]) FRect(xorigin, newyorigin, xwidth, newywidth) @@ -270,6 +295,8 @@ function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::Axis) FRect(newxorigin, newyorigin, newxwidth, newywidth) end + inv_transf = AbstractPlotting.inverse_transform(transf) + tlimits[] = AbstractPlotting.apply_transform(inv_transf, newrect_trans) end end @@ -287,23 +314,50 @@ function process_interaction(dp::DragPan, event::MouseEvent, ax) panbutton = ax.panbutton scene = ax.scene + cam = camera(scene) + pa = pixelarea(scene)[] + + mp_axscene = Vec4f0((event.px .- pa.origin)..., 0, 1) + mp_axscene_prev = Vec4f0((event.prev_px .- pa.origin)..., 0, 1) + + mp_axfraction, mp_axfraction_prev = map((mp_axscene, mp_axscene_prev)) do mp + # first to normal -1..1 space + (cam.pixel_space[] * mp)[1:2] .* + # now to 1..-1 if an axis is reversed to correct zoom point + (-2 .* ((ax.xreversed[], ax.yreversed[])) .+ 1) .* + # now to 0..1 + 0.5 .+ 0.5 + end + + xscale = ax.xscale[] + yscale = ax.yscale[] + + transf = (xscale, yscale) + tlimits_trans = AbstractPlotting.apply_transform(transf, tlimits[]) + + movement_frac = mp_axfraction .- mp_axfraction_prev + + xscale = ax.xscale[] + yscale = ax.yscale[] - movement = AbstractPlotting.to_world(ax.scene, event.px) .- - AbstractPlotting.to_world(ax.scene, event.prev_px) + transf = (xscale, yscale) + tlimits_trans = AbstractPlotting.apply_transform(transf, tlimits[]) - xori, yori = Vec2f0(tlimits[].origin) .- movement + xori, yori = tlimits_trans.origin .- movement_frac .* widths(tlimits_trans) if xpanlock[] || ispressed(scene, ypankey[]) - xori = tlimits[].origin[1] + xori = tlimits_trans.origin[1] end if ypanlock[] || ispressed(scene, xpankey[]) - yori = tlimits[].origin[2] + yori = tlimits_trans.origin[2] end timed_ticklabelspace_reset(ax, dp.reset_timer, dp.prev_xticklabelspace, dp.prev_yticklabelspace, dp.reset_delay) - tlimits[] = FRect(Vec2f0(xori, yori), widths(tlimits[])) + inv_transf = AbstractPlotting.inverse_transform(transf) + newrect_trans = FRect(Vec2f0(xori, yori), widths(tlimits_trans)) + tlimits[] = AbstractPlotting.apply_transform(inv_transf, newrect_trans) return nothing end diff --git a/src/makielayout/layoutables/axis.jl b/src/makielayout/layoutables/axis.jl index 63daafa92..b51f5ee5e 100644 --- a/src/makielayout/layoutables/axis.jl +++ b/src/makielayout/layoutables/axis.jl @@ -47,7 +47,20 @@ function layoutable(::Type{<:Axis}, fig_or_scene::Union{Figure, Scene}; bbox = n layoutobservables = LayoutObservables{Axis}(attrs.width, attrs.height, attrs.tellwidth, attrs.tellheight, halign, valign, attrs.alignmode; suggestedbbox = bbox, protrusions = protrusions) - finallimits = Node(FRect(0, 0, 100, 100)) + # initialize either with user limits, or pick defaults based on scales + # so that we don't immediately error + targetlimits = Node{FRect2D}(defaultlimits(limits[], attrs.xscale[], attrs.yscale[])) + finallimits = Node{FRect2D}(targetlimits[]) + + onany(targetlimits, attrs.xscale, attrs.yscale) do lims, xsc, ysc + # this should validate the targetlimits before anything else happens with them + # so there should be nothing before this lifting `targetlimits` + # we don't use finallimits because that's one step later and you + # already shouldn't set invalid targetlimits (even if they could + # theoretically be adjusted to fit somehow later?) + # and this way we can error pretty early + validate_limits_for_scales(lims, xsc, ysc) + end scenearea = sceneareanode!(layoutobservables.computedbbox, finallimits, aspect) @@ -100,7 +113,7 @@ function layoutable(::Type{<:Axis}, fig_or_scene::Union{Figure, Scene}; bbox = n translate!(yminorgridlines, 0, 0, -10) decorations[:yminorgridlines] = yminorgridlines - onany(finallimits, xreversed, yreversed) do lims, xrev, yrev + onany(finallimits, xreversed, yreversed, attrs.xscale, attrs.yscale) do lims, xrev, yrev, xsc, ysc nearclip = -10_000f0 farclip = 10_000f0 @@ -112,11 +125,18 @@ function layoutable(::Type{<:Axis}, fig_or_scene::Union{Figure, Scene}; bbox = n bottomtop = yrev ? (top, bottom) : (bottom, top) projection = AbstractPlotting.orthographicprojection( - leftright..., bottomtop..., nearclip, farclip) + xsc.(leftright)..., + ysc.(bottomtop)..., nearclip, farclip) camera(scene).projection[] = projection camera(scene).projectionview[] = projection end + onany(attrs.xscale, attrs.yscale) do xsc, ysc + scene.transformation.transform_func[] = (xsc, ysc) + end + + notify(attrs.xscale) + xaxis_endpoints = lift(xaxisposition, scene.px_area) do xaxisposition, area if xaxisposition == :bottom bottomline(FRect2D(area)) @@ -174,7 +194,7 @@ function layoutable(::Type{<:Axis}, fig_or_scene::Union{Figure, Scene}; bbox = n ticksvisible = xticksvisible, spinevisible = xspinevisible, spinecolor = xspinecolor, spinewidth = spinewidth, ticklabelsize = xticklabelsize, trimspine = xtrimspine, ticksize = xticksize, reversed = xreversed, tickwidth = xtickwidth, tickcolor = xtickcolor, - minorticksvisible = xminorticksvisible, minortickalign = xminortickalign, minorticksize = xminorticksize, minortickwidth = xminortickwidth, minortickcolor = xminortickcolor, minorticks = xminorticks, + minorticksvisible = xminorticksvisible, minortickalign = xminortickalign, minorticksize = xminorticksize, minortickwidth = xminortickwidth, minortickcolor = xminortickcolor, minorticks = xminorticks, scale = attrs.xscale, ) decorations[:xaxis] = xaxis @@ -187,7 +207,7 @@ function layoutable(::Type{<:Axis}, fig_or_scene::Union{Figure, Scene}; bbox = n ticksvisible = yticksvisible, spinevisible = yspinevisible, spinecolor = yspinecolor, spinewidth = spinewidth, trimspine = ytrimspine, ticklabelsize = yticklabelsize, ticksize = yticksize, flip_vertical_label = flip_ylabel, reversed = yreversed, tickwidth = ytickwidth, tickcolor = ytickcolor, - minorticksvisible = yminorticksvisible, minortickalign = yminortickalign, minorticksize = yminorticksize, minortickwidth = yminortickwidth, minortickcolor = yminortickcolor, minorticks = yminorticks, + minorticksvisible = yminorticksvisible, minortickalign = yminortickalign, minorticksize = yminorticksize, minortickwidth = yminortickwidth, minortickcolor = yminortickcolor, minorticks = yminorticks, scale = attrs.yscale, ) decorations[:yaxis] = yaxis @@ -355,7 +375,7 @@ function layoutable(::Type{<:Axis}, fig_or_scene::Union{Figure, Scene}; bbox = n interactions = Dict{Symbol, Tuple{Bool, Any}}() ax = Axis(fig_or_scene, layoutobservables, attrs, decorations, scene, - xaxislinks, yaxislinks, finallimits, block_limit_linking, + xaxislinks, yaxislinks, targetlimits, finallimits, block_limit_linking, mouseeventhandle, scrollevents, keysevents, interactions) function process_event(event) @@ -385,17 +405,19 @@ function layoutable(::Type{<:Axis}, fig_or_scene::Union{Figure, Scene}; bbox = n DragPan(Ref{Any}(nothing), Ref{Any}(0), Ref{Any}(0), 0.2)) + # these are the user defined limits on(limits) do mlims reset_limits!(ax) end - on(attrs.targetlimits) do tlims + # these are the limits that we try to target, but they can be changed for correct aspects + on(targetlimits) do tlims update_linked_limits!(block_limit_linking, xaxislinks, yaxislinks, tlims) end # compute limits that adhere to the limit aspect ratio whenever the targeted # limits or the scene size change, because both influence the displayed ratio - onany(scene.px_area, ax.targetlimits) do pxa, lims + onany(scene.px_area, targetlimits) do pxa, lims adjustlimits!(ax) end @@ -419,6 +441,7 @@ function reset_limits!(ax; xauto = true, yauto = true) xlims = if isnothing(mxlims) if xauto xautolimits(ax) + xautolimits(ax) else left(ax.targetlimits[]), right(ax.targetlimits[]) end @@ -434,8 +457,13 @@ function reset_limits!(ax; xauto = true, yauto = true) else convert(Tuple{Float32, Float32}, mylims) end - @assert xlims[1] <= xlims[2] - @assert ylims[1] <= ylims[2] + if !(xlims[1] <= xlims[2]) + error("Invalid x-limits as xlims[1] <= xlims[2] is not met for $xlims.") + end + if !(ylims[1] <= ylims[2]) + error("Invalid y-limits as ylims[1] <= ylims[2] is not met for $ylims.") + end + ax.targetlimits[] = BBox(xlims..., ylims...) nothing end @@ -450,6 +478,25 @@ function convert_limit_attribute(lims::Tuple{Any, Any}) end can_be_current_axis(ax::Axis) = true +function validate_limits_for_scales(lims::Rect, xsc, ysc) + mi = minimum(lims) + ma = maximum(lims) + xlims = (mi[1], ma[1]) + ylims = (mi[2], ma[2]) + + if !validate_limits_for_scale(xlims, xsc) + error("Invalid x-limits $xlims for scale $xsc") + end + if !validate_limits_for_scale(ylims, ysc) + error("Invalid y-limits $ylims for scale $ysc") + end + nothing +end + +validate_limits_for_scale(lims, ::typeof(identity)) = true +validate_limits_for_scale(lims, ::Union{typeof(log2), typeof(log10), typeof(log)}) = all(>(0), lims) +validate_limits_for_scale(lims, ::typeof(sqrt)) = all(>=(0), lims) + function AbstractPlotting.plot!( la::Axis, P::AbstractPlotting.PlotFunc, attributes::AbstractPlotting.Attributes, args...; @@ -497,12 +544,16 @@ function limitunion(lims1, lims2) (min(lims1..., lims2...), max(lims1..., lims2...)) end -function expandlimits(lims, marginleft, marginright) +function expandlimits(lims, margin_low, margin_high, scale) + # expand limits so that the margins are applied at the current axis scale limsordered = (min(lims[1], lims[2]), max(lims[1], lims[2])) - w = limsordered[2] - limsordered[1] - dleft = w * marginleft - dright = w * marginright - lims = (limsordered[1] - dleft, limsordered[2] + dright) + lims_scaled = scale.(limsordered) + + w_scaled = lims_scaled[2] - lims_scaled[1] + d_low_scaled = w_scaled * margin_low + d_high_scaled = w_scaled * margin_high + inverse = AbstractPlotting.inverse_transform(scale) + lims = inverse.((lims_scaled[1] - d_low_scaled, lims_scaled[2] + d_high_scaled)) # guard against singular limits from something like a vline or hline if lims[2] - lims[1] == 0 @@ -513,6 +564,8 @@ end function getlimits(la::Axis, dim) + # find all plots that don't have exclusion attributes set + # for this dimension plots_with_autolimits = if dim == 1 filter(p -> !haskey(p.attributes, :xautolimits) || p.attributes.xautolimits[], la.scene.plots) elseif dim == 2 @@ -521,17 +574,24 @@ function getlimits(la::Axis, dim) error("Dimension $dim not allowed. Only 1 or 2.") end + # only use visible plots for limits visible_plots = filter( p -> !haskey(p.attributes, :visible) || p.attributes.visible[], plots_with_autolimits) + # get all data limits bboxes = [FRect2D(AbstractPlotting.data_limits(p)) for p in visible_plots] + + # filter out bboxes that are invalid somehow finite_bboxes = filter(AbstractPlotting.isfinite_rect, bboxes) + # if there are no bboxes remaining, `nothing` signals that no limits could be determined isempty(finite_bboxes) && return nothing + # otherwise start with the first box templim = (finite_bboxes[1].origin[dim], finite_bboxes[1].origin[dim] + finite_bboxes[1].widths[dim]) + # and union all other limits with it for bb in finite_bboxes[2:end] templim = limitunion(templim, (bb.origin[dim], bb.origin[dim] + bb.widths[dim])) end @@ -601,7 +661,18 @@ function autolimits!(ax::Axis) end function xautolimits(ax::Axis) + # try getting x limits for the axis and then union them with linked axes xlims = getxlimits(ax) + # but if we have limits, then expand them with the auto margins + if !isnothing(xlims) + if !validate_limits_for_scale(xlims, ax.xscale[]) + error("Found invalid x-limits $xlims for scale $(ax.xscale[])") + end + xlims = expandlimits(xlims, + ax.attributes.xautolimitmargin[][1], + ax.attributes.xautolimitmargin[][2], + ax.xscale[]) + end for link in ax.xaxislinks if isnothing(xlims) xlims = getxlimits(link) @@ -612,18 +683,26 @@ function xautolimits(ax::Axis) end end end + # if no limits have been found, use the targetlimits directly if isnothing(xlims) xlims = (ax.targetlimits[].origin[1], ax.targetlimits[].origin[1] + ax.targetlimits[].widths[1]) - else - xlims = expandlimits(xlims, - ax.attributes.xautolimitmargin[][1], - ax.attributes.xautolimitmargin[][2]) end xlims end function yautolimits(ax) + # try getting y limits for the axis and then union them with linked axes ylims = getylimits(ax) + # but if we have limits, then expand them with the auto margins + if !isnothing(ylims) + if !validate_limits_for_scale(ylims, ax.yscale[]) + error("Found invalid direct y-limits $ylims for scale $(ax.yscale[])") + end + ylims = expandlimits(ylims, + ax.attributes.yautolimitmargin[][1], + ax.attributes.yautolimitmargin[][2], + ax.yscale[]) + end for link in ax.yaxislinks if isnothing(ylims) ylims = getylimits(link) @@ -634,13 +713,11 @@ function yautolimits(ax) end end end + # if no limits have been found, use the targetlimits directly if isnothing(ylims) - ylims = (ax.targetlimits[].origin[2], ax.targetlimits[].origin[2] + ax.targetlimits[].widths[2]) - else - ylims = expandlimits(ylims, - ax.attributes.yautolimitmargin[][1], - ax.attributes.yautolimitmargin[][2]) + ylims = (ax.targetlimits[].origin[2], ax.targetlimits[].origin[2] + ax.targetlimits[].widths[2]) end + ylims end """ @@ -658,6 +735,7 @@ function adjustlimits!(la) asp = la.autolimitaspect[] target = la.targetlimits[] + # in the simplest case, just update the final limits with the target limits if isnothing(asp) la.finallimits[] = target return @@ -684,7 +762,7 @@ function adjustlimits!(la) (la.xautolimitmargin[] ./ marginsum) end - xlims = expandlimits(xlims, ((correction_factor - 1) .* ratios)...) + xlims = expandlimits(xlims, ((correction_factor - 1) .* ratios)..., identity) # don't use scale here? elseif correction_factor < 1 # need to go taller @@ -694,7 +772,7 @@ function adjustlimits!(la) else (la.yautolimitmargin[] ./ marginsum) end - ylims = expandlimits(ylims, (((1 / correction_factor) - 1) .* ratios)...) + ylims = expandlimits(ylims, (((1 / correction_factor) - 1) .* ratios)..., identity) # don't use scale here? end bbox = BBox(xlims[1], xlims[2], ylims[1], ylims[2]) @@ -996,3 +1074,24 @@ function Base.empty!(ax::Axis) end ax end + +AbstractPlotting.transform_func(ax::Axis) = AbstractPlotting.transform_func(ax.scene) + + +# these functions pick limits for different x and y scales, so that +# we don't pick values that are invalid, such as 0 for log etc. +function defaultlimits(userlimits::Tuple{Any, Any, Any, Any}, xscale, yscale) + BBox(userlimits...) +end + +function defaultlimits(userlimits::Tuple{Any, Any}, xscale, yscale) + xl = isnothing(userlimits[1]) ? defaultlimits(xscale) : userlimits[1] + yl = isnothing(userlimits[2]) ? defaultlimits(yscale) : userlimits[2] + BBox(xl..., yl...) +end + +defaultlimits(::typeof(log10)) = (1.0, 1000.0) +defaultlimits(::typeof(log2)) = (1.0, 8.0) +defaultlimits(::typeof(log)) = (1.0, exp(3.0)) +defaultlimits(::typeof(identity)) = (0.0, 10.0) +defaultlimits(::typeof(sqrt)) = (0.0, 100.0) \ No newline at end of file diff --git a/src/makielayout/layoutables/axis3d.jl b/src/makielayout/layoutables/axis3d.jl index 549102cc8..3ce67b461 100644 --- a/src/makielayout/layoutables/axis3d.jl +++ b/src/makielayout/layoutables/axis3d.jl @@ -43,15 +43,15 @@ function layoutable(::Type{<:Axis3}, fig_or_scene::Union{Figure, Scene}; bbox = end ticknode_1 = lift(finallimits, attrs.xticks, attrs.xtickformat) do lims, ticks, format - tl = get_ticks(ticks, format, minimum(lims)[1], maximum(lims)[1]) + tl = get_ticks(ticks, identity, format, minimum(lims)[1], maximum(lims)[1]) end ticknode_2 = lift(finallimits, attrs.yticks, attrs.ytickformat) do lims, ticks, format - tl = get_ticks(ticks, format, minimum(lims)[2], maximum(lims)[2]) + tl = get_ticks(ticks, identity, format, minimum(lims)[2], maximum(lims)[2]) end ticknode_3 = lift(finallimits, attrs.zticks, attrs.ztickformat) do lims, ticks, format - tl = get_ticks(ticks, format, minimum(lims)[3], maximum(lims)[3]) + tl = get_ticks(ticks, identity, format, minimum(lims)[3], maximum(lims)[3]) end mi1 = @lift(!(pi/2 <= $azimuth % 2pi < 3pi/2)) @@ -814,7 +814,8 @@ function xautolimits(ax::Axis3) else xlims = expandlimits(xlims, ax.attributes.xautolimitmargin[][1], - ax.attributes.xautolimitmargin[][2]) + ax.attributes.xautolimitmargin[][2], + identity) end xlims end @@ -827,7 +828,8 @@ function yautolimits(ax::Axis3) else ylims = expandlimits(ylims, ax.attributes.yautolimitmargin[][1], - ax.attributes.yautolimitmargin[][2]) + ax.attributes.yautolimitmargin[][2], + identity) end ylims end @@ -840,7 +842,8 @@ function zautolimits(ax::Axis3) else zlims = expandlimits(zlims, ax.attributes.zautolimitmargin[][1], - ax.attributes.zautolimitmargin[][2]) + ax.attributes.zautolimitmargin[][2], + identity) end zlims end \ No newline at end of file diff --git a/src/makielayout/layoutables/colorbar.jl b/src/makielayout/layoutables/colorbar.jl index 001ec94b9..61367ebda 100644 --- a/src/makielayout/layoutables/colorbar.jl +++ b/src/makielayout/layoutables/colorbar.jl @@ -73,12 +73,23 @@ function layoutable(::Type{<:Colorbar}, fig_or_scene; bbox = nothing, kwargs...) leftspinecolor, rightspinecolor, bottomspinecolor, colormap, limits, halign, valign, vertical, flipaxis, ticklabelalign, flip_vertical_label, nsteps, highclip, lowclip, - minorticksvisible, minortickalign, minorticksize, minortickwidth, minortickcolor, minorticks) + minorticksvisible, minortickalign, minorticksize, minortickwidth, minortickcolor, minorticks, scale) decorations = Dict{Symbol, Any}() protrusions = Node(GridLayoutBase.RectSides{Float32}(0, 0, 0, 0)) - layoutobservables = LayoutObservables{Colorbar}(attrs.width, attrs.height, attrs.tellwidth, attrs.tellheight, + + # make the layout width and height settings depend on `size` if they are set to automatic + # and determine whether they are nothing or `size` depending on colorbar orientation + _width = lift(attrs.size, attrs.width, vertical, typ = Any) do sz, w, v + w === AbstractPlotting.automatic ? (v ? sz : nothing) : w + end + + _height = lift(attrs.size, attrs.height, vertical, typ = Any) do sz, h, v + h === AbstractPlotting.automatic ? (v ? nothing : sz) : h + end + + layoutobservables = LayoutObservables{Colorbar}(_width, _height, attrs.tellwidth, attrs.tellheight, halign, valign, attrs.alignmode; suggestedbbox = bbox, protrusions = protrusions) framebox = @lift(round_to_IRect2D($(layoutobservables.computedbbox))) @@ -131,7 +142,7 @@ function layoutable(::Type{<:Colorbar}, fig_or_scene; bbox = nothing, kwargs...) map_is_categorical = lift(x -> x isa PlotUtils.CategoricalColorGradient, cgradient) steps = lift(cgradient, nsteps) do cgradient, n - if cgradient isa PlotUtils.CategoricalColorGradient + s = if cgradient isa PlotUtils.CategoricalColorGradient cgradient.values else collect(LinRange(0, 1, n)) @@ -149,16 +160,22 @@ function layoutable(::Type{<:Colorbar}, fig_or_scene; bbox = nothing, kwargs...) # for categorical colormaps we make a number of rectangle polys - rects_and_colors = lift(barbox, vertical, steps, cgradient) do bbox, v, steps, gradient + rects_and_colors = lift(barbox, vertical, steps, cgradient, scale, limits) do bbox, v, steps, gradient, scale, lims + + # we need to convert the 0 to 1 steps into rescaled 0 to 1 steps given the + # colormap's `scale` attribute + + s_scaled = scaled_steps(steps, scale, lims) + xmin, ymin = minimum(bbox) xmax, ymax = maximum(bbox) rects = if v - yvals = steps .* (ymax - ymin) .+ ymin + yvals = s_scaled .* (ymax - ymin) .+ ymin [BBox(xmin, xmax, b, t) for (b, t) in zip(yvals[1:end-1], yvals[2:end])] else - xvals = steps .* (xmax - xmin) .+ xmin + xvals = s_scaled .* (xmax - xmin) .+ xmin [BBox(l, r, ymin, ymax) for (l, r) in zip(xvals[1:end-1], xvals[2:end])] end @@ -182,8 +199,10 @@ function layoutable(::Type{<:Colorbar}, fig_or_scene; bbox = nothing, kwargs...) # for continous colormaps we sample a 1d image # to avoid white lines when rendering vector graphics - continous_pixels = lift(vertical, nsteps, cgradient) do v, n, grad - px = get.(Ref(grad), LinRange(0, 1, n)) + continous_pixels = lift(vertical, nsteps, cgradient, limits, scale) do v, n, grad, lims, scale + + s_steps = scaled_steps(LinRange(0, 1, n), scale, lims) + px = get.(Ref(grad), s_steps) v ? reshape(px, 1, n) : reshape(px, n, 1) end @@ -312,7 +331,7 @@ function layoutable(::Type{<:Colorbar}, fig_or_scene; bbox = nothing, kwargs...) spinecolor = :transparent, spinevisible = :false, flip_vertical_label = flip_vertical_label, minorticksvisible = minorticksvisible, minortickalign = minortickalign, minorticksize = minorticksize, minortickwidth = minortickwidth, - minortickcolor = minortickcolor, minorticks = minorticks) + minortickcolor = minortickcolor, minorticks = minorticks, scale = scale) decorations[:axis] = axis @@ -351,3 +370,13 @@ end function tight_ticklabel_spacing!(lc::Colorbar) tight_ticklabel_spacing!(lc.elements[:axis]) end + +function scaled_steps(steps, scale, lims) + # first scale to limits so we can actually apply the scale to the values + # (log(0) doesn't work etc.) + s_limits = steps .* (lims[2] - lims[1]) .+ lims[1] + # scale with scaling function + s_limits_scaled = scale.(s_limits) + # then rescale to 0 to 1 + s_scaled = (s_limits_scaled .- s_limits_scaled[1]) ./ (s_limits_scaled[end] - s_limits_scaled[1]) +end \ No newline at end of file diff --git a/src/makielayout/lineaxis.jl b/src/makielayout/lineaxis.jl index 5f17ddb66..f848a15ca 100644 --- a/src/makielayout/lineaxis.jl +++ b/src/makielayout/lineaxis.jl @@ -192,9 +192,9 @@ function LineAxis(parent::Scene; kwargs...) tickvalues = Node(Float32[]) - tickvalues_labels_unfiltered = lift(pos_extents_horizontal, limits, ticks, tickformat) do (position, extents, horizontal), - limits, ticks, tickformat - get_ticks(ticks, tickformat, limits...) + tickvalues_labels_unfiltered = lift(pos_extents_horizontal, limits, ticks, tickformat, attrs.scale) do (position, extents, horizontal), + limits, ticks, tickformat, scale + get_ticks(ticks, scale, tickformat, limits...) end tickpositions = Node(Point2f0[]) @@ -215,10 +215,19 @@ function LineAxis(parent::Scene; kwargs...) lim_w = limits[][2] - limits[][1] # if labels are given manually, it's possible that some of them are outside the displayed limits - i_values_within_limits = findall(tv -> lim_o <= tv <= (lim_o + lim_w), tickvalues_unfiltered) + # we only check approximately because otherwise because of floating point errors, ticks can be dismissed sometimes + i_values_within_limits = findall(tickvalues_unfiltered) do tv + (limits[][1] <= tv || limits[][1] ≈ tv) && + (tv <= limits[][2] || tv ≈ limits[][2]) + end + tickvalues[] = tickvalues_unfiltered[i_values_within_limits] - tick_fractions = (tickvalues[] .- lim_o) ./ lim_w + scale = attrs.scale[] + tickvalues_scaled = scale.(tickvalues[]) + + tick_fractions = (tickvalues_scaled .- scale(limits[][1])) ./ (scale(limits[][2]) - scale(limits[][1])) + tick_scenecoords = px_o .+ px_width .* tick_fractions tickpos = if horizontal @@ -237,7 +246,7 @@ function LineAxis(parent::Scene; kwargs...) minortickpositions = Node(Point2f0[]) onany(tickvalues, minorticks) do tickvalues, minorticks - minortickvalues[] = get_minor_tickvalues(minorticks, tickvalues, limits[]...) + minortickvalues[] = get_minor_tickvalues(minorticks, attrs.scale[], tickvalues, limits[]...) end onany(minortickvalues) do minortickvalues @@ -251,7 +260,11 @@ function LineAxis(parent::Scene; kwargs...) lim_o = limits[][1] lim_w = limits[][2] - limits[][1] - tick_fractions = (minortickvalues .- lim_o) ./ lim_w + scale = attrs.scale[] + tickvalues_scaled = scale.(minortickvalues) + + tick_fractions = (tickvalues_scaled .- scale(limits[][1])) ./ (scale(limits[][2]) - scale(limits[][1])) + tick_scenecoords = px_o .+ px_width .* tick_fractions minortickpositions[] = if horizontal @@ -417,13 +430,28 @@ Base function that calls `get_tickvalues(ticks, vmin, max)` and For custom ticks / formatter combinations, this method can be overloaded directly, or both `get_tickvalues` and `get_ticklabels` separately. """ -function get_ticks(ticks, formatter, vmin, vmax) - tickvalues = get_tickvalues(ticks, vmin, vmax) +function get_ticks(ticks, scale, formatter, vmin, vmax) + tickvalues = get_tickvalues(ticks, scale, vmin, vmax) ticklabels = get_ticklabels(formatter, tickvalues) return tickvalues, ticklabels end -function get_ticks(ticks_and_labels::Tuple{Any, Any}, ::AbstractPlotting.Automatic, vmin, vmax) +# automatic with identity scaling uses WilkinsonTicks by default +get_tickvalues(::AbstractPlotting.Automatic, ::typeof(identity), vmin, vmax) = get_tickvalues(WilkinsonTicks(5, k_min = 3), vmin, vmax) + +# fall back to identity if not overloaded scale function is used with automatic +get_tickvalues(::AbstractPlotting.Automatic, F, vmin, vmax) = get_tickvalues(AbstractPlotting.automatic, identity, vmin, vmax) + +# fall back to non-scale aware behavior if no special version is overloaded +get_tickvalues(ticks, scale, vmin, vmax) = get_tickvalues(ticks, vmin, vmax) + +# get_tickvalues(::AbstractPlotting.Automatic, ::typeof(log10), vmin, vmax) = get_tickvalues(Log10Ticks(), vmin, vmax) + +# get_tickvalues(::AbstractPlotting.Automatic, ::typeof(log2), vmin, vmax) = get_tickvalues(Log2Ticks(), vmin, vmax) + +# get_tickvalues(::AbstractPlotting.Automatic, ::typeof(log), vmin, vmax) = get_tickvalues(LogTicks(), vmin, vmax) + +function get_ticks(ticks_and_labels::Tuple{Any, Any}, any_scale, ::AbstractPlotting.Automatic, vmin, vmax) n1 = length(ticks_and_labels[1]) n2 = length(ticks_and_labels[2]) if n1 != n2 @@ -432,7 +460,7 @@ function get_ticks(ticks_and_labels::Tuple{Any, Any}, ::AbstractPlotting.Automat ticks_and_labels end -function get_ticks(tickfunction::Function, formatter, vmin, vmax) +function get_ticks(tickfunction::Function, any_scale, formatter, vmin, vmax) result = tickfunction(vmin, vmax) if result isa Tuple{Any, Any} tickvalues, ticklabels = result @@ -443,6 +471,27 @@ function get_ticks(tickfunction::Function, formatter, vmin, vmax) return tickvalues, ticklabels end +_logbase(::typeof(log10)) = "10" +_logbase(::typeof(log2)) = "2" +_logbase(::typeof(log)) = "e" + +# log ticks just use the normal pipeline but with log'd limits, then transform the labels +function get_ticks(x, scale::Union{typeof(log10), typeof(log2), typeof(log)}, y, vmin, vmax) + ticks_scaled = get_tickvalues(x, identity, scale(vmin), scale(vmax)) + + ticks = AbstractPlotting.inverse_transform(scale).(ticks_scaled) + + if y === AbstractPlotting.automatic + # here we assume that the labels are normal numbers, and we just superscript them + labels_scaled = get_ticklabels(AbstractPlotting.automatic, ticks_scaled) + labels = _logbase(scale) .* AbstractPlotting.UnicodeFun.to_superscript.(labels_scaled) + else + # otherwise the formatter has to handle the real tick numbers + labels = get_ticklabels(y, ticks) + end + + (ticks, labels) +end """ get_tickvalues(lt::LinearTicks, vmin, vmax) @@ -452,6 +501,17 @@ Runs a common tick finding algorithm to as many ticks as requested by the """ get_tickvalues(lt::LinearTicks, vmin, vmax) = locateticks(vmin, vmax, lt.n_ideal) +# function get_tickvalues(::Log10Ticks, vmin, vmax) +# exp10.(ceil(log10(vmin)):floor(log10(vmax))) +# end + +# function get_tickvalues(::Log2Ticks, vmin, vmax) +# exp2.(ceil(log2(vmin)):floor(log2(vmax))) +# end + +# function get_tickvalues(::LogTicks, vmin, vmax) +# exp.(ceil(log(vmin)):floor(log(vmax))) +# end """ get_tickvalues(tickvalues, vmin, vmax) @@ -482,7 +542,7 @@ Gets tick labels by formatting each value in `values` according to a `Formatting get_ticklabels(formatstring::AbstractString, values) = [Formatting.format(formatstring, v) for v in values] -function get_ticks(m::MultiplesTicks, ::AbstractPlotting.Automatic, vmin, vmax) +function get_ticks(m::MultiplesTicks, any_scale, ::AbstractPlotting.Automatic, vmin, vmax) dvmin = vmin / m.multiple dvmax = vmax / m.multiple multiples = MakieLayout.get_tickvalues(LinearTicks(m.n_ideal), dvmin, dvmax) @@ -491,7 +551,7 @@ function get_ticks(m::MultiplesTicks, ::AbstractPlotting.Automatic, vmin, vmax) end -function get_minor_tickvalues(i::IntervalsBetween, tickvalues, vmin, vmax) +function get_minor_tickvalues(i::IntervalsBetween, scale, tickvalues, vmin, vmax) vals = Float32[] length(tickvalues) < 2 && return vals n = i.n @@ -526,5 +586,49 @@ function get_minor_tickvalues(i::IntervalsBetween, tickvalues, vmin, vmax) end end + vals +end + +# for log scales, we need to step in log steps at the edges +function get_minor_tickvalues(i::IntervalsBetween, scale::Union{typeof(log), typeof(log2), typeof(log10)}, tickvalues, vmin, vmax) + vals = Float32[] + length(tickvalues) < 2 && return vals + n = i.n + + invscale = AbstractPlotting.inverse_transform(scale) + + if i.mirror + firstinterval_scaled = scale(tickvalues[2]) - scale(tickvalues[1]) + stepsize = firstinterval_scaled / n + prevtick = invscale(scale(tickvalues[1]) - firstinterval_scaled) + stepsize = (tickvalues[1] - prevtick) / n + v = tickvalues[1] - stepsize + while v >= vmin + pushfirst!(vals, v) + v -= stepsize + end + end + + for (lo, hi) in zip(@view(tickvalues[1:end-1]), @view(tickvalues[2:end])) + interval = hi - lo + stepsize = interval / n + v = lo + for i in 1:n-1 + v += stepsize + push!(vals, v) + end + end + + if i.mirror + lastinterval_scaled = scale(tickvalues[end]) - scale(tickvalues[end-1]) + nexttick = invscale(scale(tickvalues[end]) + lastinterval_scaled) + stepsize = (nexttick - tickvalues[end]) / n + v = tickvalues[end] + stepsize + while v <= vmax + push!(vals, v) + v += stepsize + end + end + vals end \ No newline at end of file diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index cbb1256d4..a20fdfd95 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -50,6 +50,8 @@ struct MultiplesTicks suffix::String end +struct Log10Ticks end + """ IntervalsBetween(n::Int, mirror::Bool = true) @@ -121,6 +123,7 @@ end scene::Scene xaxislinks::Vector{Axis} yaxislinks::Vector{Axis} + targetlimits::Node{FRect2D} finallimits::Node{FRect2D} block_limit_linking::Node{Bool} mouseeventhandle::MouseEventHandle diff --git a/src/scenes.jl b/src/scenes.jl index 0d60af79c..3b63f6988 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -583,3 +583,4 @@ function update_limits!(scene::Scene, new_limits::Rect, padding::Vec3f0=scene.pa scene.data_limits[] = FRect3D(minimum(lims) .- padd_abs, lim_w .+ 2padd_abs) scene end + diff --git a/test/unit_tests/makielayout.jl b/test/unit_tests/makielayout.jl index d45eaedb7..25ca405e8 100644 --- a/test/unit_tests/makielayout.jl +++ b/test/unit_tests/makielayout.jl @@ -34,7 +34,8 @@ end @testset "Axis limits basics" begin f = Figure() - ax = Axis(f[1, 1], targetlimits = BBox(0, 10, 0, 20), limits = (nothing, nothing)) + ax = Axis(f[1, 1], limits = (nothing, nothing)) + ax.targetlimits[] = BBox(0, 10, 0, 20) @test ax.finallimits[] == BBox(0, 10, 0, 20) @test ax.limits[] == (nothing, nothing) xlims!(ax, -10, 10) diff --git a/test/unit_tests/transformations.jl b/test/unit_tests/transformations.jl index 6b8513b2a..035561dca 100644 --- a/test/unit_tests/transformations.jl +++ b/test/unit_tests/transformations.jl @@ -1,24 +1,80 @@ -using AbstractPlotting: PointTrans, xyz_boundingbox +using AbstractPlotting: PointTrans, xyz_boundingbox, apply_transform @testset "Basic transforms" begin - function point2(x::Point2) + function fpoint2(x::Point2) return Point2f0(x[1] + 10, x[2] - 77) end - function point3(x::Point3) + function fpoint3(x::Point3) return Point3f0(x[1] + 10, x[2] - 77, x[3] / 4) end - trans2 = PointTrans{2}(point2) - trans3 = PointTrans{3}(point3) - points = [Point2f0(0, 0), Point2f0(0, 1)] - bb = xyz_boundingbox(trans2, points) + trans2 = PointTrans{2}(fpoint2) + trans3 = PointTrans{3}(fpoint3) + points2 = [Point2f0(0, 0), Point2f0(0, 1)] + bb = xyz_boundingbox(trans2, points2) @test bb == Rect(Vec3f0(10, -77, 0), Vec3f0(0, 1, 0)) - bb = xyz_boundingbox(trans3, points) + bb = xyz_boundingbox(trans3, points2) @test bb == Rect(Vec3f0(10, -77, 0), Vec3f0(0, 1, 0)) - points = [Point3f0(0, 0, 4), Point3f0(0, 1, -8)] - bb = xyz_boundingbox(trans2, points) + points3 = [Point3f0(0, 0, 4), Point3f0(0, 1, -8)] + bb = xyz_boundingbox(trans2, points3) @test bb == Rect(Vec3f0(10, -77, -8), Vec3f0(0, 1, 12)) - bb = xyz_boundingbox(trans3, points) + bb = xyz_boundingbox(trans3, points3) @test bb == Rect(Vec3f0(10, -77, -2.0), Vec3f0(0, 1, 3.0)) + + @test apply_transform(trans2, points2) == fpoint2.(points2) + @test apply_transform(trans3, points3) == fpoint3.(points3) + + @test_throws ErrorException PointTrans{2}(x::Int -> x) + @test_throws ErrorException PointTrans{3}(x::Int -> x) end + + +@testset "Tuple and identity transforms" begin + t1 = sqrt + t2 = (sqrt, log) + t3 = (sqrt, log, log10) + + p2 = Point(2.0, 5.0) + p3 = Point(2.0, 5.0, 4.0) + + @test apply_transform(identity, p2) == p2 + @test apply_transform(identity, p3) == p3 + + @test apply_transform(t1, p2) == Point(sqrt(2.0), sqrt(5.0)) + @test apply_transform(t1, p3) == Point(sqrt(2.0), sqrt(5.0), sqrt(4.0)) + + @test apply_transform(t2, p2) == Point2f0(sqrt(2.0), log(5.0)) + @test apply_transform(t2, p3) == Point3f0(sqrt(2.0), log(5.0), 4.0) + + @test apply_transform(t3, p3) == Point3f0(sqrt(2.0), log(5.0), log10(4.0)) + + i2 = (identity, identity) + i3 = (identity, identity, identity) + @test apply_transform(i2, p2) == p2 + @test apply_transform(i3, p3) == p3 + + # test that identity gives back exact same arrays without copying + p2s = Point2f0[(1, 2), (3, 4)] + @test apply_transform(identity, p2s) === p2s + @test apply_transform(i2, p2s) === p2s + @test apply_transform(i3, p2s) === p2s + + p3s = Point3f0[(1, 2, 3), (3, 4, 5)] + @test apply_transform(identity, p3s) === p3s + @test apply_transform(i2, p3s) === p3s + @test apply_transform(i3, p3s) === p3s + + @test apply_transform(identity, 1) == 1 + @test apply_transform(i2, 1) == 1 + @test apply_transform(i3, 1) == 1 + + @test apply_transform(identity, 1..2) == 1..2 + @test apply_transform(i2, 1..2) == 1..2 + @test apply_transform(i3, 1..2) == 1..2 + + pa = Point2f0(1, 2) + pb = Point2f0(3, 4) + r2 = FRect2D(pa, pb .- pa) + @test apply_transform(t1, r2) == FRect2D(apply_transform(t1, pa), apply_transform(t1, pb) .- apply_transform(t1, pa) ) +end \ No newline at end of file