""" ================= Cross hair cursor ================= This example adds a cross hair as a data cursor. The cross hair is implemented as regular line objects that are updated on mouse move. We show three implementations: 1) A simple cursor implementation that redraws the figure on every mouse move. This is a bit slow and you may notice some lag of the cross hair movement. 2) A cursor that uses blitting for speedup of the rendering. 3) A cursor that snaps to data points. Faster cursoring is possible using native GUI drawing, as in :doc:`/gallery/user_interfaces/wxcursor_demo_sgskip`. The mpldatacursor__ and mplcursors__ third-party packages can be used to achieve a similar effect. __ https://github.com/joferkington/mpldatacursor __ https://github.com/anntzer/mplcursors """ import matplotlib.pyplot as plt import numpy as np 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('x=%1.2f, y=%1.2f' % (x, y)) 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) ############################################################################## # Faster redrawing using blitting # """"""""""""""""""""""""""""""" # This technique stores the rendered plot as a background image. Only the # changed artists (cross hair lines and text) are rendered anew. They are # combined with the background using blitting. # # This technique is significantly faster. It requires a bit more setup because # the background has to be stored without the cross hair lines (see # ``create_new_background()``). Additionally, a new background has to be # created whenever the figure changes. This is achieved by connecting to the # ``'draw_event'``. 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('x=%1.2f, y=%1.2f' % (x, y)) 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) ############################################################################## # Snapping to data points # """"""""""""""""""""""" # The following cursor snaps its position to the data points of a `.Line2D` # object. # # To save unnecessary redraws, the index of the last indicated data point is # saved in ``self._last_index``. A redraw is only triggered when the mouse # moves far enough so that another data point must be selected. This reduces # the lag due to many redraws. Of course, blitting could still be added on top # for additional speedup. 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('x=%1.2f, y=%1.2f' % (x, y)) 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()