Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zoom_factory does not preserve canvas position #7

Open
antoniovazquezblanco opened this issue Jun 1, 2023 · 3 comments
Open

zoom_factory does not preserve canvas position #7

antoniovazquezblanco opened this issue Jun 1, 2023 · 3 comments

Comments

@antoniovazquezblanco
Copy link

Bug report

When using zoom_factory, scrolling events do not preserve current canvas position...

zoom_factory

Code for reproduction

#!/usr/bin/env python

from matplotlib import pyplot
from mpl_interactions import zoom_factory
import numpy as np

fig = pyplot.gcf()
zoom_factory(fig.gca())

t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)
fig.gca().plot(t, s)

pyplot.show()

Actual outcome

Whenever I pan my figure, if an scroll event takes place, I am automatically panned to the previous location...

Expected outcome

Whenever I pan my figure, if an scroll event takes place, only zoom should be affected and no panning should take place...

Version Info

  • Operating system: Affects both Windows and Archlinux
  • Matplotlib version: 3.7.1
  • Python version: 3.11.2 & 3.11.3
@ianhi ianhi transferred this issue from mpl-extensions/mpl-interactions Jun 1, 2023
@ianhi
Copy link
Collaborator

ianhi commented Jun 1, 2023

Hi @antoniovazquezblanco thanks for reporting!

one small note - i transferred this issue because I'm try to deprecate the zoom factory from mpl-interactions and move it over to a standalone package here.

This is indeed an annoying behavior - happily there is an easy workaround.

What seems to matter here is the ordering of plotting vs applying the zoom factory. Modifying your example to have the zoom factory after the plotting calls gives me the expected behavior:

from matplotlib import pyplot
from mpl_interactions import zoom_factory
import numpy as np

fig = pyplot.gcf()


t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)
fig.gca().plot(t, s)

zoom_factory(fig.gca())

pyplot.show()

this is a consequence of the auto centering behavior when you zoom far out. The "center" and extents chosen for that are locked in stone when the zoom_factory is created.

orig_xlim = ax.get_xlim()
orig_ylim = ax.get_ylim()
orig_yrange = limits_to_range(orig_ylim)
orig_xrange = limits_to_range(orig_xlim)
orig_center = ((orig_xlim[0] + orig_xlim[1]) / 2, (orig_ylim[0] + orig_ylim[1]) / 2)

then we're hitting this code block:

if np.abs(new_yrange) > np.abs(orig_yrange):
new_ylim = orig_center[1] - new_yrange / 2, orig_center[1] + new_yrange / 2
if np.abs(new_xrange) > np.abs(orig_xrange):
new_xlim = orig_center[0] - new_xrange / 2, orig_center[0] + new_xrange / 2

I'm not sure there's an obvious to check for updating those, but if there is I'd be happy to accept a PR implementing more nuanced behavior. I'd also accept a PR adding a note to the docs about the above workaround

@antoniovazquezblanco
Copy link
Author

Nice to know!

I will be updating the graph regularly, I guess this means I have to disable the zoom and then reapply.

I currently do not have the time to look into improving this behaviour. I may find some time in the near future :).

Thanks.

@ianhi
Copy link
Collaborator

ianhi commented Jun 1, 2023

ahh - actually probably the simplest fix would be to make that autocentering behavior optional, perhaps even off by default.

For your case you should instead just use this version of the function with teh autocentering chopped out:

from matplotlib import pyplot
import numpy as np

def zoom_factory(ax, base_scale=1.1):
    """
    Add ability to zoom with the scroll wheel.


    Parameters
    ----------
    ax : matplotlib axes object
        axis on which to implement scroll to zoom
    base_scale : float
        how much zoom on each tick of scroll wheel

    Returns
    -------
    disconnect_zoom : function
        call this to disconnect the scroll listener
    """

    def limits_to_range(lim):
        return lim[1] - lim[0]

    fig = ax.get_figure()  # get the figure of interest
    if hasattr(fig.canvas, "capture_scroll"):
        fig.canvas.capture_scroll = True
    has_toolbar = hasattr(fig.canvas, "toolbar") and fig.canvas.toolbar is not None
    if has_toolbar:
        # it might be possible to have an interactive backend without
        # a toolbar. I'm not sure so being safe here
        toolbar = fig.canvas.toolbar
        toolbar.push_current()
    def zoom_fun(event):
        if event.inaxes is not ax:
            return
        # get the current x and y limits
        cur_xlim = ax.get_xlim()
        cur_ylim = ax.get_ylim()
        # set the range
        (cur_xlim[1] - cur_xlim[0]) * 0.5
        (cur_ylim[1] - cur_ylim[0]) * 0.5
        xdata = event.xdata  # get event x location
        ydata = event.ydata  # get event y location
        if event.button == "up":
            # deal with zoom in
            scale_factor = base_scale
        elif event.button == "down":
            # deal with zoom out
            scale_factor = 1 / base_scale
        else:
            # deal with something that should never happen
            scale_factor = 1
        # set new limits
        new_xlim = [
            xdata - (xdata - cur_xlim[0]) / scale_factor,
            xdata + (cur_xlim[1] - xdata) / scale_factor,
        ]
        new_ylim = [
            ydata - (ydata - cur_ylim[0]) / scale_factor,
            ydata + (cur_ylim[1] - ydata) / scale_factor,
        ]
        new_yrange = limits_to_range(new_ylim)
        new_xrange = limits_to_range(new_xlim)

        ax.set_xlim(new_xlim)
        ax.set_ylim(new_ylim)


        if has_toolbar:
            toolbar.push_current()
        ax.figure.canvas.draw_idle()  # force re-draw

    # attach the call back
    cid = fig.canvas.mpl_connect("scroll_event", zoom_fun)

    def disconnect_zoom():
        fig.canvas.mpl_disconnect(cid)

    # return the disconnect function
    return disconnect_zoom


fig = pyplot.gcf()

zoom_factory(fig.gca())

t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)
fig.gca().plot(t, s)
pyplot.show()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants