Source code for schrodinger.ui.qt.label_cursor

[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)