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