Source code for schrodinger.application.bioluminate.sliderchart

"""
Tools for using matplotlib charts.

Copyright Schrodinger, LLC. All rights reserved.

"""

# Contributors: Dave Giesen

import os
import sys
from past.utils import old_div

import numpy

from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import smatplotlib
from schrodinger.ui.qt import swidgets

COLOR_NAME = [
    "black", "red", "green", "blue", "purple", "yellow", "orange", "violet",
    "skyblue", "gold", "grey"
]

MARKER_TRANSLATION = {
    "cross": "+",
    "rectangle": "s",
    "diamond": "d",
    "circle": "o",
    "square": "s",
    "x": "x",
    "arrow": "^"
}


[docs]def prevent_overlapping_x_labels(canvas, axes_number=0): """ Given a canvas that contains a figure that contains at least one axes instance, checks the x-axis tick labels to make sure they don't overlap. If they do, the number of ticks is reduced until there is no overlap. :type canvas: matplotlib canvas object :param canvas: the canvas that contains the figure/axes objects :type axes_number: int :param axes_number: the index of the axes on the figure to examine. Default is 0, which is the first set of axis added to the figure. """ # Force the canvas to draw, or it won't have determined the tick marks canvas.draw() xaxis = canvas.figure.get_axes()[axes_number].get_xaxis() labels = xaxis.get_majorticklabels() overlap = True # matplotlib will insist on keeping a minimum number of labels (some of # which may be blank), so to be safe we need to set a maximum number of # times we'll try to reduce the number of labels. max_tries = len(labels) - 1 tries = 0 while overlap and tries < max_tries: tries = tries + 1 overlap = False for index, label in enumerate(labels): if index: try: old_label_pos = \ labels[index - 1].get_window_extent().get_points() new_label_pos = \ labels[index].get_window_extent().get_points() except RuntimeError: # MATSCI-5918/PANEL-12805 due to # https://github.com/matplotlib/matplotlib/issues/10874 return # For unknown reason, sometimes there are labels of zero area. # Excludes such labels from comparison. if (old_label_pos[0][0] == old_label_pos[1][0] or new_label_pos[0][0] == new_label_pos[1][0]): continue # Determine if the left side of this label overlaps the right # side of the previous label old_right_x = old_label_pos[1][0] new_left_x = new_label_pos[0][0] if label.get_text(): if new_left_x - old_right_x < 1: overlap = True break if overlap: # Reduce the number of ticks by one to make room for the labels locator = xaxis.get_major_locator() # Number of bins = number of ticks - 1, so to get one fewer tick, we # need two fewer bins than the current number of ticks. locator.set_params(nbins=len(labels) - 2) # Must reset the ticks - matplotlib leaves an extra tick at the end # when it figures out new ticks. This at least makes sure the last # tick is blank. xaxis.reset_ticks() canvas.draw() labels = xaxis.get_majorticklabels()
[docs]class SliderPlot(QtWidgets.QFrame): """ A chart that contains four lines the user can slide to select a region of the chart. """
[docs] def __init__(self, xvals=None, yvals=None, size=(400, 400), x_label='Residue Sequence', y_label='Values', x_range=None, y_range=None, color='black', cvals=None, bg='white', title='Results', fontsize='small', chart_type='line', marker='square', marker_size=9, shade_color='0.70', hslider_color='red', vslider_color='blue', slider_thickness=3, slider_pick_tolerance=7, slider_moved_callback=None, use_hsliders=True, xstart=0.2, xextent=None, subplot_pos=None): """ Create a SliderPlot instance. The plot is returned in a QFrame widget. :type xvals: list :param xvals: the x values to plot :type yvals: list :param yvals: y series to plot, should be the same length as xvals :type size: tuple :param size: (x, y) plot size in pixels :type x_label: str :param x_label: X-axis label :type y_label: str :param y_label: Y-axis label :type x_range: tuple :param x_range: (min, max) values for the X-axis, default is to show all values :type y_range: tuple :param y_range: (min, max) values for the Y-axis, default is to show all values :type color: str :param color: color for the line plot - this is overridden by cvals if cvals is given. :type cvals: list, tuple or str :param cvals: For scatterplots, either a list or tuple of color values for every point, or a string to set a single color for all points. Do not use an RGB list or tuple to set a single color for all points, as that will be interpreted as intended to set individual colors for 3 (or 4 for RGBA) points. This overrides the value of color, and is not used for line plots, only scatter plots. If not given, the value of color is used. :type bg: str :param bg: color name for the plot background. See marker:color for some color names. :type title: str :param title: the title of the plot :type chart_type: str :param chart_type: 'line' if the chart is a line plot (default), 'scatter' if the chart is scatterplot :type shade_color: str :param shade_color: A matplotlib-recognized color string that the unselected areas will be shaded :type hslider_color: str :param hslider_color: A matplotlib-recognized color string that the horizontal sliders will be colored :type vslider_color: str :param vslider_color: A matplotlib-recognized color string that the vertical sliders will be colored :type slider_thickness: int :param slider_thickness: Linewidth of the slider lines :type slider_pick_tolerance: int :param slider_pick_tolerance: Number of pixels the mouse click can be off and still grab the slider :type slider_moved_callback: callable :param slider_moved_callback: Called when one of the slider lines has been moved. The callback will receive the SlidableLine object that was moved. :type marker: tuple :param marker: tuple of (symbol, color, size), only used for scatter plots Marker names are: - symbol (1-character str) - s - square ('square', rectangle accepted) - o - circle ('circle' accepted) - ^ - triangle up ('arrow' accepted) - > - triangle right - < - triangle left - v - triangle down - d - diamond ('diamond' accepted) - p - pentagon - h - hexagon - 8 - octagon - + - plus ('cross' accepted) - x - x :type marker_size: int :param marker_size: size of the marker :type fontsize: int or str :param fontsize: size in points, or one of the following - - xx-small - x-small - small - medium - large - x-large - xx-large :type use_hsliders: bool :param use_hsliders: horizontal slider is enabled, if True :xstart: float :param xstart: left percentage margin of the figure :type xextent: float of None :param xextent: if float, right percentage margin of the figure :param int subplot_pos: A three digit integer, where the first digit is the number of rows, the second the number of columns, and the third the index of the current subplot. Index goes left to right, followed but top to bottom. Hence in a 4 grid, top-left is 1, top-right is 2 bottom left is 3 and bottom right is 4. Subplot overrides x_start, y_start, x_end, and y_end. :rtype: QFrame :return: The QFrame widget that contains the plot """ QtWidgets.QFrame.__init__(self) # Grab some of the keywords, the rest will be passed on dpi = 100 plotsize = size width = old_div(plotsize[0], dpi) height = old_div(plotsize[1], dpi) layout = swidgets.SVBoxLayout(self) canvas = smatplotlib.SmatplotlibCanvas(width=width, height=height, dpi=dpi, layout=layout) self.figure = canvas.figure self.figure.set_facecolor(bg) symbol = MARKER_TRANSLATION.get(marker, 'square') if not xextent: xextent = 1.0 - (xstart + .10) self.subplot_pos = subplot_pos if self.subplot_pos: self.plot = self.figure.add_subplot(subplot_pos) else: self.plot = self.figure.add_axes([xstart, .2, xextent, .7]) self.plot.set_facecolor(bg) self.plot.tick_params(labelsize=fontsize) # Remove the axis lines and ticks on the top and right self.use_hsliders = use_hsliders if self.use_hsliders: self.plot.spines['right'].set_color('none') self.plot.spines['top'].set_color('none') self.plot.xaxis.set_ticks_position('bottom') self.plot.yaxis.set_ticks_position('left') # When using subplot the set message needs to be changed to include its # coordinates in the message if self.subplot_pos: canvas.toolbar.set_message = lambda x: None canvas.mpl_connect('motion_notify_event', self.setToolbarMessage) self.title = title self.x_label = x_label self.y_label = y_label self.fontsize = fontsize # self.original_xvals may have missing (None) data in it. These are # removed before plotting. self.original_xvals = xvals # self.original_xvals may have missing (None) data in it. These are # removed before plotting. self.original_yvals = yvals self.x_range = x_range self.y_range = y_range self.cvals = cvals self.original_cvals = cvals self.color = color self.symbol = symbol self.marker_size = marker_size self.slider_thickness = slider_thickness self.hslider_color = hslider_color self.vslider_color = vslider_color self.shade_color = shade_color self.slider_pick_tolerance = slider_pick_tolerance self.slider_moved_callback = slider_moved_callback self.chart_type = chart_type self.canvas = canvas self.series = None self.hsliders = [] self.vsliders = [] self.replot()
[docs] def replot(self): """ Replot the chart with the current settings """ if self.series is not None: self.series.remove() self.series = None # Add labels if self.title: self.plot.set_title(self.title, size=self.fontsize) if self.x_label: self.plot.set_xlabel(self.x_label, size=self.fontsize) if self.y_label: self.plot.set_ylabel(self.y_label, size=self.fontsize) # Remove any points with data = None self.xvals, self.yvals = self.removeMissingPoints() # Axes ranges do_not_plot = False if not self.xvals: self.xvals = [0, 1] do_not_plot = True if not self.yvals: self.yvals = [0, 1] do_not_plot = True # Find the min/max. removeMissingPoints won't have removed None values # if we don't have both X & Y set yet, so we have to make sure we remove # None first. xset = set(self.xvals) xset.discard(None) xmin = min(xset) xmax = max(xset) yset = set(self.yvals) yset.discard(None) ymin = min(yset) ymax = max(yset) padding = 0.03 if self.x_range is None: delta = padding * (xmax - xmin) if delta: self.x_range = (xmin - delta, xmax + delta) else: # All points have the same value self.x_range = (xmin - 1, xmin + 1) if self.y_range is None: delta = padding * (ymax - ymin) if delta: self.y_range = (ymin - delta, ymax + delta) else: # All points have the same value self.y_range = (ymin - 1, ymin + 1) self.plot.set_xlim(self.x_range) self.plot.set_ylim(self.y_range) # Plot each series if not do_not_plot: if self.chart_type == 'scatter': if self.cvals: color = self.cvals else: color = self.color self.series = self.plot.scatter(self.xvals, self.yvals, c=color, edgecolors='none', marker=self.symbol, s=self.marker_size) else: self.series = self.plot.plot(self.xvals, self.yvals, c=self.color)[0] # Plot the slider lines if self.use_hsliders: # Try to set the slider between the edge of the plot and the data delta = (ymax - ymin) * padding / 2 s_low = max([self.y_range[0], ymin - delta]) s_high = min([self.y_range[1], ymax + delta]) if self.hsliders: self.hsliders[0].setPosition(s_low) self.hsliders[1].setPosition(s_high) else: for sval, border in zip([s_low, s_high], ['low', 'high']): hline = self.plot.axhline(y=sval, dashes=[5, 5], linewidth=self.slider_thickness, color=self.hslider_color, zorder=-9000) self.hsliders.append( SlidableHLine(self, self.plot, hline, border=border, shade_color=self.shade_color, tolerance=self.slider_pick_tolerance, callback=self.slider_moved_callback)) delta = (xmax - xmin) * padding / 2 s_low = max([self.x_range[0], xmin - delta]) s_high = min([self.x_range[1], xmax + delta]) if self.vsliders: self.vsliders[0].setPosition(s_low) self.vsliders[1].setPosition(s_high) else: for sval, border in zip([s_low, s_high], ['low', 'high']): vline = self.plot.axvline(x=sval, dashes=[5, 5], linewidth=self.slider_thickness, color=self.vslider_color, zorder=-9000) self.vsliders.append( SlidableVLine(self, self.plot, vline, border=border, shade_color=self.shade_color, tolerance=self.slider_pick_tolerance, callback=self.slider_moved_callback)) # Make sure the tick labels don't overlap prevent_overlapping_x_labels(self.canvas) # Prevent ylabel overlapping in case of multiple plots if self.subplot_pos: self.figure.tight_layout()
[docs] def removeColorsForMissingPoints(self): """ Remove any colors for points that are missing X or Y data (X or Y = None) from the list of original values. Does nothing if the point colors are not a list or tuple. """ if not isinstance(self.original_cvals, (list, tuple)): self.cvals = self.original_cvals return try: maxlen = max([len(self.original_xvals), len(self.original_yvals)]) except TypeError: # x- or y-value list is actually None instead of a list self.cvals = self.original_cvals return cvals = [] for index in range(maxlen): try: if self.original_xvals[index] is not None and \ self.original_yvals[index] is not None: cvals.append(self.original_cvals[index]) except IndexError: pass self.cvals = cvals
[docs] def setCVals(self, cvals): """ Set the color values for scatterplot points and replot :type cvals: list, tuple or str :param cvals: Either a list or tuple of color values for every point, or a string to set a single color for all points. Do not use an RGB list or tuple to set a single color for all points, as that will be interpreted as intended to set individual colors for 3 (or 4 for RGBA) points. """ self.original_cvals = cvals self.removeColorsForMissingPoints() if self.series: self.series.set_facecolor(self.cvals) self.canvas.draw()
[docs] def removeMissingPoints(self): """ Remove any points that are missing X or Y data (X or Y = None) from the list of original values :rtype: tuple :return: tuple of (x-values, y-values) where each item is a list of values. Any point for which x-value or y-value is None has been removed. """ try: maxlen = max([len(self.original_xvals), len(self.original_yvals)]) except TypeError: # x- or y-value list is actually None instead of a list return self.original_xvals, self.original_yvals xvals = [] yvals = [] for index in range(maxlen): try: if self.original_xvals[index] is not None and \ self.original_yvals[index] is not None: xvals.append(self.original_xvals[index]) yvals.append(self.original_yvals[index]) except IndexError: pass self.removeColorsForMissingPoints() return xvals, yvals
[docs] def setXY(self, xvals, yvals, x_range=None, y_range=None, replot=True): """ Change the X and Y values of the plot :type xvals: list :param xvals: the x values to plot :type yvals: list :param yvals: y series to plot, should be the same length as xvals :type x_range: tuple :param x_range: (min, max) values for the X-axis, default is to show all values :type y_range: tuple :param y_range: (min, max) values for the Y-axis, default is to show all values :type replot: bool :param replot: True of plot should be redrawn (default), False if not. False can be used if a subsequent setY is required. """ self.original_xvals = xvals self.original_yvals = yvals self.x_range = x_range self.y_range = y_range if replot: self.replot() else: self.xvals, self.yvals = self.removeMissingPoints()
[docs] def setX(self, xvals, x_range=None, replot=True, reset_yrange=False): """ Change the X values of the plot :type xvals: list :param xvals: the x values to plot :type x_range: tuple :param x_range: (min, max) values for the X-axis, default is to show all values :type replot: bool :param replot: True of plot should be redrawn (default), False if not. False can be used if a subsequent setY is required. :type reset_yrange: bool :param reset_yrange: True if the y_range should be reset, False (default) if not. It is useful to reset this if the number of datapoints is changing. """ if reset_yrange: y_range = None else: y_range = self.y_range self.setXY(xvals, self.original_yvals, x_range=x_range, y_range=y_range, replot=replot)
[docs] def setY(self, yvals, y_range=None, replot=True, reset_xrange=False): """ Change the Y values of the plot :type yvals: list :param yvals: the y values to plot :type y_range: tuple :param y_range: (min, max) values for the Y-axis, default is to show all values :type replot: bool :param replot: True of plot should be redrawn (default), False if not. False can be used if a subsequent setY is required. :type reset_xrange: bool :param reset_xrange: True if the y_range should be reset, False (default) if not. It is useful to reset this if the number of datapoints is changing. """ if reset_xrange: x_range = None else: x_range = self.x_range self.setXY(self.original_xvals, yvals, x_range=x_range, y_range=y_range, replot=replot)
[docs] def getHSliderMin(self): """ Get the current value of the minimum horizontal slider line in plot data units :rtype: float or -INF :return: The current value of the minimum horizontal slider, if in use """ try: return self.hsliders[0].getPosition() except IndexError: return -numpy.inf
[docs] def getHSliderMax(self): """ Get the current value of the maximum horizontal slider line in plot data units :rtype: float or INF :return: The current value of the maximum horizontal slider, if in use """ try: return self.hsliders[1].getPosition() except IndexError: return numpy.inf
[docs] def getVSliderMin(self): """ Get the current value of the minimum vertical slider line in plot data units :rtype: float :return: The current value of the minimum vertical slider """ return self.vsliders[0].getPosition()
[docs] def getVSliderMax(self): """ Get the current value of the maximum vertical slider line in plot data units :rtype: float :return: The current value of the maximum vertical slider """ return self.vsliders[1].getPosition()
[docs] def getSelectedIndexes(self): """ Get the index in the x-value list of points that are contained within the slider lines. :rtype: list :return: index in the x-value list of points that are within the box bounded by the slider lines. """ xmin = self.getVSliderMin() xmax = self.getVSliderMax() ymin = self.getHSliderMin() ymax = self.getHSliderMax() selected = [] try: list(zip(self.original_xvals, self.original_yvals)) except TypeError: # One of the value lists is not set return selected for index, vals in enumerate( zip(self.original_xvals, self.original_yvals)): xval, yval = vals if xval is not None and yval is not None: if xmin <= xval <= xmax and ymin <= yval <= ymax: selected.append(index) return selected
[docs] def getSelectedXY(self): """ Get the set of (x, y) points that are contained within the slider lines :rtype: list :return: (x, y) data points that are within the box bounded by the slider lines """ xmin = self.getVSliderMin() xmax = self.getVSliderMax() ymin = self.getHSliderMin() ymax = self.getHSliderMax() selected = [] for xval, yval in zip(self.xvals, self.yvals): if xmin <= xval <= xmax and ymin <= yval <= ymax: selected.append((xval, yval)) return selected
[docs] def getSelectedX(self): """ Get the set of x values for all points that are contained within the slider lines. :rtype: list :return: x values of data points that are within the box bounded by the slider lines """ return [x for x, y in self.getSelectedXY()]
[docs] def setToolbarMessage(self, event): """ Set toolbar message to also work with subplots and show coordinates of subplots in the message :param `matplotlib.backend_bases.MouseEvent` event: the mouse event prompting this method to be called. Contains information about the location of the cursor and above which `matplotlib.axes.Axes` object the cursor was at the time of the event, if any. """ toolbar = self.canvas.toolbar # Add mode message if toolbar.mode: message = toolbar.mode else: message = '' # Add coordinate to message xval, yval = (event.xdata, event.ydata) # xval and yval will be None if outside the plot no_coords_check = xval is None or yval is None if no_coords_check: # Empty message when mouse is not inside a plot message += '' else: # Create axis message with coordinates concatenator = ', ' if toolbar.mode else '' oldwidth = toolbar.geometry().width() message += f'{concatenator}x={xval}\ty={yval}' # Call the message signal emitter toolbar.message.emit(message) # Set message to the toolbar toolbar.locLabel.setText(message) # Split mode message and coordinates in different lines if size is too # small if not no_coords_check: newwidth = toolbar.sizeHint().width() if newwidth > oldwidth: toolbar.locLabel.setText(message.replace(', ', '\n'))
[docs] def reset(self): """ Reset the plot """ self.setXY([], [])
[docs]class SlidableLine(object): """ A line on a matplotlib plot that can be grabbed and moved by the user """
[docs] def __init__(self, parent, axes, line, tolerance=7, border='high', shade_color='0.70', callback=None): """ Create a SlidableLine object :type parent: SliderPlot object :param parent: The matplotlib canvas these lines are plotted on :type axes: matplotlib Axes object :param axes: The matplotlib axes these lines are plotted on :type line: matplotlib Line2D object :param line: the actual line that will be moved :type tolerance: int :param tolerance: The amount that the user can "miss" the line and still grab it :type border: str :param border: either "high" or "low", the role this line plays in bounding the box - high lines are those that bound the upper value of X or Y :type shade_color: str :param shade_color: A matplotlib-recognized color string that the unselected areas will be shaded :type callback: callable :param callback: Called when the slider line has been moved. The callback will receive the SlidableLine object that was moved. """ self.parent = parent self.axes = axes self.figure = axes.figure self.canvas = self.figure.canvas self.line = line self.tolerance = tolerance self.border = border self.shade_color = shade_color self.canvas.mpl_connect('motion_notify_event', self.onMotion) self.canvas.mpl_connect('button_press_event', self.onPress) self.canvas.mpl_connect('button_release_event', self.onRelease) self.picked = False self.mouse_x = None self.mouse_y = None self.callback = callback
[docs] def onRelease(self, event): """ Stop checking for movement when the mouse button is released """ if self.picked and self.callback: self.callback(self) self.picked = False
[docs] def remove(self): """ Remove this from the plot """ self.line.remove() self.span.remove()
[docs]class SlidableHLine(SlidableLine): """ A horizontal line on a matplotlib plot that can be grabbed and moved by the user """
[docs] def __init__(self, *args, **kwargs): """ Create a SlidableHLine object See parent class for argument documentation """ SlidableLine.__init__(self, *args, **kwargs) self.orientation = 'horizontal' extreme = self.getAxisExtreme() if self.border == 'high': self.span = self.axes.axhspan(self.getPosition(), extreme, color=self.shade_color, zorder=-10000) else: self.span = self.axes.axhspan(extreme, self.getPosition(), color=self.shade_color, zorder=-10000)
[docs] def getPosition(self): """ Return the position of the line in data coordinates :rtype: float :return: The current position of the line in data coordinates """ return self.line.get_ydata()[0]
[docs] def getAxisExtreme(self): """ Return the most extreme (high or low) value this line should take :rtype: float :return: The most extreme value this line can take in data coordinates """ if self.border == 'high': return self.axes.get_ylim()[1] else: return self.axes.get_ylim()[0]
[docs] def setPosition(self, value): """ Change the position of the line to value. However, we don't allow the high/low boundary lines to cross, or to go off the plot. :type value: float :param value: The new y value to attempt to place the line at """ # Keep track of the most extreme value this line should take ext = self.getAxisExtreme() # Make sure we don't cross the streams (allow the min & max sliders to # occupy the same value - or flip). We allow a padding between the # lines equal to the tolerance set to determine if the line was picked. # Tolerance is in screen pixels but the line position is in data units, # so we do some fancy conversion footwork. screen_pos = self.axes.transData.transform((0, value)) neutral_zone = self.axes.transData.inverted().transform( (screen_pos[0], screen_pos[1] + self.tolerance))[1] pad = neutral_zone - value if self.border == 'high': value = min(max(self.parent.getHSliderMin() + pad, value), ext) else: value = max(min(self.parent.getHSliderMax() - pad, value), ext) self.line.set_ydata([value, value]) self.span.set_xy([[0.0, ext], [0.0, value], [1.0, value], [1.0, ext], [0.0, ext]])
[docs] def onMotion(self, event): """ Move the line if it is currently moving and the mouse moved :type event: matplotlib mouse event :param event: the event object generated by mouse movement """ if self.picked and event.ydata is not None: self.setPosition(event.ydata) self.canvas.draw()
[docs] def onPress(self, event): """ Check to see if the user clicked this line :type event: matplotlib mouse event :param event: the event object generated by mouse click """ # Ignore clicks in certain regions outside the plot - like the title if event.xdata is None or event.ydata is None: self.picked = False return # Have to put the line position and screen click into the same units as # the tolerance value (screen pixels) screen_click = self.axes.transData.transform((event.xdata, event.ydata)) screen_line = self.axes.transData.transform((0.0, self.getPosition())) try: if abs(screen_click[1] - screen_line[1]) < self.tolerance: self.picked = True else: self.picked = False except TypeError: # Happens most often if user click is not within the axes self.picked = False
[docs]class SlidableVLine(SlidableLine): """ A vertical line on a matplotlib plot that can be grabbed and moved by the user """
[docs] def __init__(self, *args, **kwargs): """ Create a SlidableVLine object See parent class for argument documentation """ SlidableLine.__init__(self, *args, **kwargs) extreme = self.getAxisExtreme() self.orientation = 'vertical' if self.border == 'high': self.span = self.axes.axvspan(self.getPosition(), extreme, color=self.shade_color, zorder=-10000) else: self.span = self.axes.axvspan(extreme, self.getPosition(), color=self.shade_color, zorder=-10000)
[docs] def getPosition(self): """ Return the position of the line in data coordinates :rtype: float :return: The current position of the line in data coordinates """ return self.line.get_xdata()[0]
[docs] def setPosition(self, value): """ Change the position of the line to value. However, we don't allow the high/low boundary lines to cross, or to go off the plot. :type value: float :param value: The new x value to attempt to place the line at """ # Keep track of the most extreme value this line should take ext = self.getAxisExtreme() # Make sure we don't cross the streams (allow the min & max sliders to # occupy the same value - or flip). We allow a padding between the # lines equal to the tolerance set to determine if the line was picked. # Tolerance is in screen pixels but the line position is in data units, # so we do some fancy conversion footwork. screen_pos = self.axes.transData.transform((value, 0)) neutral_zone = self.axes.transData.inverted().transform( (screen_pos[0] + self.tolerance, screen_pos[1]))[0] pad = neutral_zone - value if self.border == 'high': value = min(max(self.parent.getVSliderMin() + pad, value), ext) else: value = max(min(self.parent.getVSliderMax() - pad, value), ext) self.line.set_xdata([value, value]) self.span.set_xy([[ext, 0.0], [value, 0.0], [value, 1.0], [ext, 1.0], [ext, 0.0]])
[docs] def getAxisExtreme(self): """ Return the most extreme (high or low) value this line should take :rtype: float :return: The most extreme value this line can take in data coordinates """ if self.border == 'high': return self.axes.get_xlim()[1] else: return self.axes.get_xlim()[0]
[docs] def onMotion(self, event): """ Move the line if it is currently moving and the mouse moved :type event: matplotlib mouse event :param event: the event object generated by mouse movement """ if self.picked and event.xdata is not None: self.setPosition(event.xdata) self.canvas.draw()
[docs] def onPress(self, event): """ Check to see if the user clicked this line :type event: matplotlib mouse event :param event: the event object generated by mouse click """ # Ignore clicks in certain regions outside the plot - like the title if event.xdata is None or event.ydata is None: self.picked = False return # Have to put the line position and screen click into the same units as # the tolerance value (screen pixels) screen_click = self.axes.transData.transform((event.xdata, event.ydata)) screen_line = self.axes.transData.transform((self.getPosition(), 0.0)) if abs(screen_click[0] - screen_line[0]) < self.tolerance: self.picked = True else: self.picked = False
if ("__main__" == __name__): # Check for a PyQt application instance and create one if needed: app = QtWidgets.QApplication([]) if len(sys.argv) != 2: print("Usage: $SCHRODINGER/run %s <data-file>" % sys.argv[0]) sys.exit(0) if (not os.path.isfile(sys.argv[1])): print("Data file not found: %s" % sys.argv[1]) sys.exit(0) lines = open(sys.argv[1], "r").read().split("\n") x = [] y = [] y_err = [] for line in lines: line = line.strip() if (line != "" and line[0] != "#"): token = line.split() x.append(float(token[0])) y.append(float(token[1])) frame = SliderPlot(x, y, x_label="time (ps)", y_label='Y values', shade_color='pink', hslider_color='y', vslider_color='green', slider_thickness=10, slider_pick_tolerance=20) frame.show() app.exec()