[docs]class MultiAxesLabelCursor(object):
    """
    A label for a matplotlib plot that follows the cursor.  A vertical line is
    drawn at the cursor, and two columns of text are placed at the top of the
    plot describing the current x-value.  This class allows the cursor to span
    multiple axes in the same canvas.  See `LabelCursor` for a single axes
    convenience class.
    :ivar _needclear: Do we need to make the label invisible if the user
        moves off the plot?  (i.e. Is the label currently visible?)
    :vartype _needclear: bool
    :ivar _first_draw: Will the next drawing of the text be the first one?  Note
        that drawing the vertical line without rendering the text doesn't count,
        since that won't set the height and width of the text box.
    :vartype _first_draw: bool
    :ivar _on_draw_cid: The callback ID for the _onDraw call.  (Used so we
        can later disconnect the callback.)
    """
[docs]    def __init__(self, labels, index_func=None):
        """
        :param labels: A list of `LabelCursorAxes` objects for each set of axes
            to be labeled
        :type labels: list
        :param index_func: A function for calculating the appropriate
            `LabelCursorAxes` left_label_data/right_label_data index from an x data
            coordinate.  Defaults to rounding the x coordinate to the nearest int.
        :type index_func: function
        """
        self._labels = labels
        if index_func is None:
            self._index_func = lambda x: int(round(x))
        else:
            self._index_func = index_func
        self.ax_list = [cur_label.ax for cur_label in labels]
        self.canvas = labels[0].ax.figure.canvas
        self.canvas.mpl_connect('motion_notify_event', self.onMove)
        self._needclear = False
        self._first_draw = True
        self._on_draw_cid = None 
[docs]    def set_visible(self, visible):
        """
        Set the visibility of the cursor.  (Note that this function is
        set_visible() rather than setVisible() to match matplotlib's function.)
        :param visible: What the visibility should be set to
        :type visible: bool
        """
        for cur_label in self._labels:
            cur_label.set_visible(visible)
        self._needclear = visible 
    def _setVisibleAlpha(self, visible):
        """
        Set the visibility of the cursor using alpha transparency rather than
        set_visible().  A matplotlib element that is invisible due to alpha
        transparency is laid out, while an element that is set_visible(False) is
        not.  In this class, this function is used to allow text box width to be
        calculated without actually displaying an incorrectly placed text box.
        :param visible: What the visibility should be set to
        :type visible: bool
        """
        alpha = None if visible else 0
        for cur_label in self._labels:
            cur_label.set_alpha(alpha)
    def _updateLabel(self, event):
        """
        Update the positions of the text blocks and the vertical line
        :param event: The move event that triggered this callback
        :type event: `matplotlib.backend_bases.LocationEvent`
        :return: The index to the data lists (left_label_data and
            right_label_data) for the currently labeled point.
        :rtype: int
        """
        idx = self._index_func(event.xdata)
        for cur_label in self._labels:
            cur_label.updateLabel(event.x, event.xdata, idx)
        return idx
[docs]    def onMove(self, event):
        """
        If the user moves the mouse inside of a labeled axes, draw the labels.
        If the user moves the mouse outside of the axes, erase the labels.
        :param event: The move event that triggered this callback
        :type event: `matplotlib.backend_bases.LocationEvent`
        """
        if event.inaxes in self.ax_list:
            idx = self._updateLabel(event)
            self.set_visible(True)
            if self._first_draw and self._labels[0].labelDrawn(idx):
                # See self.on Draw() for an explanation
                self._setVisibleAlpha(False)
                callback_func = lambda x: self.onDraw(event)
                self._on_draw_cid = self.canvas.mpl_connect(
                    'draw_event', callback_func)
                self._first_draw = False
        elif self._needclear:
            self.set_visible(False)
        self.canvas.draw_idle() 
