{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n# Cross-hair cursor\n\nThis example adds a cross-hair as a data cursor. The cross-hair is\nimplemented as regular line objects that are updated on mouse move.\n\nWe show three implementations:\n\n1) A simple cursor implementation that redraws the figure on every mouse move.\n This is a bit slow, and you may notice some lag of the cross-hair movement.\n2) A cursor that uses blitting for speedup of the rendering.\n3) A cursor that snaps to data points.\n\nFaster cursoring is possible using native GUI drawing, as in\n:doc:`/gallery/user_interfaces/wxcursor_demo_sgskip`.\n\nThe mpldatacursor__ and mplcursors__ third-party packages can be used to\nachieve a similar effect.\n\n__ https://github.com/joferkington/mpldatacursor\n__ https://github.com/anntzer/mplcursors\n\n.. redirect-from:: /gallery/misc/cursor_demo\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\nimport numpy as np\n\nfrom matplotlib.backend_bases import MouseEvent\n\n\nclass Cursor:\n \"\"\"\n A cross hair cursor.\n \"\"\"\n def __init__(self, ax):\n self.ax = ax\n self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')\n self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')\n # text location in axes coordinates\n self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)\n\n def set_cross_hair_visible(self, visible):\n need_redraw = self.horizontal_line.get_visible() != visible\n self.horizontal_line.set_visible(visible)\n self.vertical_line.set_visible(visible)\n self.text.set_visible(visible)\n return need_redraw\n\n def on_mouse_move(self, event):\n if not event.inaxes:\n need_redraw = self.set_cross_hair_visible(False)\n if need_redraw:\n self.ax.figure.canvas.draw()\n else:\n self.set_cross_hair_visible(True)\n x, y = event.xdata, event.ydata\n # update the line positions\n self.horizontal_line.set_ydata([y])\n self.vertical_line.set_xdata([x])\n self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')\n self.ax.figure.canvas.draw()\n\n\nx = np.arange(0, 1, 0.01)\ny = np.sin(2 * 2 * np.pi * x)\n\nfig, ax = plt.subplots()\nax.set_title('Simple cursor')\nax.plot(x, y, 'o')\ncursor = Cursor(ax)\nfig.canvas.mpl_connect('motion_notify_event', cursor.on_mouse_move)\n\n# Simulate a mouse move to (0.5, 0.5), needed for online docs\nt = ax.transData\nMouseEvent(\n \"motion_notify_event\", ax.figure.canvas, *t.transform((0.5, 0.5))\n)._process()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Faster redrawing using blitting\nThis technique stores the rendered plot as a background image. Only the\nchanged artists (cross-hair lines and text) are rendered anew. They are\ncombined with the background using blitting.\n\nThis technique is significantly faster. It requires a bit more setup because\nthe background has to be stored without the cross-hair lines (see\n``create_new_background()``). Additionally, a new background has to be\ncreated whenever the figure changes. This is achieved by connecting to the\n``'draw_event'``.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class BlittedCursor:\n \"\"\"\n A cross-hair cursor using blitting for faster redraw.\n \"\"\"\n def __init__(self, ax):\n self.ax = ax\n self.background = None\n self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')\n self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')\n # text location in axes coordinates\n self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)\n self._creating_background = False\n ax.figure.canvas.mpl_connect('draw_event', self.on_draw)\n\n def on_draw(self, event):\n self.create_new_background()\n\n def set_cross_hair_visible(self, visible):\n need_redraw = self.horizontal_line.get_visible() != visible\n self.horizontal_line.set_visible(visible)\n self.vertical_line.set_visible(visible)\n self.text.set_visible(visible)\n return need_redraw\n\n def create_new_background(self):\n if self._creating_background:\n # discard calls triggered from within this function\n return\n self._creating_background = True\n self.set_cross_hair_visible(False)\n self.ax.figure.canvas.draw()\n self.background = self.ax.figure.canvas.copy_from_bbox(self.ax.bbox)\n self.set_cross_hair_visible(True)\n self._creating_background = False\n\n def on_mouse_move(self, event):\n if self.background is None:\n self.create_new_background()\n if not event.inaxes:\n need_redraw = self.set_cross_hair_visible(False)\n if need_redraw:\n self.ax.figure.canvas.restore_region(self.background)\n self.ax.figure.canvas.blit(self.ax.bbox)\n else:\n self.set_cross_hair_visible(True)\n # update the line positions\n x, y = event.xdata, event.ydata\n self.horizontal_line.set_ydata([y])\n self.vertical_line.set_xdata([x])\n self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')\n\n self.ax.figure.canvas.restore_region(self.background)\n self.ax.draw_artist(self.horizontal_line)\n self.ax.draw_artist(self.vertical_line)\n self.ax.draw_artist(self.text)\n self.ax.figure.canvas.blit(self.ax.bbox)\n\n\nx = np.arange(0, 1, 0.01)\ny = np.sin(2 * 2 * np.pi * x)\n\nfig, ax = plt.subplots()\nax.set_title('Blitted cursor')\nax.plot(x, y, 'o')\nblitted_cursor = BlittedCursor(ax)\nfig.canvas.mpl_connect('motion_notify_event', blitted_cursor.on_mouse_move)\n\n# Simulate a mouse move to (0.5, 0.5), needed for online docs\nt = ax.transData\nMouseEvent(\n \"motion_notify_event\", ax.figure.canvas, *t.transform((0.5, 0.5))\n)._process()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Snapping to data points\nThe following cursor snaps its position to the data points of a `.Line2D`\nobject.\n\nTo save unnecessary redraws, the index of the last indicated data point is\nsaved in ``self._last_index``. A redraw is only triggered when the mouse\nmoves far enough so that another data point must be selected. This reduces\nthe lag due to many redraws. Of course, blitting could still be added on top\nfor additional speedup.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class SnappingCursor:\n \"\"\"\n A cross-hair cursor that snaps to the data point of a line, which is\n closest to the *x* position of the cursor.\n\n For simplicity, this assumes that *x* values of the data are sorted.\n \"\"\"\n def __init__(self, ax, line):\n self.ax = ax\n self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')\n self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')\n self.x, self.y = line.get_data()\n self._last_index = None\n # text location in axes coords\n self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)\n\n def set_cross_hair_visible(self, visible):\n need_redraw = self.horizontal_line.get_visible() != visible\n self.horizontal_line.set_visible(visible)\n self.vertical_line.set_visible(visible)\n self.text.set_visible(visible)\n return need_redraw\n\n def on_mouse_move(self, event):\n if not event.inaxes:\n self._last_index = None\n need_redraw = self.set_cross_hair_visible(False)\n if need_redraw:\n self.ax.figure.canvas.draw()\n else:\n self.set_cross_hair_visible(True)\n x, y = event.xdata, event.ydata\n index = min(np.searchsorted(self.x, x), len(self.x) - 1)\n if index == self._last_index:\n return # still on the same data point. Nothing to do.\n self._last_index = index\n x = self.x[index]\n y = self.y[index]\n # update the line positions\n self.horizontal_line.set_ydata([y])\n self.vertical_line.set_xdata([x])\n self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')\n self.ax.figure.canvas.draw()\n\n\nx = np.arange(0, 1, 0.01)\ny = np.sin(2 * 2 * np.pi * x)\n\nfig, ax = plt.subplots()\nax.set_title('Snapping cursor')\nline, = ax.plot(x, y, 'o')\nsnap_cursor = SnappingCursor(ax, line)\nfig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)\n\n# Simulate a mouse move to (0.5, 0.5), needed for online docs\nt = ax.transData\nMouseEvent(\n \"motion_notify_event\", ax.figure.canvas, *t.transform((0.5, 0.5))\n)._process()\n\nplt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.2" } }, "nbformat": 4, "nbformat_minor": 0 }