Pan the Graph with Mouse Drag in Matplotlib

2024-06-11matplotlib,Python

Introduction

In the previous article, I explained and implemented how to zoom in and out of a graph using the mouse wheel. In this article, I will further explain how to pan the graph by dragging it.

What I Want to Implement

  1. Dragging the mouse moves the graph in parallel.
  2. Pressing the cursor keys moves the graph parallel to the direction pressed.

How to Recieve Drag and Key Operations

Receiving Drag Operations

To receive drag operations, use 'motion_notify_event'. This event is triggered when the cursor moves within the graph, but it can also receive the state of the mouse buttons. By utilizing this, you can determine that dragging is occurring when the left button is pressed.

Example

import matplotlib.pyplot as plt

def on_move(event):
    if event.button == 1:
        print("dragging")

fig, ax = plt.subplots()
fig.canvas.mpl_connect('motion_notify_event', on_move)

event.button represents the mouse button that is pressed. 1 indicates the left button. In this example, when you drag while holding down the left mouse button, "dragging" is output to the console.

Receiving Key Operations

In the previous article, The function of receiving the Ctrl key was implemented. Similarly, the 'key_press_event' will be used.

Example

import matplotlib.pyplot as plt

def on_key_press(event):
    print(event.key)

fig, ax = plt.subplots()
fig.canvas.mpl_connect('key_press_event', on_key_press)

In the example above, the string corresponding to the pressed key is output to the console. If you hold down the key, the string will continue to be displayed at regular intervals.

Implementation of Panning Operation

Applying the above, let’s implement it in practice. The MplZoomHelper class, created in the previous article, will be extended by the implementation of panning operation. As an additional feature, pressing the 'h' key will allow you to return to the initial position.

import matplotlib.pyplot as plt

class MplZoomHelper:
    def __init__(self, ax):
        self.ax = ax
        self.ctrl_flag = False
        self.zoom_rate = 0.9

        self.move_rate = 0.05
        self.x_on_click = 0
        self.y_on_click = 0
        self.xlim_on_click = ax.get_xlim()
        self.ylim_on_click = ax.get_ylim()

        self.initial_xlim = ax.get_xlim()
        self.initial_ylim = ax.get_ylim()

        ax.figure.canvas.mpl_connect('key_press_event', self.on_key_press)
        ax.figure.canvas.mpl_connect('key_release_event', self.on_key_release)
        ax.figure.canvas.mpl_connect('scroll_event', self.on_scroll)
        ax.figure.canvas.mpl_connect(
            'button_press_event', self.on_button_press)
        ax.figure.canvas.mpl_connect('motion_notify_event', self.on_move)

    def on_key_press(self, event):
        if event.inaxes is not self.ax:
            return

        if event.key == 'control':
            self.ctrl_flag = True
        elif event.key == 'h':
            self.on_home(event)
        elif event.key == 'right':
            self.on_move_x(event, 1)
        elif event.key == 'left':
            self.on_move_x(event, -1)
        elif event.key == 'up':
            self.on_move_y(event, 1)
        elif event.key == 'down':
            self.on_move_y(event, -1)

    def on_key_release(self, event):
        if event.inaxes is not self.ax:
            return

        if event.key == 'control':
            self.ctrl_flag = False

    def on_home(self, event):
        self.ax.set_xlim(self.initial_xlim)
        self.ax.set_ylim(self.initial_ylim)
        self.ax.figure.canvas.draw()

    def on_scroll(self, event):
        if event.inaxes is not self.ax:
            return

        if self.ctrl_flag:
            self.on_scroll_y(event)
        else:
            self.on_scroll_x(event)

        self.ax.figure.canvas.draw()

    def on_scroll_x(self, event):
        x_pos = event.xdata
        min_value, max_value = self.ax.get_xlim()

        if event.button == 'up':
            new_min_value = x_pos - (x_pos - min_value) * self.zoom_rate
            new_max_value = (max_value - x_pos) * self.zoom_rate + x_pos
        elif event.button == 'down':
            new_min_value = x_pos - (x_pos - min_value) / self.zoom_rate
            new_max_value = (max_value - x_pos) / self.zoom_rate + x_pos

        self.ax.set_xlim(new_min_value, new_max_value)

    def on_scroll_y(self, event):
        y_pos = event.ydata
        min_value, max_value = self.ax.get_ylim()

        if event.button == 'up':
            new_min_value = y_pos - (y_pos - min_value) * self.zoom_rate
            new_max_value = (max_value - y_pos) * self.zoom_rate + y_pos
        elif event.button == 'down':
            new_min_value = y_pos - (y_pos - min_value) / self.zoom_rate
            new_max_value = (max_value - y_pos) / self.zoom_rate + y_pos

        self.ax.set_ylim(new_min_value, new_max_value)

    def on_button_press(self, event):
        if event.inaxes is not self.ax:
            return

        if event.button != 1:
            return

        self.x_on_click = event.xdata
        self.y_on_click = event.ydata
        self.xlim_on_click = self.ax.get_xlim()
        self.ylim_on_click = self.ax.get_ylim()

    def on_move(self, event):
        if event.inaxes is not self.ax:
            return

        if event.button != 1:
            return

        dx = event.xdata - self.x_on_click
        dy = event.ydata - self.y_on_click
        self.xlim_on_click -= dx
        self.ylim_on_click -= dy
        self.ax.set_xlim(self.xlim_on_click)
        self.ax.set_ylim(self.ylim_on_click)
        self.ax.figure.canvas.draw()

    def on_move_x(self, event, dir):
        min_value, max_value = self.ax.get_xlim()
        width = max_value - min_value
        dx = width * self.move_rate * dir
        self.ax.set_xlim(min_value + dx, max_value + dx)
        self.ax.figure.canvas.draw()

    def on_move_y(self, event, dir):
        min_value, max_value = self.ax.get_ylim()
        width = max_value - min_value
        dy = width * self.move_rate * dir
        self.ax.set_ylim(min_value + dy, max_value + dy)
        self.ax.figure.canvas.draw()