[docs]    def onDraw(self, event):
        """
        After the first time we draw the labels (and *only* the first time),
        immediately re-draw them.  We do this because we can't possibly know the
        width of the text before the first draw, but we can't position the text
        correctly unless we know the width.  To get around this catch 22, we
        initially draw the labels as invisible using alpha transparency.  The
        alpha transparency prevents the user from seeing the incorrectly placed
        labels, but still allows us to determine their width.  Immediately
        following this invisible draw, we re-calculate the text positions (now
        that we know their width), make the labels visible, and re-draw.
        As this function only needs to be called after the first draw of the
        labels, it removes its own callback
        :param event: The move event that happened immediately before this draw.
            Note that this is *not* the draw event.
        :type event: `matplotlib.backend_bases.LocationEvent`
        """
        self._updateLabel(event)
        self.set_visible(True)
        self._setVisibleAlpha(True)
        self.canvas.mpl_disconnect(self._on_draw_cid)
        self.canvas.draw_idle()  
[docs]class LabelCursorAxes(object):
    """
    A label for a matplotlib plot that follows the cursor.  A vertical line is
    drawn at the cursor, and two columns of text are placed at the top of the
    plot describing the current x-value.
    :cvar Z_INDEX: The starting Z-index for the matplotlib elements
    :vartype Z_INDEX: int
    :cvar OFFSET: The spacing placed around the text boxes
    :vartype OFFSET: int
    :ivar _max_width: The max width of the left and right labels that
        we've seen.
    :vartype _max_width: list
    :ivar _text_min: The cached value of the y-coordinate of the bottom of the
        left and right labels in axis coordinates.
    :vartype _text_min: list
    """
    Z_INDEX = 10
    OFFSET = 4
[docs]    def __init__(self,
                 ax,
                 left_label_text,
                 left_label_data,
                 right_label_text,
                 right_label_data,
                 skip=None,
                 font_size=None,
                 text_y=0.98,
                 line_width=2,
                 line_color="blue"):
        """
        :param ax: The matplotlib axes that this cursor should appear on
        :type ax: `matplotlib.axes.AxesSubplot`
        :param left_label_text: The string to display in the left column of text
        :type left_label_text: str
        :param left_label_data: The data to interpolate into left_label_text.
            For any given x-value, left_label_data[x] will be interpolated.
        :type left_label_data: list or dict
        :param right_label_text: The string to display in the right column of
            text
        :type right_label_text: str
        :param right_label_data: The data to interpolate into right_label_text.
            For any given x-value, right_label_data[x] will be interpolated.
        :type right_label_data: list or dict
        :param skip: A list of indices to not display the label for.  Defaults
            to displaying the labels for all indices.  (Note that indices that
            aren't present in left_label_data and right_label_data will be skipped
            automatically, regardless of this list.)
        :type skip: list or None
        :param font_size: The font size for the label.  May be an absolute font
            size in points or a size string (e.g. "small", "xx-large").  Defaults to
            the default Matplotlib font size.
        :type font_size: NoneType, int, or str
        :param text_y: The vertical location of the top of the text boxes,
            ranging from 0 to 1, with 1 being the top of the axes.
        :type text_y: float
        :param line_width: The width of the vertical line
        :type line_width: int
        :param line_color: The color of the vertical line
        :type line_color: str
        """
        self.ax = ax
        self.left_label_text = left_label_text
        self.left_label_data = left_label_data
        self.right_label_text = right_label_text
        self.right_label_data = right_label_data
        if skip is not None:
            self.skip = skip
        else:
            self.skip = []
        self.vert_line = self.ax.axvline(0,
                                         visible=False,
                                         zorder=self.Z_INDEX,
                                         color=line_color,
                                         linewidth=line_width)
        self.lannotation = self.ax.text(0,
                                        text_y,
                                        "",
                                        visible=False,
                                        zorder=self.Z_INDEX + 1,
                                        transform=self.ax.transAxes,
                                        va="top",
                                        ha="right",
                                        size=font_size)
        self.rannotation = self.ax.text(0,
                                        text_y,
                                        "",
                                        visible=False,
                                        zorder=self.Z_INDEX + 2,
                                        transform=self.ax.transAxes,
                                        va="top",
                                        size=font_size)
        self._max_width = [float("-inf"), float("-inf")]
        self._text_min = [1, 1] 
