matplotlib 3.5.1

>>> """
=================
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()
...