Source code for sas.qtgui.Plotting.QRangeSlider

"""Double slider interactor for setting the Q range for a fit or function"""
import numpy as np
from typing import Union
from PyQt5.QtCore import QEvent
from PyQt5.QtWidgets import QLineEdit, QTextEdit

from sas.qtgui.Plotting.PlotterData import Data1D
from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
import sas.qtgui.Utilities.ObjectLibrary as ol


[docs]class QRangeSlider(BaseInteractor): """ Draw a pair of draggable vertical lines. Each line can be linked to a GUI input. The GUI input should update the lines and vice-versa. """
[docs] def __init__(self, base, axes, color='black', zorder=5, data=None): # type: (Plotter, Plotter.ax, str, int, Data1D) -> None """ Initialize the slideable lines and associated markers """ # Assert the data object is a plottable assert isinstance(data, Data1D) # Plotter object BaseInteractor.__init__(self, base, axes, color=color) self.base = base self.markers = [] self.axes = axes self.data = data # Track the visibility for toggling the slider on/off self.is_visible = False self.connect = self.base.connect # Min and max x values self.x_min = np.fabs(min(self.data.x)) self.y_marker_min = self.data.y[np.where(self.data.x == self.x_min)[0][0]] self.x_max = np.fabs(max(self.data.x)) self.y_marker_max = self.data.y[np.where(self.data.x == self.x_max)[0][-1]] # Should the inputs update while the bar is actively being dragged? self.updateOnMove = data.slider_update_on_move # Draw the lines self.line_min = LineInteractor(self, axes, zorder=zorder, x=self.x_min, y=self.y_marker_min, input=data.slider_low_q_input, setter=data.slider_low_q_setter, getter=data.slider_low_q_getter, perspective=data.slider_perspective_name, tab=data.slider_tab_name) self.line_max = LineInteractor(self, axes, zorder=zorder, x=self.x_max, y=self.y_marker_max, input=data.slider_high_q_input, setter=data.slider_high_q_setter, getter=data.slider_high_q_getter, perspective=data.slider_perspective_name, tab=data.slider_tab_name) self.has_move = True self.update()
[docs] def clear(self): # type: () -> None """ Clear this slicer and its markers """ self.clear_markers()
[docs] def show(self): # type: () -> None """ Show this slicer and its markers """ self.line_max.draw() self.line_min.draw() self.update()
[docs] def remove(self): # type: () -> None """ Remove this slicer and its markers """ self.line_max.remove() self.line_min.remove() self.draw() self.is_visible = False
[docs] def update(self, x=None, y=None): # type: (float, float) -> None """Draw the new lines on the graph.""" self.line_min.update(x, y, draw=self.updateOnMove) self.line_max.update(x, y, draw=self.updateOnMove) self.base.update() self.is_visible = True
[docs] def save(self, ev): # type: (QEvent) -> None """ Remember the position of the lines so that we can restore on Esc. """ self.line_min.save(ev) self.line_max.save(ev)
[docs] def restore(self, ev): # type: (QEvent) -> None """ Restore the lines. """ self.line_max.restore(ev) self.line_min.restore(ev)
[docs] def toggle(self): # type: () -> None """ Toggle the slider visibility. """ if self.is_visible: self.remove() else: self.show()
[docs] def move(self, x, y, ev): # type: (float, float, QEvent) -> None """ Process move to a new position, making sure that the move is allowed. """ pass
[docs] def clear_markers(self): # type: () -> None """ Clear each of the lines individually """ self.line_min.clear() self.line_max.clear()
[docs] def draw(self): # type: () -> None """ Update the plot """ self.base.draw()
[docs]class LineInteractor(BaseInteractor): """ Draw a single vertical line that can be dragged on a plot """
[docs] def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5, input=None, setter=None, getter=None, perspective=None, tab=None): # type: (Plotter, Plotter.ax, str, int, float, float, [str], [str], [str], str, str) -> None """ Initialize the line interactor object""" BaseInteractor.__init__(self, base, axes, color=color) # Plotter object self.base = base # Inputs and methods linking this slider to a GUI element so, as one changes, the other also updates self._input = None self._setter = None self._getter = None # The marker(s) for this line - typically only one self.markers = [] # The Plotter.ax object self.axes = axes # X and Y values used for the line and markers self.x = x self.save_x = self.x self.y_marker = y self.save_y = self.y_marker self.draw(zorder) # Is the slider able to move self.has_move = True try: data_explorer = ol.getObject('DataExplorer') self.perspective = data_explorer.parent.loadedPerspectives.get(perspective, None) except AttributeError: # QRangeSlider is disconnected from GuiManager for testing self.perspective = None if self.perspective is None: return if tab and hasattr(self.perspective, 'getTabByName'): # If the perspective is tabbed, set the perspective to the tab this slider in associated with self.perspective = self.perspective.getTabByName(tab) if self.perspective: # Connect the inputs and methods self.input = self._get_input_or_callback(input) self.setter = self._get_input_or_callback(setter) self.getter = self._get_input_or_callback(getter) self.connect_markers([self.line, self.inner_marker]) self.update(draw=True)
@property def input(self): # type: () -> Union[QLineEdit, QTextEdit, None] """ Get the text input that should be linked to the position of this slider """ return self._input @input.setter def input(self, input): # type: (Union[QLineEdit, QTextEdit, None]) -> None """ Set the text input that should be linked to the position of this slider """ self._input = input if self._input: self._input.editingFinished.connect(self.inputChanged) @property def setter(self): # type: () -> Union[callable, None] """ Get the x-value setter method associated with this slider """ return self._setter @setter.setter def setter(self, setter): # type: (Union[callable, None]) -> None """ Set the x-value setter method associated with this slider """ self._setter = setter if callable(setter) else None @property def getter(self): # type: () -> Union[callable, None] """ Get the x-value getter method associated with this slider """ return self._getter @getter.setter def getter(self, getter): # type: (Union[callable, None]) -> None """ Set the x-value getter associated with this slider """ self._getter = getter if callable(getter) else None
[docs] def clear(self): # type: () -> None """ Disconnect any inputs and callbacks and the clear the line and marker """ self.clear_markers() self.remove()
[docs] def remove(self): # type: () -> None """ Clear this slicer and its markers """ if self.inner_marker: self.inner_marker.remove() if self.line: self.line.remove()
[docs] def draw(self, zorder=5): # type: (int) -> None """ Draw the line and marker on the linked plot using the latest x and y values """ # Inner circle marker self.inner_marker = self.axes.plot([self.x], [self.y_marker], linestyle='', marker='o', markersize=4, color=self.color, alpha=0.6, pickradius=5, label=None, zorder=zorder, visible=True)[0] self.line = self.axes.axvline(self.x, linestyle='-', color=self.color, marker='', pickradius=5, label=None, zorder=zorder, visible=True)
[docs] def _get_input_or_callback(self, connection_list=None): # type: ([str]) -> Union[QLineEdit, QTextEdit, None] """ Returns an input or callback method based on a list of inputs/commands """ connection = None if isinstance(connection_list, list): connection = self.perspective for item in connection_list: try: connection = getattr(connection, item) except Exception: return None return connection
[docs] def _set_q(self, value): # type: (float) -> None """ Call the q setter callback method if it exists """ self.x = value if self.setter and callable(self.setter): self.setter(value) elif hasattr(self.input, 'setText'): self.input.setText(f"{value:.3}")
[docs] def _get_q(self): # type: () -> None """ Get the q value, inferring the method to get the value """ if self.getter: # Separate callback method to get Q value self.x = float(self.getter()) elif hasattr(self.input, 'text'): # Line edit box self.x = float(self.input.text()) elif hasattr(self.input, 'getText'): # Text box self.x = float(self.input.getText())
[docs] def inputChanged(self): # type: () -> None """ Track the input linked to the x value for this slider and update as needed """ self._get_q() self.y_marker = self.base.data.y[(np.abs(self.base.data.x - self.x)).argmin()] self.update(draw=True)
[docs] def update(self, x=None, y=None, draw=False): # type: (float, float, bool) -> None """ Update the line position on the graph. """ # Reset x, y -coordinates if given as parameters if x is not None: self.x = np.sign(self.x) * np.fabs(x) if y is not None: self.y_marker = y # Draw lines and markers self.inner_marker.set_xdata([self.x]) self.inner_marker.set_ydata([self.y_marker]) self.line.set_xdata([self.x]) if draw: self.base.draw()
[docs] def save(self, ev): # type: (QEvent) -> None """ Remember the position for this line so that we can restore on Esc. """ self.save_x = self.x self.save_y = self.y_marker
[docs] def restore(self, ev): # type: (QEvent) -> None """ Restore the position for this line """ self.x = self.save_x self.y_marker = self.save_y
[docs] def move(self, x, y, ev): # type: (float, float, QEvent) -> None """ Process move to a new position, making sure that the move is allowed. """ self.has_move = True self.x = x if self.base.updateOnMove: self._set_q(x) self.y_marker = self.base.data.y[(np.abs(self.base.data.x - self.x)).argmin()] self.update(draw=self.base.updateOnMove)
[docs] def onRelease(self, ev): # type: (QEvent) -> bool """ Update the line position when the mouse button is released """ # Set the Q value if a callable setter exists otherwise update the attached input self._set_q(self.x) self.update(draw=True) self.moveend(ev) return True
[docs] def clear_markers(self): # type: () -> None """ Disconnect the input and clear the callbacks """ if self.input: self.input.editingFinished.disconnect(self.inputChanged) self.setter = None self.getter = None self.input = None