[docs]    def set_visible(self, visible):
        """
        Set the visibility of the cursor.  (Note that this function is
        set_visible() rather than setVisible() to match matplotlib's function.)
        :param visible: What the visibility should be set to
        :type visible: bool
        """
        self.vert_line.set_visible(visible)
        self.lannotation.set_visible(visible)
        self.rannotation.set_visible(visible) 
[docs]    def set_alpha(self, alpha):
        """
        Set the alpha transparency of the cursor.  (Note that this function is
        set_alpha() rather than setAlpha() to match matplotlib's function.)
        :param alpha: What the alpha transparency should be set to
        :type alpha: float, int, or NoneType
        """
        self.vert_line.set_alpha(alpha)
        self.lannotation.set_alpha(alpha)
        self.rannotation.set_alpha(alpha) 
[docs]    def updateLabel(self, x, xdata, idx):
        """
        Update the positions of the text blocks and the vertical line
        :param x: The x coordinate of the cursor in canvas pixel coordinates
        :type x: int
        :param xdata: The x coordinate of the cursor in data coordinates
        :type xdata: float
        :param idx: The current index for left_label_data and right_label_data
        :type idx: int
        """
        left_label = self._interpolateText(self.left_label_text,
                                           self.left_label_data, idx)
        right_label = self._interpolateText(self.right_label_text,
                                            self.right_label_data, idx)
        (x_l, x_r, line_top) = self._calcTextCoords(x)
        if not self.labelDrawn(idx):
            line_top = 1
        self.lannotation.set_x(x_l)
        self.lannotation.set_text(left_label)
        self.rannotation.set_x(x_r)
        self.rannotation.set_text(right_label)
        self.vert_line.set_xdata((xdata, xdata))
        self.vert_line.set_ydata((0, line_top)) 
