class QSignalSpy(QObject): """ QSignalSpy(boundsignal) """ def __init__(self, boundsig, **kwargs): super(QSignalSpy, self).__init__(**kwargs) from AnyQt.QtCore import QEventLoop, QTimer self.__boundsig = boundsig self.__recorded = recorded = [] # type: List[List[Any]] self.__loop = loop = QEventLoop() self.__timer = QTimer(self, singleShot=True) self.__timer.timeout.connect(self.__loop.quit) def record(*args): # Record the emitted arguments and quit the loop if running. # NOTE: not capturing self from parent scope recorded.append(list(args)) if loop.isRunning(): loop.quit() # Need to keep reference at least for PyQt4 4.11.4, sip 4.16.9 on # python 3.4 (if the signal is emitted during gc collection, and # the boundsignal is a QObject.destroyed signal). self.__record = record boundsig.connect(record) def signal(self): return _QByteArray(self.__boundsig.signal[1:].encode("latin-1")) def isValid(self): return True def wait(self, timeout=5000): count = len(self) self.__timer.stop() self.__timer.setInterval(timeout) self.__timer.start() self.__loop.exec_() self.__timer.stop() return len(self) != count def __getitem__(self, index): return self.__recorded[index] def __setitem__(self, index, value): self.__recorded.__setitem__(index, value) def __delitem__(self, index): self.__recorded.__delitem__(index) def __len__(self): return len(self.__recorded)
class QSignalSpy(QObject): """ QSignalSpy(boundsignal) """ def __init__(self, boundsig, **kwargs): super(QSignalSpy, self).__init__(**kwargs) from AnyQt.QtCore import QEventLoop, QTimer self.__boundsig = boundsig self.__boundsig.connect(lambda *args: self.__record(*args)) self.__recorded = [] # type: List[List[Any]] self.__loop = QEventLoop() self.__timer = QTimer(self, singleShot=True) self.__timer.timeout.connect(self.__loop.quit) def __record(self, *args): self.__recorded.append(list(args)) if self.__loop.isRunning(): self.__loop.quit() def signal(self): return _QByteArray(self.__boundsig.signal[1:].encode("latin-1")) def isValid(self): return True def wait(self, timeout=5000): count = len(self) self.__timer.stop() self.__timer.setInterval(timeout) self.__timer.start() self.__loop.exec_() self.__timer.stop() return len(self) != count def __getitem__(self, index): return self.__recorded[index] def __setitem__(self, index, value): self.__recorded.__setitem__(index, value) def __delitem__(self, index): self.__recorded.__delitem__(index) def __len__(self): return len(self.__recorded)
class EventSpy(QObject): """ A testing utility class (similar to QSignalSpy) to record events delivered to a QObject instance. Note ---- Only event types can be recorded (as QEvent instances are deleted on delivery). Note ---- Can only be used with a QCoreApplication running. Parameters ---------- object : QObject An object whose events need to be recorded. etype : Union[QEvent.Type, Sequence[QEvent.Type] A event type (or types) that should be recorded """ def __init__(self, object, etype, **kwargs): super().__init__(**kwargs) if not isinstance(object, QObject): raise TypeError self.__object = object try: len(etype) except TypeError: etypes = {etype} else: etypes = set(etype) self.__etypes = etypes self.__record = [] self.__loop = QEventLoop() self.__timer = QTimer(self, singleShot=True) self.__timer.timeout.connect(self.__loop.quit) self.__object.installEventFilter(self) def wait(self, timeout=5000): """ Start an event loop that runs until a spied event or a timeout occurred. Parameters ---------- timeout : int Timeout in milliseconds. Returns ------- res : bool True if the event occurred and False otherwise. Example ------- >>> app = QCoreApplication.instance() or QCoreApplication([]) >>> obj = QObject() >>> spy = EventSpy(obj, QEvent.User) >>> app.postEvent(obj, QEvent(QEvent.User)) >>> spy.wait() True >>> print(spy.events()) [1000] """ count = len(self.__record) self.__timer.stop() self.__timer.setInterval(timeout) self.__timer.start() self.__loop.exec_() self.__timer.stop() return len(self.__record) != count def eventFilter(self, reciever, event): if reciever is self.__object and event.type() in self.__etypes: self.__record.append(event.type()) if self.__loop.isRunning(): self.__loop.quit() return super().eventFilter(reciever, event) def events(self): """ Return a list of all (listened to) event types that occurred. Returns ------- events : List[QEvent.Type] """ return list(self.__record)
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin): """ Base class for VizRank dialogs, providing a GUI with a table and a button, and the skeleton for managing the evaluation of visualizations. Derived classes must provide methods - `iterate_states` for generating combinations (e.g. pairs of attritutes), - `compute_score(state)` for computing the score of a combination, - `row_for_state(state)` that returns a list of items inserted into the table for the given state. and, optionally, - `state_count` that returns the number of combinations (used for progress bar) - `on_selection_changed` that handles event triggered when the user selects a table row. The method should emit signal `VizRankDialog.selectionChanged(object)`. - `bar_length` returns the length of the bar corresponding to the score. The class provides a table and a button. A widget constructs a single instance of this dialog in its `__init__`, like (in Sieve) by using a convenience method :obj:`add_vizrank`:: self.vizrank, self.vizrank_button = SieveRank.add_vizrank( box, self, "Score Combinations", self.set_attr) When the widget receives new data, it must call the VizRankDialog's method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the state. Clicking the Start button calls method `run` (and renames the button to Pause). Run sets up a progress bar by getting the number of combinations from :obj:`VizRankDialog.state_count()`. It restores the paused state (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For each generated state, it calls :obj:`VizRankDialog.score(state)`, which must return the score (lower is better) for this state. If the returned state is not `None`, the data returned by `row_for_state` is inserted at the appropriate place in the table. Args: master (Orange.widget.OWWidget): widget to which the dialog belongs Attributes: master (Orange.widget.OWWidget): widget to which the dialog belongs captionTitle (str): the caption for the dialog. This can be a class attribute. `captionTitle` is used by the `ProgressBarMixin`. """ captionTitle = "" NEGATIVE_COLOR = QColor(70, 190, 250) POSITIVE_COLOR = QColor(170, 242, 43) processingStateChanged = Signal(int) progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) selectionChanged = Signal(object) class Information(WidgetMessagesMixin.Information): nothing_to_rank = Msg("There is nothing to rank.") def __init__(self, master): """Initialize the attributes and set up the interface""" QDialog.__init__(self, master, windowTitle=self.captionTitle) WidgetMessagesMixin.__init__(self) self.setLayout(QVBoxLayout()) self.insert_message_bar() self.layout().insertWidget(0, self.message_bar) self.master = master self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.scores = [] self.add_to_model = queue.Queue() self.update_timer = QTimer(self) self.update_timer.timeout.connect(self._update) self.update_timer.setInterval(200) self._thread = None self._worker = None self.filter = QLineEdit() self.filter.setPlaceholderText("Filter ...") self.filter.textChanged.connect(self.filter_changed) self.layout().addWidget(self.filter) # Remove focus from line edit self.setFocus(Qt.ActiveWindowFocusReason) self.rank_model = QStandardItemModel(self) self.model_proxy = QSortFilterProxyModel(self, filterCaseSensitivity=False) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False, editTriggers=gui.TableView.NoEditTriggers) if self._has_bars: view.setItemDelegate(TableBarItem()) else: view.setItemDelegate(HorizontalGridDelegate()) view.setModel(self.model_proxy) view.selectionModel().selectionChanged.connect( self.on_selection_changed) view.horizontalHeader().setStretchLastSection(True) view.horizontalHeader().hide() self.layout().addWidget(view) self.button = gui.button(self, self, "Start", callback=self.toggle, default=True) @property def _has_bars(self): return type(self).bar_length is not VizRankDialog.bar_length @classmethod def add_vizrank(cls, widget, master, button_label, set_attr_callback): """ Equip the widget with VizRank button and dialog, and monkey patch the widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too. Args: widget (QWidget): the widget into whose layout to insert the button master (Orange.widgets.widget.OWWidget): the master widget button_label: the label for the button set_attr_callback: the callback for setting the projection chosen in the vizrank Returns: tuple with Vizrank dialog instance and push button """ # Monkey patching could be avoided by mixing-in the class (not # necessarily a good idea since we can make a mess of multiple # defined/derived closeEvent and hideEvent methods). Furthermore, # per-class patching would be better than per-instance, but we don't # want to mess with meta-classes either. vizrank = cls(master) button = gui.button(widget, master, button_label, callback=vizrank.reshow, enabled=False) vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args)) master_close_event = master.closeEvent master_hide_event = master.hideEvent master_delete_event = master.onDeleteWidget def closeEvent(event): vizrank.close() master_close_event(event) def hideEvent(event): vizrank.hide() master_hide_event(event) def deleteEvent(): vizrank.keep_running = False if vizrank._thread is not None and vizrank._thread.isRunning(): vizrank._thread.quit() vizrank._thread.wait() master_delete_event() master.closeEvent = closeEvent master.hideEvent = hideEvent master.onDeleteWidget = deleteEvent return vizrank, button def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def initialize(self): """ Clear and initialize the dialog. This method must be called by the widget when the data is reset, e.g. from `set_data` handler. """ if self._thread is not None and self._thread.isRunning(): self.keep_running = False self._thread.quit() self._thread.wait() self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.update_timer.stop() self.progressBarFinished() self.scores = [] self._update_model() # empty queue self.rank_model.clear() self.button.setText("Start") self.button.setEnabled(self.check_preconditions()) self._thread = QThread(self) self._worker = Worker(self) self._worker.moveToThread(self._thread) self._worker.stopped.connect(self._thread.quit) self._worker.stopped.connect(self._select_first_if_none) self._worker.stopped.connect(self._stopped) self._worker.done.connect(self._done) self._thread.started.connect(self._worker.do_work) def filter_changed(self, text): self.model_proxy.setFilterFixedString(text) def stop_and_reset(self, reset_method=None): if self.keep_running: self.scheduled_call = reset_method or self.initialize self.keep_running = False else: self.initialize() def check_preconditions(self): """Check whether there is sufficient data for ranking.""" return True def on_selection_changed(self, selected, deselected): """ Set the new visualization in the widget when the user select a row in the table. If derived class does not reimplement this, the table gives the information but the user can't click it to select the visualization. Args: selected: the index of the selected item deselected: the index of the previously selected item """ pass def iterate_states(self, initial_state): """ Generate all possible states (e.g. attribute combinations) for the given data. The content of the generated states is specific to the visualization. This method must be defined in the derived classes. Args: initial_state: initial state; None if this is the first call """ raise NotImplementedError def state_count(self): """ Return the number of states for the progress bar. Derived classes should implement this to ensure the proper behaviour of the progress bar""" return 0 def compute_score(self, state): """ Abstract method for computing the score for the given state. Smaller scores are better. Args: state: the state, e.g. the combination of attributes as generated by :obj:`state_count`. """ raise NotImplementedError def bar_length(self, score): """Compute the bar length (between 0 and 1) corresponding to the score. Return `None` if the score cannot be normalized. """ return None def row_for_state(self, score, state): """ Abstract method that return the items that are inserted into the table. Args: score: score, computed by :obj:`compute_score` state: the state, e.g. combination of attributes """ raise NotImplementedError def _select_first_if_none(self): if not self.rank_table.selectedIndexes(): self.rank_table.selectRow(0) def _done(self): self.button.setText("Finished") self.button.setEnabled(False) self.keep_running = False self.saved_state = None def _stopped(self): self.update_timer.stop() self.progressBarFinished() self._update_model() self.stopped() if self.scheduled_call: self.scheduled_call() def _update(self): self._update_model() self._update_progress() def _update_progress(self): self.progressBarSet( int(self.saved_progress * 100 / max(1, self.state_count()))) def _update_model(self): try: while True: pos, row_items = self.add_to_model.get_nowait() self.rank_model.insertRow(pos, row_items) except queue.Empty: pass def toggle(self): """Start or pause the computation.""" self.keep_running = not self.keep_running if self.keep_running: self.button.setText("Pause") self.progressBarInit() self.update_timer.start() self.before_running() self._thread.start() else: self.button.setText("Continue") self._thread.quit() # Need to sync state (the worker must read the keep_running # state and stop) for reliable restart. self._thread.wait() def before_running(self): """Code that is run before running vizrank in its own thread""" pass def stopped(self): """Code that is run after stopping the vizrank thread""" pass
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin): """ Base class for VizRank dialogs, providing a GUI with a table and a button, and the skeleton for managing the evaluation of visualizations. Derived classes must provide methods - `iterate_states` for generating combinations (e.g. pairs of attritutes), - `compute_score(state)` for computing the score of a combination, - `row_for_state(state)` that returns a list of items inserted into the table for the given state. and, optionally, - `state_count` that returns the number of combinations (used for progress bar) - `on_selection_changed` that handles event triggered when the user selects a table row. The method should emit signal `VizRankDialog.selectionChanged(object)`. - `bar_length` returns the length of the bar corresponding to the score. The class provides a table and a button. A widget constructs a single instance of this dialog in its `__init__`, like (in Sieve) by using a convenience method :obj:`add_vizrank`:: self.vizrank, self.vizrank_button = SieveRank.add_vizrank( box, self, "Score Combinations", self.set_attr) When the widget receives new data, it must call the VizRankDialog's method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the state. Clicking the Start button calls method `run` (and renames the button to Pause). Run sets up a progress bar by getting the number of combinations from :obj:`VizRankDialog.state_count()`. It restores the paused state (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For each generated state, it calls :obj:`VizRankDialog.score(state)`, which must return the score (lower is better) for this state. If the returned state is not `None`, the data returned by `row_for_state` is inserted at the appropriate place in the table. Args: master (Orange.widget.OWWidget): widget to which the dialog belongs Attributes: master (Orange.widget.OWWidget): widget to which the dialog belongs captionTitle (str): the caption for the dialog. This can be a class attribute. `captionTitle` is used by the `ProgressBarMixin`. """ captionTitle = "" processingStateChanged = Signal(int) progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) selectionChanged = Signal(object) class Information(WidgetMessagesMixin.Information): nothing_to_rank = Msg("There is nothing to rank.") def __init__(self, master): """Initialize the attributes and set up the interface""" QDialog.__init__(self, master, windowTitle=self.captionTitle) WidgetMessagesMixin.__init__(self) self.setLayout(QVBoxLayout()) self.insert_message_bar() self.layout().insertWidget(0, self.message_bar) self.master = master self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.scores = [] self.add_to_model = queue.Queue() self.update_timer = QTimer(self) self.update_timer.timeout.connect(self._update) self.update_timer.setInterval(200) self._thread = None self._worker = None self.filter = QLineEdit() self.filter.setPlaceholderText("Filter ...") self.filter.textChanged.connect(self.filter_changed) self.layout().addWidget(self.filter) # Remove focus from line edit self.setFocus(Qt.ActiveWindowFocusReason) self.rank_model = QStandardItemModel(self) self.model_proxy = QSortFilterProxyModel( self, filterCaseSensitivity=False) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False, editTriggers=gui.TableView.NoEditTriggers) if self._has_bars: view.setItemDelegate(TableBarItem()) else: view.setItemDelegate(HorizontalGridDelegate()) view.setModel(self.model_proxy) view.selectionModel().selectionChanged.connect( self.on_selection_changed) view.horizontalHeader().setStretchLastSection(True) view.horizontalHeader().hide() self.layout().addWidget(view) self.button = gui.button( self, self, "Start", callback=self.toggle, default=True) @property def _has_bars(self): return type(self).bar_length is not VizRankDialog.bar_length @classmethod def add_vizrank(cls, widget, master, button_label, set_attr_callback): """ Equip the widget with VizRank button and dialog, and monkey patch the widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too. Args: widget (QWidget): the widget into whose layout to insert the button master (Orange.widgets.widget.OWWidget): the master widget button_label: the label for the button set_attr_callback: the callback for setting the projection chosen in the vizrank Returns: tuple with Vizrank dialog instance and push button """ # Monkey patching could be avoided by mixing-in the class (not # necessarily a good idea since we can make a mess of multiple # defined/derived closeEvent and hideEvent methods). Furthermore, # per-class patching would be better than per-instance, but we don't # want to mess with meta-classes either. vizrank = cls(master) button = gui.button( widget, master, button_label, callback=vizrank.reshow, enabled=False) vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args)) master_close_event = master.closeEvent master_hide_event = master.hideEvent master_delete_event = master.onDeleteWidget def closeEvent(event): vizrank.close() master_close_event(event) def hideEvent(event): vizrank.hide() master_hide_event(event) def deleteEvent(): vizrank.keep_running = False if vizrank._thread is not None and vizrank._thread.isRunning(): vizrank._thread.quit() vizrank._thread.wait() master_delete_event() master.closeEvent = closeEvent master.hideEvent = hideEvent master.onDeleteWidget = deleteEvent return vizrank, button def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def initialize(self): """ Clear and initialize the dialog. This method must be called by the widget when the data is reset, e.g. from `set_data` handler. """ if self._thread is not None and self._thread.isRunning(): self.keep_running = False self._thread.quit() self._thread.wait() self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.update_timer.stop() self.progressBarFinished() self.scores = [] self._update_model() # empty queue self.rank_model.clear() self.button.setText("Start") self.button.setEnabled(self.check_preconditions()) self._thread = QThread(self) self._worker = Worker(self) self._worker.moveToThread(self._thread) self._worker.stopped.connect(self._thread.quit) self._worker.stopped.connect(self._select_first_if_none) self._worker.stopped.connect(self._stopped) self._worker.done.connect(self._done) self._thread.started.connect(self._worker.do_work) def filter_changed(self, text): self.model_proxy.setFilterFixedString(text) def stop_and_reset(self, reset_method=None): if self.keep_running: self.scheduled_call = reset_method or self.initialize self.keep_running = False else: self.initialize() def check_preconditions(self): """Check whether there is sufficient data for ranking.""" return True def on_selection_changed(self, selected, deselected): """ Set the new visualization in the widget when the user select a row in the table. If derived class does not reimplement this, the table gives the information but the user can't click it to select the visualization. Args: selected: the index of the selected item deselected: the index of the previously selected item """ pass def iterate_states(self, initial_state): """ Generate all possible states (e.g. attribute combinations) for the given data. The content of the generated states is specific to the visualization. This method must be defined in the derived classes. Args: initial_state: initial state; None if this is the first call """ raise NotImplementedError def state_count(self): """ Return the number of states for the progress bar. Derived classes should implement this to ensure the proper behaviour of the progress bar""" return 0 def compute_score(self, state): """ Abstract method for computing the score for the given state. Smaller scores are better. Args: state: the state, e.g. the combination of attributes as generated by :obj:`state_count`. """ raise NotImplementedError def bar_length(self, score): """Compute the bar length (between 0 and 1) corresponding to the score. Return `None` if the score cannot be normalized. """ return None def row_for_state(self, score, state): """ Abstract method that return the items that are inserted into the table. Args: score: score, computed by :obj:`compute_score` state: the state, e.g. combination of attributes """ raise NotImplementedError def _select_first_if_none(self): if not self.rank_table.selectedIndexes(): self.rank_table.selectRow(0) def _done(self): self.button.setText("Finished") self.button.setEnabled(False) self.keep_running = False self.saved_state = None def _stopped(self): self.update_timer.stop() self.progressBarFinished() self._update_model() self.stopped() if self.scheduled_call: self.scheduled_call() def _update(self): self._update_model() self._update_progress() def _update_progress(self): self.progressBarSet(int(self.saved_progress * 100 / max(1, self.state_count()))) def _update_model(self): try: while True: pos, row_items = self.add_to_model.get_nowait() self.rank_model.insertRow(pos, row_items) except queue.Empty: pass def toggle(self): """Start or pause the computation.""" self.keep_running = not self.keep_running if self.keep_running: self.button.setText("Pause") self.progressBarInit() self.update_timer.start() self.before_running() self._thread.start() else: self.button.setText("Continue") self._thread.quit() # Need to sync state (the worker must read the keep_running # state and stop) for reliable restart. self._thread.wait() def before_running(self): """Code that is run before running vizrank in its own thread""" pass def stopped(self): """Code that is run after stopping the vizrank thread""" pass
class OWTimeSlice(widget.OWWidget): name = 'Time Slice' description = 'Select a slice of measurements on a time interval.' icon = 'icons/TimeSlice.svg' priority = 550 class Inputs: data = Input("Data", Table) class Outputs: subset = Output("Subset", Table) settings_version = 2 want_main_area = False class Error(widget.OWWidget.Error): no_time_variable = widget.Msg('Data contains no time variable') no_time_delta = widget.Msg('Data contains only one time point') MAX_SLIDER_VALUE = 500 DATE_FORMATS = ('yyyy', '-MM', '-dd', ' HH:mm:ss.zzz') # only appropriate overlap amounts are shown, but these are all the options DELAY_VALUES = (0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30) STEP_SIZES = OrderedDict( (('1 second', 1), ('5 seconds', 5), ('10 seconds', 10), ('15 seconds', 15), ('30 seconds', 30), ('1 minute', 60), ('5 minutes', 300), ('10 minutes', 600), ('15 minutes', 900), ('30 minutes', 1800), ('1 hour', 3600), ('2 hours', 7200), ('3 hours', 10800), ('6 hours', 21600), ('12 hours', 43200), ('1 day', 86400), ('1 week', 604800), ('2 weeks', 1209600), ('1 month', (1, 'month')), ('2 months', (2, 'month')), ('3 months', (3, 'month')), ('6 months', (6, 'month')), ('1 year', (1, 'year')), ('2 years', (2, 'year')), ('5 years', (5, 'year')), ('10 years', (10, 'year')), ('25 years', (25, 'year')), ('50 years', (50, 'year')), ('100 years', (100, 'year')))) loop_playback = settings.Setting(True) custom_step_size = settings.Setting(False) step_size = settings.Setting(next(iter(STEP_SIZES))) playback_interval = settings.Setting(1) slider_values = settings.Setting((0, .2 * MAX_SLIDER_VALUE)) icons_font = None def __init__(self): super().__init__() self._delta = 0 self.play_timer = QTimer(self, interval=1000 * self.playback_interval, timeout=self.play_single_step) slider = self.slider = Slider(Qt.Horizontal, self, minimum=0, maximum=self.MAX_SLIDER_VALUE, tracking=True, playbackInterval=1000 * self.playback_interval, valuesChanged=self.sliderValuesChanged, minimumValue=self.slider_values[0], maximumValue=self.slider_values[1]) slider.setShowText(False) selectBox = gui.vBox(self.controlArea, 'Select a Time Range') selectBox.layout().addWidget(slider) dtBox = gui.hBox(selectBox) kwargs = dict(calendarPopup=True, displayFormat=' '.join(self.DATE_FORMATS), timeSpec=Qt.UTC) date_from = self.date_from = QDateTimeEdit(self, **kwargs) date_to = self.date_to = QDateTimeEdit(self, **kwargs) def datetime_edited(dt_edit): minTime = self.date_from.dateTime().toMSecsSinceEpoch() / 1000 maxTime = self.date_to.dateTime().toMSecsSinceEpoch() / 1000 if minTime > maxTime: minTime = maxTime = minTime if dt_edit == self.date_from else maxTime other = self.date_to if dt_edit == self.date_from else self.date_from with blockSignals(other): other.setDateTime(dt_edit.dateTime()) self.dteditValuesChanged(minTime, maxTime) date_from.dateTimeChanged.connect(lambda: datetime_edited(date_from)) date_to.dateTimeChanged.connect(lambda: datetime_edited(date_to)) # hotfix, does not repaint on click of arrow date_from.calendarWidget().currentPageChanged.connect( lambda: date_from.calendarWidget().repaint()) date_to.calendarWidget().currentPageChanged.connect( lambda: date_to.calendarWidget().repaint()) dtBox.layout().addStretch(100) dtBox.layout().addWidget(date_from) dtBox.layout().addWidget(QLabel(' – ')) dtBox.layout().addWidget(date_to) dtBox.layout().addStretch(100) hCenterBox = gui.hBox(self.controlArea) gui.rubber(hCenterBox) vControlsBox = gui.vBox(hCenterBox) stepThroughBox = gui.vBox(vControlsBox, 'Step/Play Through') gui.rubber(stepThroughBox) gui.checkBox(stepThroughBox, self, 'loop_playback', label='Loop playback') customStepBox = gui.hBox(stepThroughBox) gui.checkBox( customStepBox, self, 'custom_step_size', label='Custom step size: ', toolTip='If not chosen, the active interval moves forward ' '(backward), stepping in increments of its own size.') self.stepsize_combobox = gui.comboBox(customStepBox, self, 'step_size', items=tuple( self.STEP_SIZES.keys()), sendSelectedValue=True) playBox = gui.hBox(stepThroughBox) gui.rubber(playBox) gui.rubber(stepThroughBox) if self.icons_font is None: self.icons_font = load_icons_font() self.step_backward = gui.button( playBox, self, '⏪', callback=lambda: self.play_single_step(backward=True), autoDefault=False) self.step_backward.setFont(self.icons_font) self.play_button = gui.button(playBox, self, '▶️', callback=self.playthrough, toggleButton=True, default=True) self.play_button.setFont(self.icons_font) self.step_forward = gui.button(playBox, self, '⏩', callback=self.play_single_step, autoDefault=False) self.step_forward.setFont(self.icons_font) gui.rubber(playBox) intervalBox = gui.vBox(vControlsBox, 'Playback/Tracking interval') intervalBox.setToolTip( 'In milliseconds, set the delay for playback and ' 'for sending data upon manually moving the interval.') def set_intervals(): self.play_timer.setInterval(1000 * self.playback_interval) self.slider.tracking_timer.setInterval(1000 * self.playback_interval) gui.valueSlider(intervalBox, self, 'playback_interval', label='Delay:', labelFormat='%.2g sec', values=self.DELAY_VALUES, callback=set_intervals) gui.rubber(hCenterBox) gui.rubber(self.controlArea) self._set_disabled(True) def sliderValuesChanged(self, minValue, maxValue): self._delta = max(1, (maxValue - minValue)) minTime = self.slider.scale(minValue) maxTime = self.slider.scale(maxValue) from_dt = QDateTime.fromMSecsSinceEpoch(minTime * 1000).toUTC() to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC() if self.date_from.dateTime() != from_dt: with blockSignals(self.date_from): self.date_from.setDateTime(from_dt) if self.date_from.dateTime() != to_dt: with blockSignals(self.date_to): self.date_to.setDateTime(to_dt) self.send_selection(minTime, maxTime) def dteditValuesChanged(self, minTime, maxTime): minValue = self.slider.unscale(minTime) maxValue = self.slider.unscale(maxTime) if minValue == maxValue: # maxValue's range is minValue's range shifted by one maxValue += 1 maxTime = self.slider.scale(maxValue) to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC() with blockSignals(self.date_to): self.date_to.setDateTime(to_dt) self._delta = max(1, (maxValue - minValue)) if self.slider_values != (minValue, maxValue): self.slider_values = (minValue, maxValue) with blockSignals(self.slider): self.slider.setValues(minValue, maxValue) self.send_selection(minTime, maxTime) def send_selection(self, minTime, maxTime): try: time_values = self.data.time_values except AttributeError: return indices = (minTime <= time_values) & (time_values < maxTime) self.Outputs.subset.send(self.data[indices] if indices.any() else None) def playthrough(self): playing = self.play_button.isChecked() for widget in (self.slider, self.step_forward, self.step_backward): widget.setDisabled(playing) for widget in (self.date_from, self.date_to): widget.setReadOnly(playing) if playing: self.play_timer.start() self.play_button.setText('⏸') else: self.play_timer.stop() self.play_button.setText('▶️') # hotfix self.repaint() def play_single_step(self, backward=False): minValue, maxValue = self.slider.values() orig_delta = delta = self._delta def new_value(value): if self.custom_step_size: step_amount = self.STEP_SIZES[self.step_size] time = datetime.datetime.fromtimestamp( self.slider.scale(value), tz=datetime.timezone.utc) newTime = add_time(time, step_amount, -1 if backward else 1) return self.slider.unscale(newTime.timestamp()) return value + (-delta if backward else delta) if maxValue == self.slider.maximum() and not backward: minValue = self.slider.minimum() maxValue = self.slider.minimum() + delta if not self.loop_playback: self.play_button.click() assert not self.play_timer.isActive() assert not self.play_button.isChecked() elif minValue == self.slider.minimum() and backward: maxValue = self.slider.maximum() minValue = min(self.slider.maximum(), new_value(maxValue)) else: minValue = min(new_value(minValue), self.slider.maximum()) maxValue = min(new_value(maxValue), self.slider.maximum()) # Blocking signals because we want this to be synchronous to avoid # re-setting self._delta with blockSignals(self.slider): self.slider.setValues(minValue, maxValue) self.sliderValuesChanged(self.slider.minimumValue(), self.slider.maximumValue()) self._delta = orig_delta # Override valuesChanged handler # hotfix self.slider.repaint() def _set_disabled(self, is_disabled): if is_disabled and self.play_timer.isActive(): self.play_button.click() assert not self.play_timer.isActive() assert not self.play_button.isChecked() for func in [ self.date_from, self.date_to, self.step_backward, self.play_button, self.step_forward, self.controls.loop_playback, self.controls.step_size, self.controls.playback_interval, self.slider ]: func.setDisabled(is_disabled) @Inputs.data def set_data(self, data): slider = self.slider self.data = data = None if data is None else Timeseries.from_data_table( data) def disabled(): slider.setFormatter(str) slider.setHistogram(None) slider.setScale(0, 0, None) slider.setValues(0, 0) self._set_disabled(True) self.Outputs.subset.send(None) if data is None: disabled() return if not isinstance(data.time_variable, TimeVariable): self.Error.no_time_variable() disabled() return if not data.time_delta.deltas: self.Error.no_time_delta() disabled() return self.Error.clear() var = data.time_variable time_values = data.time_values min_dt = datetime.datetime.fromtimestamp(round(time_values.min()), tz=datetime.timezone.utc) max_dt = datetime.datetime.fromtimestamp(round(time_values.max()), tz=datetime.timezone.utc) # Depending on time delta: # - set slider maximum (granularity) # - set range for end dt (+ 1 timedelta) # - set date format # - set time overlap options delta = data.time_delta.gcd range = max_dt - min_dt if isinstance(delta, Number): maximum = round(range.total_seconds() / delta) timedelta = datetime.timedelta(milliseconds=delta * 1000) min_dt2 = min_dt + timedelta max_dt2 = max_dt + timedelta if delta >= 86400: # more than a day date_format = ''.join(self.DATE_FORMATS[0:3]) else: date_format = ''.join(self.DATE_FORMATS) for k, n in [(k, n) for k, n in self.STEP_SIZES.items() if isinstance(n, Number)]: if delta <= n: min_overlap = k break else: min_overlap = '1 day' else: # isinstance(delta, tuple) if delta[1] == 'month': months = (max_dt.year - min_dt.year) * 12 + \ (max_dt.month - min_dt.month) maximum = months / delta[0] if min_dt.month < 12 - delta[0]: min_dt2 = min_dt.replace(month=min_dt.month + delta[0]) else: min_dt2 = min_dt.replace(year=min_dt.year + 1, month=12 - min_dt.month + delta[0]) if max_dt.month < 12 - delta[0]: max_dt2 = max_dt.replace(month=max_dt.month + delta[0]) else: max_dt2 = max_dt.replace(year=max_dt.year + 1, month=12 - min_dt.month + delta[0]) date_format = ''.join(self.DATE_FORMATS[0:2]) for k, (i, u) in [(k, v) for k, v in self.STEP_SIZES.items() if isinstance(v, tuple) and v[1] == 'month']: if delta[0] <= i: min_overlap = k break else: min_overlap = '1 year' else: # elif delta[1] == 'year': years = max_dt.year - min_dt.year maximum = years / delta[0] min_dt2 = min_dt.replace(year=min_dt.year + delta[0], ) max_dt2 = max_dt.replace(year=max_dt.year + delta[0], ) date_format = self.DATE_FORMATS[0] for k, (i, u) in [(k, v) for k, v in self.STEP_SIZES.items() if isinstance(v, tuple) and v[1] == 'year']: if delta[0] <= i: min_overlap = k break else: raise Exception('Timedelta larger than 100 years') # find max sensible time overlap upper_overlap_limit = range / 2 for k, overlap in self.STEP_SIZES.items(): if isinstance(overlap, Number): if upper_overlap_limit.total_seconds() <= overlap: max_overlap = k break else: i, u = overlap if u == 'month': month_diff = (max_dt.year - min_dt.year) * 12 \ + max(0, max_dt.month - min_dt.month) if month_diff / 2 <= i: max_overlap = k break else: # if u == 'year': year_diff = max_dt.year - min_dt.year if year_diff / 2 <= i: max_overlap = k break else: # last item in step sizes *_, max_overlap = self.STEP_SIZES.keys() self.stepsize_combobox.clear() dict_iter = iter(self.STEP_SIZES.keys()) next_item = next(dict_iter) while next_item != min_overlap: next_item = next(dict_iter) self.stepsize_combobox.addItem(next_item) self.step_size = next_item while next_item != max_overlap: next_item = next(dict_iter) self.stepsize_combobox.addItem(next_item) slider.setMinimum(0) slider.setMaximum(maximum + 1) self._set_disabled(False) slider.setHistogram(time_values) slider.setFormatter(var.repr_val) slider.setScale(time_values.min(), time_values.max(), data.time_delta.gcd) self.sliderValuesChanged(slider.minimumValue(), slider.maximumValue()) def utc_dt(dt): qdt = QDateTime(dt) qdt.setTimeZone(QTimeZone.utc()) return qdt self.date_from.setDateTimeRange(utc_dt(min_dt), utc_dt(max_dt)) self.date_to.setDateTimeRange(utc_dt(min_dt2), utc_dt(max_dt2)) self.date_from.setDisplayFormat(date_format) self.date_to.setDisplayFormat(date_format) def format_time(i): dt = QDateTime.fromMSecsSinceEpoch(i * 1000).toUTC() return dt.toString(date_format) self.slider.setFormatter(format_time) @classmethod def migrate_settings(cls, settings_, version): if version < 2: interval = settings_["playback_interval"] / 1000 if interval in cls.DELAY_VALUES: settings_["playback_interval"] = interval else: settings_["playback_interval"] = 1
class OWTimeSlice(widget.OWWidget): name = 'Time Slice' description = 'Select a slice of measurements on a time interval.' icon = 'icons/TimeSlice.svg' priority = 550 inputs = [ ('Data', Table, 'set_data'), ] outputs = [('Subset', Table)] want_main_area = False class Error(widget.OWWidget.Error): no_time_variable = widget.Msg('Data contains no time variable') MAX_SLIDER_VALUE = 500 DATE_FORMATS = ('yyyy-MM-dd', 'HH:mm:ss.zzz') OVERLAP_AMOUNTS = OrderedDict( (('all but one (= shift by one slider value)', 0), ('6/7 of interval', 6 / 7), ('3/4 of interval', 3 / 4), ('1/2 of interval', 1 / 2), ('1/3 of interval', 1 / 3), ('1/5 of interval', 1 / 5))) loop_playback = settings.Setting(True) steps_overlap = settings.Setting(True) overlap_amount = settings.Setting(next(iter(OVERLAP_AMOUNTS))) playback_interval = settings.Setting(1000) slider_values = settings.Setting((0, .2 * MAX_SLIDER_VALUE)) def __init__(self): super().__init__() self._delta = 0 self.play_timer = QTimer(self, interval=self.playback_interval, timeout=self.play_single_step) slider = self.slider = Slider( Qt.Horizontal, self, minimum=0, maximum=self.MAX_SLIDER_VALUE, tracking=False, valuesChanged=self.valuesChanged, minimumValue=self.slider_values[0], maximumValue=self.slider_values[1], ) slider.setShowText(False) box = gui.vBox(self.controlArea, 'Time Slice') box.layout().addWidget(slider) hbox = gui.hBox(box) def _dateTimeChanged(editted): def handler(): minTime = self.date_from.dateTime().toMSecsSinceEpoch() / 1000 maxTime = self.date_to.dateTime().toMSecsSinceEpoch() / 1000 if minTime > maxTime: minTime = maxTime = minTime if editted == self.date_from else maxTime other = self.date_to if editted == self.date_from else self.date_from with blockSignals(other): other.setDateTime(editted.dateTime()) with blockSignals(self.slider): self.slider.setValues(self.slider.unscale(minTime), self.slider.unscale(maxTime)) self.send_selection(minTime, maxTime) return handler kwargs = dict(calendarPopup=True, displayFormat=' '.join(self.DATE_FORMATS), timeSpec=Qt.UTC) date_from = self.date_from = QDateTimeEdit(self, **kwargs) date_to = self.date_to = QDateTimeEdit(self, **kwargs) date_from.dateTimeChanged.connect(_dateTimeChanged(date_from)) date_to.dateTimeChanged.connect(_dateTimeChanged(date_to)) hbox.layout().addStretch(100) hbox.layout().addWidget(date_from) hbox.layout().addWidget(QLabel(' – ')) hbox.layout().addWidget(date_to) hbox.layout().addStretch(100) vbox = gui.vBox(self.controlArea, 'Step / Play Through') gui.checkBox(vbox, self, 'loop_playback', label='Loop playback') hbox = gui.hBox(vbox) gui.checkBox(hbox, self, 'steps_overlap', label='Stepping overlaps by:', toolTip='If enabled, the active interval moves forward ' '(backward) by half of the interval at each step.') gui.comboBox(hbox, self, 'overlap_amount', items=tuple(self.OVERLAP_AMOUNTS.keys()), sendSelectedValue=True) gui.spin(vbox, self, 'playback_interval', label='Playback delay (msec):', minv=100, maxv=30000, step=200, callback=lambda: self.play_timer.setInterval( self.playback_interval)) hbox = gui.hBox(vbox) self.step_backward = gui.button( hbox, self, '⏮', callback=lambda: self.play_single_step(backward=True), autoDefault=False) self.play_button = gui.button(hbox, self, '▶', callback=self.playthrough, toggleButton=True, default=True) self.step_forward = gui.button(hbox, self, '⏭', callback=self.play_single_step, autoDefault=False) gui.rubber(self.controlArea) def valuesChanged(self, minValue, maxValue): self.slider_values = (minValue, maxValue) self._delta = max(1, (maxValue - minValue)) minTime = self.slider.scale(minValue) maxTime = self.slider.scale(maxValue) from_dt = QDateTime.fromMSecsSinceEpoch(minTime * 1000).toUTC() to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC() with blockSignals(self.date_from, self.date_to): self.date_from.setDateTime(from_dt) self.date_to.setDateTime(to_dt) self.send_selection(minTime, maxTime) def send_selection(self, minTime, maxTime): try: time_values = self.data.time_values except AttributeError: return indices = (minTime <= time_values) & (time_values <= maxTime) self.send('Subset', self.data[indices] if indices.any() else None) def playthrough(self): playing = self.play_button.isChecked() for widget in (self.slider, self.step_forward, self.step_backward): widget.setDisabled(playing) if playing: self.play_timer.start() self.play_button.setText('▮▮') else: self.play_timer.stop() self.play_button.setText('▶') def play_single_step(self, backward=False): op = operator.sub if backward else operator.add minValue, maxValue = self.slider.values() orig_delta = delta = self._delta if self.steps_overlap: overlap_amount = self.OVERLAP_AMOUNTS[self.overlap_amount] if overlap_amount: delta = max(1, int(round(delta * (1 - overlap_amount)))) else: delta = 1 # single slider step (== 1/self.MAX_SLIDER_VALUE) if maxValue == self.slider.maximum() and not backward: minValue = self.slider.minimum() maxValue = minValue + orig_delta if not self.loop_playback: self.play_button.click() assert not self.play_timer.isActive() assert not self.play_button.isChecked() elif minValue == self.slider.minimum() and backward: maxValue = self.slider.maximum() minValue = maxValue - orig_delta else: minValue = op(minValue, delta) maxValue = op(maxValue, delta) # Blocking signals because we want this to be synchronous to avoid # re-setting self._delta with blockSignals(self.slider): self.slider.setValues(minValue, maxValue) self.valuesChanged(self.slider.minimumValue(), self.slider.maximumValue()) self._delta = orig_delta # Override valuesChanged handler def set_data(self, data): slider = self.slider self.data = data = None if data is None else Timeseries.from_data_table( data) def disabled(): slider.setFormatter(str) slider.setHistogram(None) slider.setScale(0, 0) slider.setValues(0, 0) slider.setDisabled(True) self.send('Subset', None) if data is None: disabled() return if not isinstance(data.time_variable, TimeVariable): self.Error.no_time_variable() disabled() return self.Error.clear() var = data.time_variable time_values = data.time_values slider.setDisabled(False) slider.setHistogram(time_values) slider.setFormatter(var.repr_val) slider.setScale(time_values.min(), time_values.max()) self.valuesChanged(slider.minimumValue(), slider.maximumValue()) # Update datetime edit fields min_dt = QDateTime.fromMSecsSinceEpoch(time_values[0] * 1000).toUTC() max_dt = QDateTime.fromMSecsSinceEpoch(time_values[-1] * 1000).toUTC() self.date_from.setDateTimeRange(min_dt, max_dt) self.date_to.setDateTimeRange(min_dt, max_dt) date_format = ' '.join( (self.DATE_FORMATS[0] if var.have_date else '', self.DATE_FORMATS[1] if var.have_time else '')).strip() self.date_from.setDisplayFormat(date_format) self.date_to.setDisplayFormat(date_format)
class OWTimeSlice(widget.OWWidget): name = 'Time Slice' description = 'Select a slice of measurements on a time interval.' icon = 'icons/TimeSlice.svg' priority = 550 class Inputs: data = Input("Data", Table) class Outputs: subset = Output("Subset", Table) want_main_area = False class Error(widget.OWWidget.Error): no_time_variable = widget.Msg('Data contains no time variable') MAX_SLIDER_VALUE = 500 DATE_FORMATS = ('yyyy-MM-dd', 'HH:mm:ss.zzz') OVERLAP_AMOUNTS = OrderedDict(( ('all but one (= shift by one slider value)', 0), ('6/7 of interval', 6/7), ('3/4 of interval', 3/4), ('1/2 of interval', 1/2), ('1/3 of interval', 1/3), ('1/5 of interval', 1/5))) loop_playback = settings.Setting(True) steps_overlap = settings.Setting(True) overlap_amount = settings.Setting(next(iter(OVERLAP_AMOUNTS))) playback_interval = settings.Setting(1000) slider_values = settings.Setting((0, .2 * MAX_SLIDER_VALUE)) def __init__(self): super().__init__() self._delta = 0 self.play_timer = QTimer(self, interval=self.playback_interval, timeout=self.play_single_step) slider = self.slider = Slider(Qt.Horizontal, self, minimum=0, maximum=self.MAX_SLIDER_VALUE, tracking=False, valuesChanged=self.valuesChanged, minimumValue=self.slider_values[0], maximumValue=self.slider_values[1],) slider.setShowText(False) box = gui.vBox(self.controlArea, 'Time Slice') box.layout().addWidget(slider) hbox = gui.hBox(box) def _dateTimeChanged(editted): def handler(): minTime = self.date_from.dateTime().toMSecsSinceEpoch() / 1000 maxTime = self.date_to.dateTime().toMSecsSinceEpoch() / 1000 if minTime > maxTime: minTime = maxTime = minTime if editted == self.date_from else maxTime other = self.date_to if editted == self.date_from else self.date_from with blockSignals(other): other.setDateTime(editted.dateTime()) with blockSignals(self.slider): self.slider.setValues(self.slider.unscale(minTime), self.slider.unscale(maxTime)) self.send_selection(minTime, maxTime) return handler kwargs = dict(calendarPopup=True, displayFormat=' '.join(self.DATE_FORMATS), timeSpec=Qt.UTC) date_from = self.date_from = QDateTimeEdit(self, **kwargs) date_to = self.date_to = QDateTimeEdit(self, **kwargs) date_from.dateTimeChanged.connect(_dateTimeChanged(date_from)) date_to.dateTimeChanged.connect(_dateTimeChanged(date_to)) hbox.layout().addStretch(100) hbox.layout().addWidget(date_from) hbox.layout().addWidget(QLabel(' – ')) hbox.layout().addWidget(date_to) hbox.layout().addStretch(100) vbox = gui.vBox(self.controlArea, 'Step / Play Through') gui.checkBox(vbox, self, 'loop_playback', label='Loop playback') hbox = gui.hBox(vbox) gui.checkBox(hbox, self, 'steps_overlap', label='Stepping overlaps by:', toolTip='If enabled, the active interval moves forward ' '(backward) by half of the interval at each step.') gui.comboBox(hbox, self, 'overlap_amount', items=tuple(self.OVERLAP_AMOUNTS.keys()), sendSelectedValue=True) gui.spin(vbox, self, 'playback_interval', label='Playback delay (msec):', minv=100, maxv=30000, step=200, callback=lambda: self.play_timer.setInterval(self.playback_interval)) hbox = gui.hBox(vbox) self.step_backward = gui.button(hbox, self, '⏮', callback=lambda: self.play_single_step(backward=True), autoDefault=False) self.play_button = gui.button(hbox, self, '▶', callback=self.playthrough, toggleButton=True, default=True) self.step_forward = gui.button(hbox, self, '⏭', callback=self.play_single_step, autoDefault=False) gui.rubber(self.controlArea) self._set_disabled(True) def valuesChanged(self, minValue, maxValue): self.slider_values = (minValue, maxValue) self._delta = max(1, (maxValue - minValue)) minTime = self.slider.scale(minValue) maxTime = self.slider.scale(maxValue) from_dt = QDateTime.fromMSecsSinceEpoch(minTime * 1000).toUTC() to_dt = QDateTime.fromMSecsSinceEpoch(maxTime * 1000).toUTC() with blockSignals(self.date_from, self.date_to): self.date_from.setDateTime(from_dt) self.date_to.setDateTime(to_dt) self.send_selection(minTime, maxTime) def send_selection(self, minTime, maxTime): try: time_values = self.data.time_values except AttributeError: return indices = (minTime <= time_values) & (time_values <= maxTime) self.Outputs.subset.send(self.data[indices] if indices.any() else None) def playthrough(self): playing = self.play_button.isChecked() for widget in (self.slider, self.step_forward, self.step_backward): widget.setDisabled(playing) if playing: self.play_timer.start() self.play_button.setText('▮▮') else: self.play_timer.stop() self.play_button.setText('▶') def play_single_step(self, backward=False): op = operator.sub if backward else operator.add minValue, maxValue = self.slider.values() orig_delta = delta = self._delta if self.steps_overlap: overlap_amount = self.OVERLAP_AMOUNTS[self.overlap_amount] if overlap_amount: delta = max(1, int(round(delta * (1 - overlap_amount)))) else: delta = 1 # single slider step (== 1/self.MAX_SLIDER_VALUE) if maxValue == self.slider.maximum() and not backward: minValue = self.slider.minimum() maxValue = minValue + orig_delta if not self.loop_playback: self.play_button.click() assert not self.play_timer.isActive() assert not self.play_button.isChecked() elif minValue == self.slider.minimum() and backward: maxValue = self.slider.maximum() minValue = maxValue - orig_delta else: minValue = op(minValue, delta) maxValue = op(maxValue, delta) # Blocking signals because we want this to be synchronous to avoid # re-setting self._delta with blockSignals(self.slider): self.slider.setValues(minValue, maxValue) self.valuesChanged(self.slider.minimumValue(), self.slider.maximumValue()) self._delta = orig_delta # Override valuesChanged handler def _set_disabled(self, is_disabled): for func in [self.date_from, self.date_to, self.step_backward, self.play_button, self.step_forward, self.controls.loop_playback, self.controls.steps_overlap, self.controls.overlap_amount, self.controls.playback_interval, self.slider]: func.setDisabled(is_disabled) @Inputs.data def set_data(self, data): slider = self.slider self.data = data = None if data is None else Timeseries.from_data_table(data) def disabled(): slider.setFormatter(str) slider.setHistogram(None) slider.setScale(0, 0) slider.setValues(0, 0) self._set_disabled(True) self.Outputs.subset.send(None) if data is None: disabled() return if not isinstance(data.time_variable, TimeVariable): self.Error.no_time_variable() disabled() return self.Error.clear() var = data.time_variable time_values = data.time_values self._set_disabled(False) slider.setHistogram(time_values) slider.setFormatter(var.repr_val) slider.setScale(time_values.min(), time_values.max()) self.valuesChanged(slider.minimumValue(), slider.maximumValue()) # Update datetime edit fields min_dt = QDateTime.fromMSecsSinceEpoch(time_values[0] * 1000).toUTC() max_dt = QDateTime.fromMSecsSinceEpoch(time_values[-1] * 1000).toUTC() self.date_from.setDateTimeRange(min_dt, max_dt) self.date_to.setDateTimeRange(min_dt, max_dt) date_format = ' '.join((self.DATE_FORMATS[0] if var.have_date else '', self.DATE_FORMATS[1] if var.have_time else '')).strip() self.date_from.setDisplayFormat(date_format) self.date_to.setDisplayFormat(date_format)