Pan the Graph with Mouse Drag in Matplotlib
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
- Dragging the mouse moves the graph in parallel.
- 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:
- Register a callback function to receive
'button_press_event'
usingmpl_connect()
. - Register a callback function to receive
'motion_notify_event'
usingmpl_connect()
. - In the callback function for
button_press_event
, capture the mouse click event and record the coordinates on the graph at that time. - 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. - Calculate the difference from the coordinates recorded at the start of the click, and change the values at both ends of the graph.
- Redraw to reflect the changed result on the screen.
Discussion
New Comments
No comments yet. Be the first one!