diff --git a/examples/misc/cursor_demo.py b/examples/misc/cursor_demo.py new file mode 100644 index 000000000000..0be812508026 --- /dev/null +++ b/examples/misc/cursor_demo.py @@ -0,0 +1,219 @@ +""" +================= +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 is 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() diff --git a/examples/misc/cursor_demo_sgskip.py b/examples/misc/cursor_demo_sgskip.py deleted file mode 100644 index cfcfdd74eee1..000000000000 --- a/examples/misc/cursor_demo_sgskip.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -=========== -Cursor Demo -=========== - -This example shows how to use Matplotlib to provide a data cursor. It uses -Matplotlib to draw the cursor and may be a slow since this requires redrawing -the figure with every mouse move. - -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: - def __init__(self, ax): - self.ax = ax - self.lx = ax.axhline(color='k') # the horiz line - self.ly = ax.axvline(color='k') # the vert line - - # text location in axes coords - self.txt = ax.text(0.7, 0.9, '', transform=ax.transAxes) - - def on_mouse_move(self, event): - if not event.inaxes: - return - - x, y = event.xdata, event.ydata - # update the line positions - self.lx.set_ydata(y) - self.ly.set_xdata(x) - - self.txt.set_text('x=%1.2f, y=%1.2f' % (x, y)) - self.ax.figure.canvas.draw() - - -class SnaptoCursor: - """ - Like Cursor but the crosshair snaps to the nearest x, y point. - For simplicity, this assumes that *x* is sorted. - """ - - def __init__(self, ax, x, y): - self.ax = ax - self.lx = ax.axhline(color='k') # the horiz line - self.ly = ax.axvline(color='k') # the vert line - self.x = x - self.y = y - # text location in axes coords - self.txt = ax.text(0.7, 0.9, '', transform=ax.transAxes) - - def on_mouse_move(self, event): - if not event.inaxes: - return - - x, y = event.xdata, event.ydata - indx = min(np.searchsorted(self.x, x), len(self.x) - 1) - x = self.x[indx] - y = self.y[indx] - # update the line positions - self.lx.set_ydata(y) - self.ly.set_xdata(x) - - self.txt.set_text('x=%1.2f, y=%1.2f' % (x, y)) - print('x=%1.2f, y=%1.2f' % (x, y)) - self.ax.figure.canvas.draw() - - -t = np.arange(0.0, 1.0, 0.01) -s = np.sin(2 * 2 * np.pi * t) - -fig, ax = plt.subplots() -ax.plot(t, s, 'o') -cursor = Cursor(ax) -fig.canvas.mpl_connect('motion_notify_event', cursor.on_mouse_move) - -fig, ax = plt.subplots() -ax.plot(t, s, 'o') -snap_cursor = SnaptoCursor(ax, t, s) -fig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move) - -plt.show()