[docs]    def labelDrawn(self, idx):
        """
        Will the text labels be drawn for the specified index?  An index for
        which there's no valid data or one that's on the skip list won't be
        drawn.
        :param idx: The specified index (i.e. index to left_label_data and
            right_label_data).
        :type idx: int
        :return: Will the text labels be drawn for the specified index?
        :rtype: bool
        """
        try:
            # Assume that left_label_data and right_label_data contain the same
            # entries, so only bother to check one.
            self.left_label_data[idx]
        except LookupError:
            return False
        return idx not in self.skip 
    def _calcTextCoords(self, event_x):
        """
        Calculate the x-coordinates of the text blocks and the y-coordinate of
        the top of the vertical line.
        :param event_x: The x coordinate (in the display coordinate system) of
            the mouse cursor
        :type event_x: float
        :return: A list of (The x-coordinate of the left text block, The
            x-coordinate of the right text block, The y-coordinate of the top
            of the vertical line). Note that all return values are in the axes
            coordinate system.
        :rtype: tuple
        """
        trans_axes = self.ax.transAxes
        trans_x = lambda x: trans_axes.transform((x, 0))[0]
        inv_trans_x = lambda x: trans_axes.inverted().transform((x, 0))[0]
        x_zero = trans_x(0)
        x_one = trans_x(1)
        width_l = self._getMaxWidth(self.lannotation, 0)
        width_r = self._getMaxWidth(self.rannotation, 1)
        if event_x - 2 * self.OFFSET - width_l < x_zero:
            # If the label should be pinned to the left side of the plot
            x_l_display = x_zero + width_l + self.OFFSET
            x_r_display = x_zero + width_l + 3 * self.OFFSET
            line_top = self._getTextMin(self.lannotation, 0)
        elif event_x + 2 * self.OFFSET + width_r > x_one:
            # If the label should be pinned to the right side of the plot
            x_l_display = x_one - width_r - 3 * self.OFFSET
            x_r_display = x_one - width_r - self.OFFSET
            line_top = self._getTextMin(self.rannotation, 1)
        else:
            # If the label should follow the cursor
            x_l_display = event_x - self.OFFSET
            x_r_display = event_x + self.OFFSET
            line_top = 1
        x_l_axes = inv_trans_x(x_l_display)
        x_r_axes = inv_trans_x(x_r_display)
        return (x_l_axes, x_r_axes, line_top)
    def _getTextMin(self, annotation, annotation_idx):
        """
        Determine the y-coordinate of the bottom of the specified text block.
        Used to decide where to cut off the vertical line.  If the text box was
        not drawn last update (and therefore the height of the text block is
        unknown), then a cached value will be used.
        :param annotation: The text block
        :type annotation: `matplotlib.text.Text`
        :param annotation_idx: The index of the cached value in self._text_min.
            Should be 0 for the left text block and 1 for the right text block.
        :type annotation_idx: int
        :return: The y-coordinate of the bottom of the specified text block in
            the axes coordinate system
        :rtype: float
        """
        bbox = annotation.get_window_extent()
        if bbox.height == 0.0 or (bbox.height == 1.0 and bbox.ymin == 0.0):
            # If the text block was not drawn last update, then use the cached
            # value
            return self._text_min[annotation_idx]
        else:
            ymin = annotation.get_window_extent().ymin
            ymin -= self.OFFSET
            ymin_axes = self.ax.transAxes.inverted().transform((0, ymin))[1]
            self._text_min[annotation_idx] = ymin_axes
            return ymin_axes
    def _getMaxWidth(self, annotation, index):
        """
        Get the maximum width seen so far for the specified text block.  We use
        the maximum width to decide where/when to pin the label when the cursor
        is at the side of the plot.  If we used the current width instead, the
        label would shift back and forth by several pixels as the width of the
        label changed due to different number widths.
        :param annotation: The text block
        :type annotation: `matplotlib.text.Text`
        :param index: The index of self._max_width to access.  Should be 0 for
            the left text block and 1 for the right text block.
        :type index: int
        :return: The maximum width for the specified text block
        :rtype: float
        """
        cur_width = annotation.get_window_extent().width
        if cur_width > self._max_width[index]:
            self._max_width[index] = cur_width
        return self._max_width[index]
    def _interpolateText(self, text, data, idx):
        """
        Interpolate the given string with data from the specified list or
        dictionary.  If no data is found in the list/dictionary or the index
        is on the skip list, return a blank string.
        :param text: The string to interpolate into
        :type text: str
        :param data: The data to interpolate
        :type data: list or dict
        :param idx: The index of data to access
        :type idx: int
        :return: The interpolated string, or a blank string if no data is found
            or if idx is in the skip list.
        :rtype: str
        """
        if idx in self.skip:
            return ""
        try:
            return text % data[idx]
        except LookupError:
            return "" 
[docs]class LabelCursor(MultiAxesLabelCursor):
    """
    A convenience class for using a MultiAxesLabelCursor on a single set of axes
    """
[docs]    def __init__(self,
                 ax,
                 left_label_text,
                 left_label_data,
                 right_label_text,
                 right_label_data,
                 skip=None,
                 font_size=None,
                 text_y=0.98,
                 line_width=2,
                 line_color="blue",
                 index_func=None):
        """
        See `LabelCursorAxes.__init__` for documentation for all arguments
        other than `index_func`.  See `MultiAxesLabelCursor.__init__` for
        documentation on `index_func`.
        """
        axes = LabelCursorAxes(ax, left_label_text, left_label_data,
                               right_label_text, right_label_data, skip,
                               font_size, text_y, line_width, line_color)
        super(LabelCursor, self).__init__([axes], index_func)