Gabriel
Gabriel

Reputation: 42329

Distinguish button_press_event from drag and zoom clicks in matplotlib

I have a simple code that shows two subplots, and lets the user left click on the second subplot while recording the x,y coordinates of those clicks.

The problem is that clicks to select a region to zoom and to drag the subplot are also identified as left clicks.

Is there a way to distinguish and filter out these left clicks?

import numpy as np
import matplotlib.pyplot as plt


def onclick(event, ax):
    # Only clicks inside this axis are valid.
    if event.inaxes == ax:
        if event.button == 1:
            print(event.xdata, event.ydata)
            # Draw the click just made
            ax.scatter(event.xdata, event.ydata)
            ax.figure.canvas.draw()
        elif event.button == 2:
            # Do nothing
            print("scroll click")
        elif event.button == 3:
            # Do nothing
            print("right click")
        else:
            pass


fig, (ax1, ax2) = plt.subplots(1, 2)
# Plot some random scatter data
ax2.scatter(np.random.uniform(0., 10., 10), np.random.uniform(0., 10., 10))

fig.canvas.mpl_connect(
    'button_press_event', lambda event: onclick(event, ax2))
plt.show()

Upvotes: 7

Views: 6710

Answers (5)

faelx
faelx

Reputation: 31

You can check for a mayplotlib widget lock (when a widget button is pressed). If it is the case, for example when panning/zooming, it returns True, and you can return from the event handler function using:

def onclick(event, ax):    
    if self.fig.canvas.widgetlock.locked():
        return
    #put handling of non-panning/zooming events here

Upvotes: 3

old-ufo
old-ufo

Reputation: 2850

My answe would not work for the all backends, however, it works in the jupyter notebook backend nbAgg, similar to @Rincewind answer


%matplotlib notebook
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as wdg 

def onclick(event, ax):
    # Only clicks inside this axis are valid.
    if event.inaxes != ax:
        return
    
    # Not adding points when zooming or paning
    ptr_type = str(fig.canvas.toolbar.cursor)
    pick = 'Cursors.POINTER'
    if ptr_type != pick:
        return
    
    if event.button == 1:
        print(event.xdata, event.ydata)
        # Draw the click just made
        ax.scatter(event.xdata, event.ydata)
        ax.figure.canvas.draw()
    elif event.button == 2:
        # Do nothing
        print("scroll click")
    elif event.button == 3:
        # Do nothing
        print("right click")
    else:
        pass


fig, (ax1, ax2) = plt.subplots(1, 2)
# Plot some random scatter data
ax2.scatter(np.random.uniform(0., 10., 10), np.random.uniform(0., 10., 10))

ka = fig.canvas.mpl_connect(
    'button_press_event', lambda event: onclick(event, ax2))
plt.show()

Upvotes: 0

Rincewind
Rincewind

Reputation: 71

I realize that this is an old question, but I just had the same problem and I think I found a neat solution; however it currently works only for the Qt backend (similar solutions might exist for other backends too). The idea is that matplotlib changes the cursor shape while zooming or panning, so you can check for that. This is the adapted code:

import numpy as np
import matplotlib
matplotlib.use('qt5agg')
import matplotlib.pyplot as plt


def onclick(event, ax):
    # Only clicks inside this axis are valid.
    try: # use try/except in case we are not using Qt backend
        zooming_panning = ( fig.canvas.cursor().shape() != 0 ) # 0 is the arrow, which means we are not zooming or panning.
    except:
        zooming_panning = False
    if zooming_panning: 
        print("Zooming or panning")
        return
    if event.inaxes == ax:
        if event.button == 1:
            print(event.xdata, event.ydata)
            # Draw the click just made
            ax.scatter(event.xdata, event.ydata)
            ax.figure.canvas.draw()
        elif event.button == 2:
            # Do nothing
            print("scroll click")
        elif event.button == 3:
            # Do nothing
            print("right click")
        else:
            pass


fig, (ax1, ax2) = plt.subplots(1, 2)
# Plot some random scatter data
ax2.scatter(np.random.uniform(0., 10., 10), np.random.uniform(0., 10., 10))

fig.canvas.mpl_connect(
    'button_press_event', lambda event: onclick(event, ax2))
plt.show()

Upvotes: 7

ImportanceOfBeingErnest
ImportanceOfBeingErnest

Reputation: 339122

You may check if the mouse button is released after the mouse has previously been moved. Since for zooming and panning, this would be the case you may call the function to draw a new point only when no previous movement has happened.

import numpy as np
import matplotlib.pyplot as plt

class Click():
    def __init__(self, ax, func, button=1):
        self.ax=ax
        self.func=func
        self.button=button
        self.press=False
        self.move = False
        self.c1=self.ax.figure.canvas.mpl_connect('button_press_event', self.onpress)
        self.c2=self.ax.figure.canvas.mpl_connect('button_release_event', self.onrelease)
        self.c3=self.ax.figure.canvas.mpl_connect('motion_notify_event', self.onmove)

    def onclick(self,event):
        if event.inaxes == self.ax:
            if event.button == self.button:
                self.func(event, self.ax)
    def onpress(self,event):
        self.press=True
    def onmove(self,event):
        if self.press:
            self.move=True
    def onrelease(self,event):
        if self.press and not self.move:
            self.onclick(event)
        self.press=False; self.move=False


def func(event, ax):
    print(event.xdata, event.ydata)
    ax.scatter(event.xdata, event.ydata)
    ax.figure.canvas.draw()

fig, (ax1, ax2) = plt.subplots(1, 2)
# Plot some random scatter data
ax2.scatter(np.random.uniform(0., 10., 10), np.random.uniform(0., 10., 10))
click = Click(ax2, func, button=1)
plt.show()

Upvotes: 11

Paul Brodersen
Paul Brodersen

Reputation: 13031

One way to distinguish between clicks and dragging/zooming (be it right click or left click) would be to measure the time between the button press and the button release and then carry out the actions on the button release, not the button press.

import numpy as np
import matplotlib.pyplot as plt
import time

MAX_CLICK_LENGTH = 0.1 # in seconds; anything longer is a drag motion

def onclick(event, ax):
    ax.time_onclick = time.time()

def onrelease(event, ax):
    # Only clicks inside this axis are valid.
    if event.inaxes == ax:
        if event.button == 1 and ((time.time() - ax.time_onclick) < MAX_CLICK_LENGTH):
            print(event.xdata, event.ydata)
            # Draw the click just made
            ax.scatter(event.xdata, event.ydata)
            ax.figure.canvas.draw()
        elif event.button == 2:
            print("scroll click")
        elif event.button == 3:
            print("right click")
        else:
            pass


fig, (ax1, ax2) = plt.subplots(1, 2)
# Plot some random scatter data
ax2.scatter(np.random.uniform(0., 10., 10), np.random.uniform(0., 10., 10))

fig.canvas.mpl_connect('button_press_event', lambda event: onclick(event, ax2))
fig.canvas.mpl_connect('button_release_event', lambda event: onrelease(event, ax2))
plt.show()

Upvotes: 4

Related Questions