Introduction
Matplotlib is a popular data visualization library that provides a wide range of tools for creating visualizations in Python. One of the interesting features of Matplotlib is the ability to add a crosshair cursor to a plot. In this lab, you will learn how to add a crosshair cursor to a Matplotlib plot.
VM Tips
After the VM startup is done, click the top left corner to switch to the Notebook tab to access Jupyter Notebook for practice.
Sometimes, you may need to wait a few seconds for Jupyter Notebook to finish loading. The validation of operations cannot be automated because of limitations in Jupyter Notebook.
If you face issues during learning, feel free to ask Labby. Provide feedback after the session, and we will promptly resolve the problem for you.
Simple Cursor
The first step is to add a simple cursor to a Matplotlib plot. This cursor will display the x and y values of the current mouse location.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backend_bases import MouseEvent
class Cursor:
"""
A cross hair cursor.
"""
def __init__(self, ax):
self.ax = ax
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
## text location in axes coordinates
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
def set_cross_hair_visible(self, visible):
need_redraw = self.horizontal_line.get_visible() != visible
self.horizontal_line.set_visible(visible)
self.vertical_line.set_visible(visible)
self.text.set_visible(visible)
return need_redraw
def on_mouse_move(self, event):
if not event.inaxes:
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.draw()
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
## update the line positions
self.horizontal_line.set_ydata([y])
self.vertical_line.set_xdata([x])
self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')
self.ax.figure.canvas.draw()
x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)
fig, ax = plt.subplots()
ax.set_title('Simple cursor')
ax.plot(x, y, 'o')
cursor = Cursor(ax)
fig.canvas.mpl_connect('motion_notify_event', cursor.on_mouse_move)
plt.show()
Faster Cursoring using Blitting
The cursor created in the previous step is a bit slow because it redraws the figure on every mouse move. In this step, we will create a cursor that uses blitting for faster rendering.
class BlittedCursor:
"""
A cross-hair cursor using blitting for faster redraw.
"""
def __init__(self, ax):
self.ax = ax
self.background = None
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
## text location in axes coordinates
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
self._creating_background = False
ax.figure.canvas.mpl_connect('draw_event', self.on_draw)
def on_draw(self, event):
self.create_new_background()
def set_cross_hair_visible(self, visible):
need_redraw = self.horizontal_line.get_visible() != visible
self.horizontal_line.set_visible(visible)
self.vertical_line.set_visible(visible)
self.text.set_visible(visible)
return need_redraw
def create_new_background(self):
if self._creating_background:
## discard calls triggered from within this function
return
self._creating_background = True
self.set_cross_hair_visible(False)
self.ax.figure.canvas.draw()
self.background = self.ax.figure.canvas.copy_from_bbox(self.ax.bbox)
self.set_cross_hair_visible(True)
self._creating_background = False
def on_mouse_move(self, event):
if self.background is None:
self.create_new_background()
if not event.inaxes:
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.restore_region(self.background)
self.ax.figure.canvas.blit(self.ax.bbox)
else:
self.set_cross_hair_visible(True)
## update the line positions
x, y = event.xdata, event.ydata
self.horizontal_line.set_ydata([y])
self.vertical_line.set_xdata([x])
self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')
self.ax.figure.canvas.restore_region(self.background)
self.ax.draw_artist(self.horizontal_line)
self.ax.draw_artist(self.vertical_line)
self.ax.draw_artist(self.text)
self.ax.figure.canvas.blit(self.ax.bbox)
x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)
fig, ax = plt.subplots()
ax.set_title('Blitted cursor')
ax.plot(x, y, 'o')
blitted_cursor = BlittedCursor(ax)
fig.canvas.mpl_connect('motion_notify_event', blitted_cursor.on_mouse_move)
plt.show()
Snapping to Data Points
The cursor created in the previous step is still a bit slow because it updates the cursor position on every mouse move. In this step, we will create a cursor that snaps to the data points of a Line2D object.
class SnappingCursor:
"""
A cross-hair cursor that snaps to the data point of a line, which is
closest to the *x* position of the cursor.
For simplicity, this assumes that *x* values of the data are sorted.
"""
def __init__(self, ax, line):
self.ax = ax
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
self.x, self.y = line.get_data()
self._last_index = None
## text location in axes coords
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
def set_cross_hair_visible(self, visible):
need_redraw = self.horizontal_line.get_visible() != visible
self.horizontal_line.set_visible(visible)
self.vertical_line.set_visible(visible)
self.text.set_visible(visible)
return need_redraw
def on_mouse_move(self, event):
if not event.inaxes:
self._last_index = None
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.draw()
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
index = min(np.searchsorted(self.x, x), len(self.x) - 1)
if index == self._last_index:
return ## still on the same data point. Nothing to do.
self._last_index = index
x = self.x[index]
y = self.y[index]
## update the line positions
self.horizontal_line.set_ydata([y])
self.vertical_line.set_xdata([x])
self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')
self.ax.figure.canvas.draw()
x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)
fig, ax = plt.subplots()
ax.set_title('Snapping cursor')
line, = ax.plot(x, y, 'o')
snap_cursor = SnappingCursor(ax, line)
fig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)
plt.show()
Summary
In this lab, you learned how to add a crosshair cursor to a Matplotlib plot. You created a simple cursor, a cursor that uses blitting for faster rendering, and a cursor that snaps to data points of a Line2D object. These cursors can be useful for exploring data and gaining insights from visualizations.