if __name__ == "__main__":
    fig, axes = plt.subplots(2, 2)
    axes[0, 0].plot([-1, 2, 3], [2, 4, 3])
    axes[1, 1].plot([0, 5, 9], [-2, 1, 10])

    z1 = MplZoomHelper(axes[0, 0])
    z2 = MplZoomHelper(axes[1, 1])
    plt.show()

Constructor

In the constructor, initialize additional variables. Also, to implement the functionality to return to the home position, keep track of the display range at the initial display.

on_key_press()

This is the behavior when cursor keys or the 'h' key are pressed. The cursor keys can be received as 'right', 'left', 'up', and 'down'. The program will jump to the corresponding processing for each key press from here.

on_home()

This is the behavior when the 'h' key is pressed. When the 'h' key is pressed, it returns to the display range that was kept in the constructor (self.initial_xlim, self.initial_ylim).

on_button_press()

This is the behavior when the mouse button is pressed. At this time, check event.inaxes to determine if the event occurred on the registered graph (self.ax).
In preparation for the drag, record the graph coordinates (event.xdata, event.ydata) when the click occurred, as well as the current x-axis and y-axis ranges.

on_move()

This is the behavior when the mouse cursor is moving. At this time, check event.inaxes to determine if the event occurred on the registered graph (self.ax). Also, check event.button and if it is 1, determine that dragging is occurring with the left button.
Obtain the graph coordinates (event.xdata, event.ydata) and calculate the difference from the coordinates at the start of the click. Shift the display range by that amount.

The result of the panning will not be reflected on the graph without redrawing. Therefore, self.ax.figure.canvas.draw() is called.

on_move_x(), on_move_y()

This is the behavior when a cursor key is pressed. The display is shifted parallel in the direction of the pressed cursor key by a fraction (self.move_rate) of the display width. To redraw after moving, self.ax.figure.canvas.draw() is called.

Summary

Here is the method to implement panning by mouse drag on a graph displayed with matplotlib:

  1. Register a callback function to receive 'button_press_event' using mpl_connect().
  2. Register a callback function to receive 'motion_notify_event' using mpl_connect().
  3. In the callback function for button_press_event, capture the mouse click event and record the coordinates on the graph at that time.
  4. In the callback function for motion_notify_event, obtain the mouse button’s pressed state. If it is in the pressed state, determine that dragging is occurring.
  5. Calculate the difference from the coordinates recorded at the start of the click, and change the values at both ends of the graph.
  6. Redraw to reflect the changed result on the screen.

matplotlib,python

Posted by izadori