Makie max pan zoom

i’d like to put a maximum on how much a user can pan or zoom out on a plot. but i could not find a function in the makie docs to set an upper bound on the axis limits. so i hacked it like this:

using GLMakie
f,a,s = scatter([1,2,3])
on(a.finallimits) do x
    if x.origin[2]>5 
        a.finallimits[] = GLMakie.GeometryBasics.HyperRectangle{2, Float32}([x.origin[1], 5], x.widths)
    end
end

try panning upwards. you can’t go above the lower y-value being 5 now.

but is there a more cannonical way to do this? did i just miss a function to do this for you? seems like it would be a nice feature to have.

1 Like

small correction-- targetlimits should be modified not finallimits.

so here then is an example which prevents the user from zooming out further than the limits of the data:

using GLMakie
x, y = rand(10), rand(10)
f,a,s = scatter(x,y)
on(a.finallimits) do fl
    o1 = max(fl.origin[1], minimum(x))
    o2 = max(fl.origin[2], minimum(y))
    w1 = min(fl.widths[1]+fl.origin[1], maximum(x))-o1
    w2 = min(fl.widths[2]+fl.origin[2], maximum(y))-o2
    a.targetlimits[] = GLMakie.GeometryBasics.HyperRectangle{2, Float32}([o1,o2],[w1,w2])
end

i believe an optional feature like this is genuinely useful.

one way to build this into Makie would be to add a bounds variable to the Axis struct, and then insert a max and min in the ScrollZoom interaction to enforce it. i didn’t go to the trouble of submitting a PR due to the lackluster response this post created.

alternatively, one could deactivate_interaction! the default, and add your own, which included these modifications.

here is a custom ScrollZoomLimit interaction, which permits zooming when scrolling like the default ScrollZoom interaction for Axis, but if ax.limits is set to something other than nothing, then you can’t zoom out further than those limits. the code is a very small modification to that in the makie source, just another 16 lines.

i would submit a PR, but a mechanism to also limit how much one can zoom in would be more generally useful, and would require some sort of additional state to specify those minimum limits.

import Base: RefValue
import Makie: Automatic, timed_ticklabelspace_reset

mutable struct ScrollZoomLimit
    speed::Float32
    reset_timer::RefValue{Union{Nothing, Timer}}
    prev_xticklabelspace::RefValue{Union{Automatic, Symbol, Float64}}
    prev_yticklabelspace::RefValue{Union{Automatic, Symbol, Float64}}
    reset_delay::Float32
end

function ScrollZoomLimit(speed, reset_delay)
    return ScrollZoomLimit(speed, RefValue{Union{Nothing, Timer}}(nothing), RefValue{Union{Automatic, Symbol, Float64}}(0.0), RefValue{Union{Automatic, Symbol, Float64}}(0.0), reset_delay)
end

function Makie.process_interaction(s::ScrollZoomLimit, event::ScrollEvent, ax::Axis)
    # use vertical zoom
    zoom = event.y

    tlimits = ax.targetlimits

    xlimit = zoom < 0 && all(.!isnothing.(ax.limits[][1]))
    ylimit = zoom < 0 && all(.!isnothing.(ax.limits[][2]))

    xzoomlock = ax.xzoomlock
    yzoomlock = ax.yzoomlock
    xzoomkey = ax.xzoomkey
    yzoomkey = ax.yzoomkey

    scene = ax.scene
    e = events(scene)
    cam = camera(scene)

    ispressed(scene, ax.zoombutton[]) || return Consume(false)

    if zoom != 0
        pa = viewport(scene)[]

        z = (1.0 - s.speed)^zoom

        mp_axscene = Vec4d((e.mouseposition[] .- pa.origin)..., 0, 1)

        # first to normal -1..1 space
        mp_axfraction = (cam.pixel_space[] * mp_axscene)[Vec(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

        xscale = ax.xscale[]
        yscale = ax.yscale[]

        transf = (xscale, yscale)
        tlimits_trans = Makie.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

        newxorigin = xzoomlock[] ? xorigin : xorigin + mp_axfraction[1] * (xwidth - newxwidth)
        newyorigin = yzoomlock[] ? yorigin : yorigin + mp_axfraction[2] * (ywidth - newywidth)

        if xlimit
            xlimwidth = ax.limits[][1][2] - ax.limits[][1][1]
            newxwidth = min(newxwidth, xlimwidth)
            if newxwidth == xlimwidth && newxorigin < ax.limits[][1][1]
                newxorigin = min(ax.limits[][1][1], newxorigin + 2 * (xorigin - newxorigin))
            end
        end
        if ylimit
            ylimwidth = ax.limits[][2][2] - ax.limits[][2][1]
            newywidth = min(newywidth, ylimwidth)
            if newywidth == ylimwidth && newyorigin < ax.limits[][2][1]
                newyorigin = min(ax.limits[][2][1], newyorigin + 2 * (yorigin - newyorigin))
            end
        end

        timed_ticklabelspace_reset(ax, s.reset_timer, s.prev_xticklabelspace, s.prev_yticklabelspace, s.reset_delay)

        newrect_trans = if ispressed(scene, xzoomkey[])
            Rectd(newxorigin, yorigin, newxwidth, ywidth)
        elseif ispressed(scene, yzoomkey[])
            Rectd(xorigin, newyorigin, xwidth, newywidth)
        else
            Rectd(newxorigin, newyorigin, newxwidth, newywidth)
        end
        inv_transf = Makie.inverse_transform(transf)
        tlimits[] = Makie.apply_transform(inv_transf, newrect_trans)
    end

    # NOTE this might be problematic if if we add scrolling to something like Menu
    return Consume(true)
end
3 Likes

Ooh I should also do that for GeoMakie…