{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Annotated cursor\n\nDisplay a data cursor including a text box, which shows the plot point close\nto the mouse pointer.\n\nThe new cursor inherits from `~matplotlib.widgets.Cursor` and demonstrates the\ncreation of new widgets and their event callbacks.\n\nSee also the :doc:`cross hair cursor\n</gallery/event_handling/cursor_demo>`, which implements a cursor tracking the\nplotted data, but without using inheritance and without displaying the\ncurrently tracked coordinates.\n\n<div class=\"alert alert-info\"><h4>Note</h4><p>The figure related to this example does not show the cursor, because that\n    figure is automatically created in a build queue, where the first mouse\n    movement, which triggers the cursor creation, is missing.</p></div>\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\nfrom matplotlib.widgets import Cursor\n\n\nclass AnnotatedCursor(Cursor):\n    \"\"\"\n    A crosshair cursor like `~matplotlib.widgets.Cursor` with a text showing \\\n    the current coordinates.\n\n    For the cursor to remain responsive you must keep a reference to it.\n    The data of the axis specified as *dataaxis* must be in ascending\n    order. Otherwise, the `numpy.searchsorted` call might fail and the text\n    disappears. You can satisfy the requirement by sorting the data you plot.\n    Usually the data is already sorted (if it was created e.g. using\n    `numpy.linspace`), but e.g. scatter plots might cause this problem.\n    The cursor sticks to the plotted line.\n\n    Parameters\n    ----------\n    line : `matplotlib.lines.Line2D`\n        The plot line from which the data coordinates are displayed.\n\n    numberformat : `python format string <https://docs.python.org/3/\\\n    library/string.html#formatstrings>`_, optional, default: \"{0:.4g};{1:.4g}\"\n        The displayed text is created by calling *format()* on this string\n        with the two coordinates.\n\n    offset : (float, float) default: (5, 5)\n        The offset in display (pixel) coordinates of the text position\n        relative to the cross-hair.\n\n    dataaxis : {\"x\", \"y\"}, optional, default: \"x\"\n        If \"x\" is specified, the vertical cursor line sticks to the mouse\n        pointer. The horizontal cursor line sticks to *line*\n        at that x value. The text shows the data coordinates of *line*\n        at the pointed x value. If you specify \"y\", it works in the opposite\n        manner. But: For the \"y\" value, where the mouse points to, there might\n        be multiple matching x values, if the plotted function is not biunique.\n        Cursor and text coordinate will always refer to only one x value.\n        So if you use the parameter value \"y\", ensure that your function is\n        biunique.\n\n    Other Parameters\n    ----------------\n    textprops : `matplotlib.text` properties as dictionary\n        Specifies the appearance of the rendered text object.\n\n    **cursorargs : `matplotlib.widgets.Cursor` properties\n        Arguments passed to the internal `~matplotlib.widgets.Cursor` instance.\n        The `matplotlib.axes.Axes` argument is mandatory! The parameter\n        *useblit* can be set to *True* in order to achieve faster rendering.\n\n    \"\"\"\n\n    def __init__(self, line, numberformat=\"{0:.4g};{1:.4g}\", offset=(5, 5),\n                 dataaxis='x', textprops=None, **cursorargs):\n        if textprops is None:\n            textprops = {}\n        # The line object, for which the coordinates are displayed\n        self.line = line\n        # The format string, on which .format() is called for creating the text\n        self.numberformat = numberformat\n        # Text position offset\n        self.offset = np.array(offset)\n        # The axis in which the cursor position is looked up\n        self.dataaxis = dataaxis\n\n        # First call baseclass constructor.\n        # Draws cursor and remembers background for blitting.\n        # Saves ax as class attribute.\n        super().__init__(**cursorargs)\n\n        # Default value for position of text.\n        self.set_position(self.line.get_xdata()[0], self.line.get_ydata()[0])\n        # Create invisible animated text\n        self.text = self.ax.text(\n            self.ax.get_xbound()[0],\n            self.ax.get_ybound()[0],\n            \"0, 0\",\n            animated=bool(self.useblit),\n            visible=False, **textprops)\n        # The position at which the cursor was last drawn\n        self.lastdrawnplotpoint = None\n\n    def onmove(self, event):\n        \"\"\"\n        Overridden draw callback for cursor. Called when moving the mouse.\n        \"\"\"\n\n        # Leave method under the same conditions as in overridden method\n        if self.ignore(event):\n            self.lastdrawnplotpoint = None\n            return\n        if not self.canvas.widgetlock.available(self):\n            self.lastdrawnplotpoint = None\n            return\n\n        # If the mouse left drawable area, we now make the text invisible.\n        # Baseclass will redraw complete canvas after, which makes both text\n        # and cursor disappear.\n        if event.inaxes != self.ax:\n            self.lastdrawnplotpoint = None\n            self.text.set_visible(False)\n            super().onmove(event)\n            return\n\n        # Get the coordinates, which should be displayed as text,\n        # if the event coordinates are valid.\n        plotpoint = None\n        if event.xdata is not None and event.ydata is not None:\n            # Get plot point related to current x position.\n            # These coordinates are displayed in text.\n            plotpoint = self.set_position(event.xdata, event.ydata)\n            # Modify event, such that the cursor is displayed on the\n            # plotted line, not at the mouse pointer,\n            # if the returned plot point is valid\n            if plotpoint is not None:\n                event.xdata = plotpoint[0]\n                event.ydata = plotpoint[1]\n\n        # If the plotpoint is given, compare to last drawn plotpoint and\n        # return if they are the same.\n        # Skip even the call of the base class, because this would restore the\n        # background, draw the cursor lines and would leave us the job to\n        # re-draw the text.\n        if plotpoint is not None and plotpoint == self.lastdrawnplotpoint:\n            return\n\n        # Baseclass redraws canvas and cursor. Due to blitting,\n        # the added text is removed in this call, because the\n        # background is redrawn.\n        super().onmove(event)\n\n        # Check if the display of text is still necessary.\n        # If not, just return.\n        # This behaviour is also cloned from the base class.\n        if not self.get_active() or not self.visible:\n            return\n\n        # Draw the widget, if event coordinates are valid.\n        if plotpoint is not None:\n            # Update position and displayed text.\n            # Position: Where the event occurred.\n            # Text: Determined by set_position() method earlier\n            # Position is transformed to pixel coordinates,\n            # an offset is added there and this is transformed back.\n            temp = [event.xdata, event.ydata]\n            temp = self.ax.transData.transform(temp)\n            temp = temp + self.offset\n            temp = self.ax.transData.inverted().transform(temp)\n            self.text.set_position(temp)\n            self.text.set_text(self.numberformat.format(*plotpoint))\n            self.text.set_visible(self.visible)\n\n            # Tell base class, that we have drawn something.\n            # Baseclass needs to know, that it needs to restore a clean\n            # background, if the cursor leaves our figure context.\n            self.needclear = True\n\n            # Remember the recently drawn cursor position, so events for the\n            # same position (mouse moves slightly between two plot points)\n            # can be skipped\n            self.lastdrawnplotpoint = plotpoint\n        # otherwise, make text invisible\n        else:\n            self.text.set_visible(False)\n\n        # Draw changes. Cannot use _update method of baseclass,\n        # because it would first restore the background, which\n        # is done already and is not necessary.\n        if self.useblit:\n            self.ax.draw_artist(self.text)\n            self.canvas.blit(self.ax.bbox)\n        else:\n            # If blitting is deactivated, the overridden _update call made\n            # by the base class immediately returned.\n            # We still have to draw the changes.\n            self.canvas.draw_idle()\n\n    def set_position(self, xpos, ypos):\n        \"\"\"\n        Finds the coordinates, which have to be shown in text.\n\n        The behaviour depends on the *dataaxis* attribute. Function looks\n        up the matching plot coordinate for the given mouse position.\n\n        Parameters\n        ----------\n        xpos : float\n            The current x position of the cursor in data coordinates.\n            Important if *dataaxis* is set to 'x'.\n        ypos : float\n            The current y position of the cursor in data coordinates.\n            Important if *dataaxis* is set to 'y'.\n\n        Returns\n        -------\n        ret : {2D array-like, None}\n            The coordinates which should be displayed.\n            *None* is the fallback value.\n        \"\"\"\n\n        # Get plot line data\n        xdata = self.line.get_xdata()\n        ydata = self.line.get_ydata()\n\n        # The dataaxis attribute decides, in which axis we look up which cursor\n        # coordinate.\n        if self.dataaxis == 'x':\n            pos = xpos\n            data = xdata\n            lim = self.ax.get_xlim()\n        elif self.dataaxis == 'y':\n            pos = ypos\n            data = ydata\n            lim = self.ax.get_ylim()\n        else:\n            raise ValueError(f\"The data axis specifier {self.dataaxis} should \"\n                             f\"be 'x' or 'y'\")\n\n        # If position is valid and in valid plot data range.\n        if pos is not None and lim[0] <= pos <= lim[-1]:\n            # Find closest x value in sorted x vector.\n            # This requires the plotted data to be sorted.\n            index = np.searchsorted(data, pos)\n            # Return none, if this index is out of range.\n            if index < 0 or index >= len(data):\n                return None\n            # Return plot point as tuple.\n            return (xdata[index], ydata[index])\n\n        # Return none if there is no good related point for this x position.\n        return None\n\n    def clear(self, event):\n        \"\"\"\n        Overridden clear callback for cursor, called before drawing the figure.\n        \"\"\"\n\n        # The base class saves the clean background for blitting.\n        # Text and cursor are invisible,\n        # until the first mouse move event occurs.\n        super().clear(event)\n        if self.ignore(event):\n            return\n        self.text.set_visible(False)\n\n    def _update(self):\n        \"\"\"\n        Overridden method for either blitting or drawing the widget canvas.\n\n        Passes call to base class if blitting is activated, only.\n        In other cases, one draw_idle call is enough, which is placed\n        explicitly in this class (see *onmove()*).\n        In that case, `~matplotlib.widgets.Cursor` is not supposed to draw\n        something using this method.\n        \"\"\"\n\n        if self.useblit:\n            super()._update()\n\n\nfig, ax = plt.subplots(figsize=(8, 6))\nax.set_title(\"Cursor Tracking x Position\")\n\nx = np.linspace(-5, 5, 1000)\ny = x**2\n\nline, = ax.plot(x, y)\nax.set_xlim(-5, 5)\nax.set_ylim(0, 25)\n\n# A minimum call\n# Set useblit=True on most backends for enhanced performance\n# and pass the ax parameter to the Cursor base class.\n# cursor = AnnotatedCursor(line=lin[0], ax=ax, useblit=True)\n\n# A more advanced call. Properties for text and lines are passed.\n# Watch the passed color names and the color of cursor line and text, to\n# relate the passed options to graphical elements.\n# The dataaxis parameter is still the default.\ncursor = AnnotatedCursor(\n    line=line,\n    numberformat=\"{0:.2f}\\n{1:.2f}\",\n    dataaxis='x', offset=[10, 10],\n    textprops={'color': 'blue', 'fontweight': 'bold'},\n    ax=ax,\n    useblit=True,\n    color='red',\n    linewidth=2)\n\n# Simulate a mouse move to (-2, 10), needed for online docs\nt = ax.transData\nMouseEvent(\n    \"motion_notify_event\", ax.figure.canvas, *t.transform((-2, 10))\n)._process()\n\nplt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Trouble with non-biunique functions\nA call demonstrating problems with the *dataaxis=y* parameter.\nThe text now looks up the matching x value for the current cursor y position\ninstead of vice versa. Hover your cursor to y=4. There are two x values\nproducing this y value: -2 and 2. The function is only unique,\nbut not biunique. Only one value is shown in the text.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "fig, ax = plt.subplots(figsize=(8, 6))\nax.set_title(\"Cursor Tracking y Position\")\n\nline, = ax.plot(x, y)\nax.set_xlim(-5, 5)\nax.set_ylim(0, 25)\n\ncursor = AnnotatedCursor(\n    line=line,\n    numberformat=\"{0:.2f}\\n{1:.2f}\",\n    dataaxis='y', offset=[10, 10],\n    textprops={'color': 'blue', 'fontweight': 'bold'},\n    ax=ax,\n    useblit=True,\n    color='red', linewidth=2)\n\n# Simulate a mouse move to (-2, 10), needed for online docs\nt = ax.transData\nMouseEvent(\n    \"motion_notify_event\", ax.figure.canvas, *t.transform((-2, 10))\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.14.6"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}