"""
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 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 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_()