MakieOrg / Makie.jl

Interactive data visualizations and plotting in Julia
https://docs.makie.org/stable
MIT License
2.37k stars 302 forks source link

Makie.jl linked axis set limits! bug #3689

Open bryaan opened 6 months ago

bryaan commented 6 months ago

I have a minimal example reproducing a bug with linked axis and setting limits on them.

In this example when you click the focus button both plot series are searched for the min and max values in y that are within the current view's xlimits.

To show the bug you have to do some panning by simply click-and-drag on the top plot, then click focus button. Repeat that once or twice and you will see that the xlimits jump back to previous values which seems to be because the ax.limits[] are not properly being updated for the linked x axis.

If you instead drag on the lower plot and focus, everything works fine.

https://gist.github.com/bryaan/0bf7d34c69729b2d7145b5e8028fa90e

using Dates

using GLMakie
GLMakie.activate!()
Makie.inline!(false)  # Probably unnecessary?

using Observables

mutable struct MouseHandlerState
    pos
    xmin
    xmax
    ymin
    ymax
    was_inside_plot::Bool
    focus::Bool
    fast_forward::Bool
    function MouseHandlerState()
        new(0, 0, 0, 0, 0, false, false, false)
    end
end

function get_xlims(ax)
    limits = ax.limits[]
    if limits[1] === nothing
        return nothing, nothing
    end
    xmin, xmax = limits[1][1], limits[1][2]
end

function register_mouse_handlers(fig, ax, plot, focus)
    # Remove the rectangle zoom on left-click drag.
    deregister_interaction!(ax, :rectanglezoom)

    mouseevents = addmouseevents!(fig.scene)

    state = MouseHandlerState()

    if focus !== nothing
        on(focus) do f
            if f == true
                xmin, xmax = get_xlims(ax)

                if xmin === nothing || xmax === nothing
                    return
                end

                _ymin1, _ymax1 = get_minmax_within_xrange(timestamps, y, xmin, xmax)
                _ymin2, _ymax2 = get_minmax_within_xrange(timestamps, y2, xmin, xmax)
                ymin = min(_ymin1, _ymin2)
                ymax = max(_ymax1, _ymax2)

                if ymin == ymax || ymin == Inf || ymax == -Inf
                    C.focus[] = false
                    return
                end

                limits!(ax, xmin, xmax, ymin, ymax)
            end
        end
    end

    on(events(ax.scene).scroll) do event
        if focus !== nothing
            focus[] = false
        end
    end

    onmouseleftdrag(mouseevents) do event
        if focus !== nothing
            focus[] = false
        end
        if state.was_inside_plot
            px_delta = event.px - state.pos
            # data_delta = event.data - event.prev_data

            scale = extrema(ax.scene.px_area[])

            dx = px_delta[1] * (state.xmax - state.xmin) / scale[2][1]
            dy = -px_delta[2] * (state.ymax - state.ymin) / scale[2][2]

            # Update the axis limits to achieve panning
            limits!(ax, state.xmin - dx, state.xmax - dx, state.ymin + dy, state.ymax + dy)
        end
    end

    onmouseleftdragstart(mouseevents) do event
        state.was_inside_plot = is_mouseinside(plot)
        state.pos = event.px

        limits = extrema(ax.finallimits[])
        state.xmin, state.xmax = limits[1][1], limits[2][1]
        state.ymin, state.ymax = limits[1][2], limits[2][2]

        # limits = C.ax.limits[]
        # state.xmin, state.xmax = limits[1][1], limits[1][2]
        # state.ymin, state.ymax = limits[2][1], limits[2][2]
    end

    onmouseleftdragstop(mouseevents) do event
        state.was_inside_plot = false
    end
end

function get_minmax_within_xrange(timestamps, values, xmin, xmax)
    _start = searchsortedfirst(timestamps[], xmin)
    _end = searchsortedfirst(timestamps[], xmax)

    if _start === nothing
        _start = 1
    end
    if _end === nothing
        _end = length(values[])
    end

    _start = max(_start, 1)
    _end = min(_end, length(values[]))
    if _start == _end
        return 0, 1
    end

    @show _start, _end

    ymin = minimum(@view values[][_start:_end])
    ymax = maximum(@view values[][_start:_end])
    ymin, ymax
end

function create_focus_button(fig, area)
    focus = Observable{Bool}(false)
    buttongrid = GridLayout(area, tellwidth=false)
    btn = Button(fig, label=@lift($focus ? "_" : "FOCUS"))
    buttongrid[1, 1] = [btn]
    on(btn.clicks) do _
        focus[] = !focus[]
    end
    focus
end

fig = Figure()

fig[1, 1] = chartgrid = GridLayout(tellwidth=false)

focus = create_focus_button(fig, chartgrid[1, 1])

ts = collect(1:100)
timestamps = Observable(ts)

ax = Axis(chartgrid[2, 1])
y = Observable(sin.(ts))
lineplot = lines!(ax, timestamps, y, color=:blue)

sub_ax = Axis(chartgrid[3, 1])
y2 = Observable(sin.(ts))
sub_lineplot = lines!(sub_ax, timestamps, y2, color=:red)

linkxaxes!(sub_ax, ax)

# Pan functionality
register_mouse_handlers(fig, ax, lineplot, focus)
register_mouse_handlers(fig, sub_ax, sub_lineplot, focus)

# xlims!(ax, Dates.value(Millisecond(Minute(0))), Dates.value(Millisecond(Minute(60 * 12))))
# ylims!(ax, -100, 100)

screen = display(fig)
wait(screen)
bryaan commented 3 months ago

Any progress on this?

ffreyer commented 1 week ago

Maybe related to #3103