def test_dynamic_link(self): link = LinkItem() anchor1 = AnchorPoint() anchor2 = AnchorPoint() self.scene.addItem(link) self.scene.addItem(anchor1) self.scene.addItem(anchor2) link.setSourceItem(None, anchor1) link.setSinkItem(None, anchor2) anchor2.setPos(100, 100) link.setSourceName("1") link.setSinkName("2") link.setDynamic(True) self.assertTrue(link.isDynamic()) link.setDynamicEnabled(True) self.assertTrue(link.isDynamicEnabled()) def advance(): clock = time.process_time() link.setDynamic(clock > 1) link.setDynamicEnabled(int(clock) % 2 == 0) timer = QTimer(link, interval=0) timer.timeout.connect(advance) timer.start() self.app.exec_()
class QCoreAppTestCase(unittest.TestCase): _AppClass = QCoreApplication @classmethod def setUpClass(cls): super(QCoreAppTestCase, cls).setUpClass() app = cls._AppClass.instance() if app is None: app = cls._AppClass([]) cls.app = app def setUp(self): super(QCoreAppTestCase, self).setUp() self._quittimer = QTimer(interval=1000) self._quittimer.timeout.connect(self.app.quit) self._quittimer.start() def tearDown(self): self._quittimer.stop() self._quittimer.timeout.disconnect(self.app.quit) self._quittimer = None super(QCoreAppTestCase, self).tearDown() @classmethod def tearDownClass(cls): gc.collect() cls.app = None super(QCoreAppTestCase, cls).tearDownClass()
def test_outputview(self): output = OutputView() output.show() line1 = "A line \n" line2 = "A different line\n" output.write(line1) self.assertEqual(output.toPlainText(), line1) output.write(line2) self.assertEqual(output.toPlainText(), line1 + line2) output.clear() self.assertEqual(output.toPlainText(), "") output.writelines([line1, line2]) self.assertEqual(output.toPlainText(), line1 + line2) output.setMaximumLines(5) def advance(): now = datetime.now().strftime("%c\n") output.write(now) text = output.toPlainText() self.assertLessEqual(len(text.splitlines()), 5) timer = QTimer(output, interval=500) timer.timeout.connect(advance) timer.start() self.app.exec_()
def test_arrowannotation(self): item = ArrowItem() self.scene.addItem(item) item.setLine(QLineF(100, 100, 100, 200)) item.setLineWidth(5) item = ArrowItem() item.setLine(QLineF(150, 100, 150, 200)) item.setLineWidth(10) item.setArrowStyle(ArrowItem.Concave) self.scene.addItem(item) item = ArrowAnnotation() item.setPos(10, 10) item.setLine(QLineF(10, 10, 200, 200)) self.scene.addItem(item) item.setLineWidth(5) def advance(): clock = time.process_time() * 10 item.setLineWidth(5 + math.sin(clock) * 5) item.setColor(QColor(Qt.red).lighter(100 + 30 * math.cos(clock))) timer = QTimer(item, interval=10) timer.timeout.connect(advance) timer.start() self.app.exec_()
def test_framelesswindow(self): window = FramelessWindow() window.show() def cycle(): window.setRadius((window.radius() + 3) % 30) timer = QTimer(window, interval=250) timer.timeout.connect(cycle) timer.start() self.app.exec_()
def test_layout(self): one_desc, negate_desc, cons_desc = self.widget_desc() one_item = NodeItem() one_item.setWidgetDescription(one_desc) one_item.setPos(0, 150) self.scene.add_node_item(one_item) cons_item = NodeItem() cons_item.setWidgetDescription(cons_desc) cons_item.setPos(200, 0) self.scene.add_node_item(cons_item) negate_item = NodeItem() negate_item.setWidgetDescription(negate_desc) negate_item.setPos(200, 300) self.scene.add_node_item(negate_item) link = LinkItem() link.setSourceItem(one_item) link.setSinkItem(negate_item) self.scene.add_link_item(link) link = LinkItem() link.setSourceItem(one_item) link.setSinkItem(cons_item) self.scene.add_link_item(link) layout = AnchorLayout() self.scene.addItem(layout) self.scene.set_anchor_layout(layout) layout.invalidateNode(one_item) layout.activate() p1, p2 = one_item.outputAnchorItem.anchorPositions() self.assertTrue(p1 > p2) self.scene.node_item_position_changed.connect(layout.invalidateNode) path = QPainterPath() path.addEllipse(125, 0, 50, 300) def advance(): t = time.process_time() cons_item.setPos(path.pointAtPercent(t % 1.0)) negate_item.setPos(path.pointAtPercent((t + 0.5) % 1.0)) timer = QTimer(negate_item, interval=20) timer.start() timer.timeout.connect(advance) self.app.exec_()
def test_dock_mainwinow(self): mw = QMainWindow() dock = CollapsibleDockWidget() w1 = QTextEdit() w2 = QToolButton() w2.setFixedSize(38, 200) dock.setExpandedWidget(w1) dock.setCollapsedWidget(w2) mw.addDockWidget(Qt.LeftDockWidgetArea, dock) mw.setCentralWidget(QTextEdit()) mw.show() timer = QTimer(dock, interval=200) timer.timeout.connect(lambda: dock.setExpanded(not dock.expanded())) timer.start()
def test_anchoritem(self): anchoritem = NodeAnchorItem(None) self.scene.addItem(anchoritem) path = QPainterPath() path.addEllipse(0, 0, 100, 100) anchoritem.setAnchorPath(path) anchor = AnchorPoint() anchoritem.addAnchor(anchor) ellipse1 = QGraphicsEllipseItem(-3, -3, 6, 6) ellipse2 = QGraphicsEllipseItem(-3, -3, 6, 6) self.scene.addItem(ellipse1) self.scene.addItem(ellipse2) anchor.scenePositionChanged.connect(ellipse1.setPos) with self.assertRaises(ValueError): anchoritem.addAnchor(anchor) anchor1 = AnchorPoint() anchoritem.addAnchor(anchor1) anchor1.scenePositionChanged.connect(ellipse2.setPos) self.assertSequenceEqual(anchoritem.anchorPoints(), [anchor, anchor1]) self.assertSequenceEqual(anchoritem.anchorPositions(), [0.5, 0.5]) anchoritem.setAnchorPositions([0.5, 0.0]) self.assertSequenceEqual(anchoritem.anchorPositions(), [0.5, 0.0]) def advance(): t = anchoritem.anchorPositions() t = [(t + 0.05) % 1.0 for t in t] anchoritem.setAnchorPositions(t) timer = QTimer(anchoritem, interval=20) timer.start() timer.timeout.connect(advance) self.app.exec_()
def test_splitter_resizer(self): w = QSplitter(orientation=Qt.Vertical) w.addWidget(QWidget()) text = QTextEdit() w.addWidget(text) resizer = SplitterResizer(parent=None) resizer.setSplitterAndWidget(w, text) def toogle(): if resizer.size() == 0: resizer.open() else: resizer.close() w.show() timer = QTimer(resizer, interval=1000) timer.timeout.connect(toogle) timer.start() self.app.exec_()
class JitterTool(DataTool): """ Jitter points around the mouse position. """ def __init__(self, parent, plot): super().__init__(parent, plot) self.__timer = QTimer(self, interval=50) self.__timer.timeout.connect(self._do) self._pos = None self._radius = 20.0 self._intensity = 5.0 self.__count = itertools.count() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.editingStarted.emit() self._pos = self.mapToPlot(event.pos()) self.__timer.start() return True else: return super().mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: self._pos = self.mapToPlot(event.pos()) return True else: return super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.__timer.stop() self.editingFinished.emit() return True else: return super().mouseReleaseEvent(event) def _do(self): self.issueCommand.emit( Jitter(self._pos, self._radius, self._intensity, next(self.__count)) )
class AirBrushTool(DataTool): """ Add points with an 'air brush'. """ only2d = False def __init__(self, parent, plot): super().__init__(parent, plot) self.__timer = QTimer(self, interval=50) self.__timer.timeout.connect(self.__timout) self.__count = itertools.count() self.__pos = None def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.editingStarted.emit() self.__pos = self.mapToPlot(event.pos()) self.__timer.start() return True else: return super().mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: self.__pos = self.mapToPlot(event.pos()) return True else: return super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.__timer.stop() self.editingFinished.emit() return True else: return super().mouseReleaseEvent(event) def __timout(self): self.issueCommand.emit( AirBrush(self.__pos, None, None, next(self.__count)) )
class MagnetTool(DataTool): """ Draw points closer to the mouse position. """ def __init__(self, parent, plot): super().__init__(parent, plot) self.__timer = QTimer(self, interval=50) self.__timer.timeout.connect(self.__timeout) self._radius = 20.0 self._density = 4.0 self._pos = None def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.editingStarted.emit() self._pos = self.mapToPlot(event.pos()) self.__timer.start() return True else: return super().mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: self._pos = self.mapToPlot(event.pos()) return True else: return super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.__timer.stop() self.editingFinished.emit() return True else: return super().mouseReleaseEvent(event) def __timeout(self): self.issueCommand.emit( Magnet(self._pos, self._radius, self._density) )
class PreviewModel(QStandardItemModel): """A model for preview items. """ def __init__(self, parent=None, items=None): QStandardItemModel.__init__(self, parent) if items is not None: self.insertColumn(0, items) self.__timer = QTimer(self) def delayedScanUpdate(self, delay=10): """Run a delayed preview item scan update. """ def iter_update(items): for item in items: try: scanner.scan_update(item) except Exception: log.error("An unexpected error occurred while " "scanning %r.", str(item.text()), exc_info=True) item.setEnabled(False) yield items = [self.item(i) for i in range(self.rowCount())] iter_scan = iter_update(items) def process_one(): try: next(iter_scan) except StopIteration: self.__timer.timeout.disconnect(process_one) self.__timer.stop() self.__timer.timeout.connect(process_one) self.__timer.start(delay)
def test_splashscreen(self): splash = pkg_resources.resource_filename( config.__package__, "icons/orange-canvas-core-splash.svg" ) w = SplashScreen() w.setPixmap(QPixmap(splash)) w.setTextRect(QRect(100, 100, 400, 50)) w.show() def advance_time(): now = datetime.now() time = now.strftime("%c : %f") w.showMessage(time, alignment=Qt.AlignCenter) i = now.second % 3 rect = QRect(100, 100 + i * 20, 400, 50) w.setTextRect(rect) self.assertEqual(w.textRect(), rect) timer = QTimer(w, interval=1) timer.timeout.connect(advance_time) timer.start() self.app.exec_()
def test_dock_standalone(self): widget = QWidget() layout = QHBoxLayout() widget.setLayout(layout) layout.addStretch(1) widget.show() dock = CollapsibleDockWidget() layout.addWidget(dock) list_view = QListView() list_view.setModel(QStringListModel(["a", "b"], list_view)) label = QLabel("A label. ") label.setWordWrap(True) dock.setExpandedWidget(label) dock.setCollapsedWidget(list_view) dock.setExpanded(True) dock.setExpanded(False) timer = QTimer(dock, interval=200) timer.timeout.connect(lambda: dock.setExpanded(not dock.expanded())) timer.start()
def test_nodeitem(self): one_item = NodeItem() one_item.setWidgetDescription(self.one_desc) one_item.setWidgetCategory(self.const_desc) one_item.setTitle("Neo") self.assertEqual(one_item.title(), "Neo") one_item.setProcessingState(True) self.assertEqual(one_item.processingState(), True) one_item.setProgress(50) self.assertEqual(one_item.progress(), 50) one_item.setProgress(100) self.assertEqual(one_item.progress(), 100) one_item.setProgress(101) self.assertEqual(one_item.progress(), 100, "Progress overshots") one_item.setProcessingState(False) self.assertEqual(one_item.processingState(), False) self.assertEqual(one_item.progress(), -1, "setProcessingState does not clear the progress.") self.scene.addItem(one_item) one_item.setPos(100, 100) negate_item = NodeItem() negate_item.setWidgetDescription(self.negate_desc) negate_item.setWidgetCategory(self.const_desc) self.scene.addItem(negate_item) negate_item.setPos(300, 100) nb_item = NodeItem() nb_item.setWidgetDescription(self.add_desc) nb_item.setWidgetCategory(self.operator_desc) self.scene.addItem(nb_item) nb_item.setPos(500, 100) positions = [] anchor = one_item.newOutputAnchor() anchor.scenePositionChanged.connect(positions.append) one_item.setPos(110, 100) self.assertTrue(len(positions) > 0) one_item.setErrorMessage("message") one_item.setWarningMessage("message") one_item.setInfoMessage("I am alive") one_item.setErrorMessage(None) one_item.setWarningMessage(None) one_item.setInfoMessage(None) one_item.setInfoMessage("I am back.") nb_item.setProcessingState(1) def progress(): p = (nb_item.progress() + 1) % 100 nb_item.setProgress(p) if p > 50: nb_item.setInfoMessage("Over 50%") one_item.setWarningMessage("Second") else: nb_item.setInfoMessage(None) one_item.setWarningMessage(None) negate_item.setAnchorRotation(50 - p) timer = QTimer(nb_item, interval=10) timer.start() timer.timeout.connect(progress) self.app.exec_()
class OWPCA(widget.OWWidget): name = "PCA" description = "Principal component analysis with a scree-diagram." icon = "icons/PCA.svg" priority = 3050 inputs = [("Data", Table, "set_data")] outputs = [("Transformed data", Table), ("Components", Table), ("PCA", PCA)] ncomponents = settings.Setting(2) variance_covered = settings.Setting(100) batch_size = settings.Setting(100) address = settings.Setting('') auto_update = settings.Setting(True) auto_commit = settings.Setting(True) normalize = settings.Setting(True) maxp = settings.Setting(20) axis_labels = settings.Setting(10) graph_name = "plot.plotItem" def __init__(self): super().__init__() self.data = None self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = False self._pca_projector = PCA() self._pca_projector.component = self.ncomponents self._pca_preprocessors = PCA.preprocessors # Components Selection box = gui.vBox(self.controlArea, "Components Selection") form = QFormLayout() box.layout().addLayout(form) self.components_spin = gui.spin( box, self, "ncomponents", 0, 1000, callback=self._update_selection_component_spin, keyboardTracking=False ) self.components_spin.setSpecialValueText("All") self.variance_spin = gui.spin( box, self, "variance_covered", 1, 100, callback=self._update_selection_variance_spin, keyboardTracking=False ) self.variance_spin.setSuffix("%") form.addRow("Components:", self.components_spin) form.addRow("Variance covered:", self.variance_spin) # Incremental learning self.sampling_box = gui.vBox(self.controlArea, "Incremental learning") self.addresstext = QLineEdit(box) self.addresstext.setPlaceholderText('Remote server') if self.address: self.addresstext.setText(self.address) self.sampling_box.layout().addWidget(self.addresstext) form = QFormLayout() self.sampling_box.layout().addLayout(form) self.batch_spin = gui.spin( self.sampling_box, self, "batch_size", 50, 100000, step=50, keyboardTracking=False) form.addRow("Batch size ~ ", self.batch_spin) self.start_button = gui.button( self.sampling_box, self, "Start remote computation", callback=self.start, autoDefault=False, tooltip="Start/abort computation on the server") self.start_button.setEnabled(False) gui.checkBox(self.sampling_box, self, "auto_update", "Periodically fetch model", callback=self.update_model) self.__timer = QTimer(self, interval=2000) self.__timer.timeout.connect(self.get_model) self.sampling_box.setVisible(remotely) # Options self.options_box = gui.vBox(self.controlArea, "Options") gui.checkBox(self.options_box, self, "normalize", "Normalize data", callback=self._update_normalize) self.maxp_spin = gui.spin( self.options_box, self, "maxp", 1, 100, label="Show only first", callback=self._setup_plot, keyboardTracking=False ) self.controlArea.layout().addStretch() gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") self.plot = pg.PlotWidget(background="w") axis = self.plot.getAxis("bottom") axis.setLabel("Principal Components") axis = self.plot.getAxis("left") axis.setLabel("Proportion of variance") self.plot_horlabels = [] self.plot_horlines = [] self.plot.getViewBox().setMenuEnabled(False) self.plot.getViewBox().setMouseEnabled(False, False) self.plot.showGrid(True, True, alpha=0.5) self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) self.mainArea.layout().addWidget(self.plot) self._update_normalize() def update_model(self): self.get_model() if self.auto_update and self.rpca and not self.rpca.ready(): self.__timer.start(2000) else: self.__timer.stop() def start(self): if 'Abort' in self.start_button.text(): self.rpca.abort() self.__timer.stop() self.start_button.setText("Start remote computation") else: self.address = self.addresstext.text() with remote.server(self.address): from Orange.projection.pca import RemotePCA maxiter = (1e5 + self.data.approx_len()) / self.batch_size * 3 self.rpca = RemotePCA(self.data, self.batch_size, int(maxiter)) self.update_model() self.start_button.setText("Abort remote computation") def set_data(self, data): self.information() if isinstance(data, SqlTable): if data.approx_len() < AUTO_DL_LIMIT: data = Table(data) elif not remotely: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = data self.fit() def fit(self): self.clear() self.start_button.setEnabled(False) if self.data is None: return data = self.data self._transformed = None if isinstance(data, SqlTable): # data was big and remote available self.sampling_box.setVisible(True) self.start_button.setText("Start remote computation") self.start_button.setEnabled(True) else: self.sampling_box.setVisible(False) pca = self._pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = numpy.cumsum(variance_ratio) self.components_spin.setRange(0, len(cumulative)) self._pca = pca self._variance_ratio = variance_ratio self._cumulative = cumulative self._setup_plot() self.unconditional_commit() def clear(self): self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = None self.plot_horlabels = [] self.plot_horlines = [] self.plot.clear() def get_model(self): if self.rpca is None: return if self.rpca.ready(): self.__timer.stop() self.start_button.setText("Restart (finished)") self._pca = self.rpca.get_state() if self._pca is None: return self._variance_ratio = self._pca.explained_variance_ratio_ self._cumulative = numpy.cumsum(self._variance_ratio) self._setup_plot() self._transformed = None self.commit() def _setup_plot(self): self.plot.clear() explained_ratio = self._variance_ratio explained = self._cumulative p = min(len(self._variance_ratio), self.maxp) self.plot.plot(numpy.arange(p), explained_ratio[:p], pen=pg.mkPen(QColor(Qt.red), width=2), antialias=True, name="Variance") self.plot.plot(numpy.arange(p), explained[:p], pen=pg.mkPen(QColor(Qt.darkYellow), width=2), antialias=True, name="Cumulative Variance") cutpos = self._nselected_components() - 1 self._line = pg.InfiniteLine( angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) self._line.setCursor(Qt.SizeHorCursor) self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) self._line.sigPositionChanged.connect(self._on_cut_changed) self.plot.addItem(self._line) self.plot_horlines = ( pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) self.plot_horlabels = ( pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) for item in self.plot_horlabels + self.plot_horlines: self.plot.addItem(item) self._set_horline_pos() self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) self._update_axis() def _set_horline_pos(self): cutidx = self.ncomponents - 1 for line, label, curve in zip(self.plot_horlines, self.plot_horlabels, (self._variance_ratio, self._cumulative)): y = curve[cutidx] line.setData([-1, cutidx], 2 * [y]) label.setPos(cutidx, y) label.setPlainText("{:.3f}".format(y)) def _on_cut_changed(self, line): # cut changed by means of a cut line over the scree plot. value = int(round(line.value())) self._line.setValue(value) current = self._nselected_components() components = value + 1 if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components self._set_horline_pos() if self._pca is not None: self.variance_covered = self._cumulative[components - 1] * 100 if current != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): # cut changed by "ncomponents" spin. if self._pca is None: self._invalidate_selection() return if self.ncomponents == 0: # Special "All" value cut = len(self._variance_ratio) else: cut = self.ncomponents self.variance_covered = self._cumulative[cut - 1] * 100 if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_selection_variance_spin(self): # cut changed by "max variance" spin. if self._pca is None: return cut = numpy.searchsorted(self._cumulative, self.variance_covered / 100.0) + 1 cut = min(cut, len(self._cumulative)) self.ncomponents = cut if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_normalize(self): if self.normalize: pp = self._pca_preprocessors + [Normalize()] else: pp = self._pca_preprocessors self._pca_projector.preprocessors = pp self.fit() if self.data is None: self._invalidate_selection() def _nselected_components(self): """Return the number of selected components.""" if self._pca is None: return 0 if self.ncomponents == 0: # Special "All" value max_comp = len(self._variance_ratio) else: max_comp = self.ncomponents var_max = self._cumulative[max_comp - 1] if var_max != numpy.floor(self.variance_covered / 100.0): cut = max_comp self.variance_covered = var_max * 100 else: self.ncomponents = cut = numpy.searchsorted( self._cumulative, self.variance_covered / 100.0) + 1 return cut def _invalidate_selection(self): self.commit() def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p-1)//(self.axis_labels-1), 1) axis.setTicks([[(i, str(i+1)) for i in range(0, p, d)]]) def commit(self): transformed = components = None if self._pca is not None: if self._transformed is None: # Compute the full transform (all components) only once. self._transformed = self._pca(self.data) transformed = self._transformed domain = Domain( transformed.domain.attributes[:self.ncomponents], self.data.domain.class_vars, self.data.domain.metas ) transformed = transformed.from_table(domain, transformed) dom = Domain(self._pca.orig_domain.attributes, metas=[StringVariable(name='component')]) metas = numpy.array([['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T components = Table(dom, self._pca.components_[:self.ncomponents], metas=metas) components.name = 'components' self._pca_projector.component = self.ncomponents self.send("Transformed data", transformed) self.send("Components", components) self.send("PCA", self._pca_projector) def send_report(self): if self.data is None: return self.report_items(( ("Selected components", self.ncomponents), ("Explained variance", "{:.3f} %".format(self.variance_covered)) )) self.report_plot()
class OWPCA(widget.OWWidget): name = "PCA" description = "Principal component analysis with a scree-diagram." icon = "icons/PCA.svg" priority = 3050 inputs = [("Data", Table, "set_data")] outputs = [("Transformed data", Table), ("Components", Table), ("PCA", PCA)] ncomponents = settings.Setting(2) variance_covered = settings.Setting(100) batch_size = settings.Setting(100) address = settings.Setting('') auto_update = settings.Setting(True) auto_commit = settings.Setting(True) normalize = settings.Setting(True) maxp = settings.Setting(20) axis_labels = settings.Setting(10) graph_name = "plot.plotItem" class Warning(widget.OWWidget.Warning): trivial_components = widget.Msg( "All components of the PCA are trivial (explain 0 variance). " "Input data is constant (or near constant).") class Error(widget.OWWidget.Error): no_features = widget.Msg("At least 1 feature is required") no_instances = widget.Msg("At least 1 data instance is required") sparse_data = widget.Msg("Sparse data is not supported") def __init__(self): super().__init__() self.data = None self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = False # max_components limit allows scikit-learn to select a faster method for big data self._pca_projector = PCA(max_components=MAX_COMPONENTS) self._pca_projector.component = self.ncomponents self._pca_preprocessors = PCA.preprocessors # Components Selection box = gui.vBox(self.controlArea, "Components Selection") form = QFormLayout() box.layout().addLayout(form) self.components_spin = gui.spin( box, self, "ncomponents", 1, MAX_COMPONENTS, callback=self._update_selection_component_spin, keyboardTracking=False) self.components_spin.setSpecialValueText("All") self.variance_spin = gui.spin( box, self, "variance_covered", 1, 100, callback=self._update_selection_variance_spin, keyboardTracking=False) self.variance_spin.setSuffix("%") form.addRow("Components:", self.components_spin) form.addRow("Variance covered:", self.variance_spin) # Incremental learning self.sampling_box = gui.vBox(self.controlArea, "Incremental learning") self.addresstext = QLineEdit(box) self.addresstext.setPlaceholderText('Remote server') if self.address: self.addresstext.setText(self.address) self.sampling_box.layout().addWidget(self.addresstext) form = QFormLayout() self.sampling_box.layout().addLayout(form) self.batch_spin = gui.spin(self.sampling_box, self, "batch_size", 50, 100000, step=50, keyboardTracking=False) form.addRow("Batch size ~ ", self.batch_spin) self.start_button = gui.button( self.sampling_box, self, "Start remote computation", callback=self.start, autoDefault=False, tooltip="Start/abort computation on the server") self.start_button.setEnabled(False) gui.checkBox(self.sampling_box, self, "auto_update", "Periodically fetch model", callback=self.update_model) self.__timer = QTimer(self, interval=2000) self.__timer.timeout.connect(self.get_model) self.sampling_box.setVisible(remotely) # Options self.options_box = gui.vBox(self.controlArea, "Options") gui.checkBox(self.options_box, self, "normalize", "Normalize data", callback=self._update_normalize) self.maxp_spin = gui.spin(self.options_box, self, "maxp", 1, MAX_COMPONENTS, label="Show only first", callback=self._setup_plot, keyboardTracking=False) self.controlArea.layout().addStretch() gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") self.plot = pg.PlotWidget(background="w") axis = self.plot.getAxis("bottom") axis.setLabel("Principal Components") axis = self.plot.getAxis("left") axis.setLabel("Proportion of variance") self.plot_horlabels = [] self.plot_horlines = [] self.plot.getViewBox().setMenuEnabled(False) self.plot.getViewBox().setMouseEnabled(False, False) self.plot.showGrid(True, True, alpha=0.5) self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) self.mainArea.layout().addWidget(self.plot) self._update_normalize() def update_model(self): self.get_model() if self.auto_update and self.rpca and not self.rpca.ready(): self.__timer.start(2000) else: self.__timer.stop() def start(self): if 'Abort' in self.start_button.text(): self.rpca.abort() self.__timer.stop() self.start_button.setText("Start remote computation") else: self.address = self.addresstext.text() with remote.server(self.address): from Orange.projection.pca import RemotePCA maxiter = (1e5 + self.data.approx_len()) / self.batch_size * 3 self.rpca = RemotePCA(self.data, self.batch_size, int(maxiter)) self.update_model() self.start_button.setText("Abort remote computation") def set_data(self, data): self.clear_messages() self.clear() self.start_button.setEnabled(False) self.information() self.data = None if isinstance(data, SqlTable): if data.approx_len() < AUTO_DL_LIMIT: data = Table(data) elif not remotely: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) else: # data was big and remote available self.sampling_box.setVisible(True) self.start_button.setText("Start remote computation") self.start_button.setEnabled(True) if not isinstance(data, SqlTable): self.sampling_box.setVisible(False) if isinstance(data, Table): if data.is_sparse(): self.Error.sparse_data() self.clear_outputs() return if len(data.domain.attributes) == 0: self.Error.no_features() self.clear_outputs() return if len(data) == 0: self.Error.no_instances() self.clear_outputs() return self.data = data self.fit() def fit(self): self.clear() self.Warning.trivial_components.clear() if self.data is None: return data = self.data if not isinstance(data, SqlTable): pca = self._pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = numpy.cumsum(variance_ratio) if numpy.isfinite(cumulative[-1]): self.components_spin.setRange(0, len(cumulative)) self._pca = pca self._variance_ratio = variance_ratio self._cumulative = cumulative self._setup_plot() else: self.Warning.trivial_components() self.unconditional_commit() def clear(self): self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = None self.plot_horlabels = [] self.plot_horlines = [] self.plot.clear() def clear_outputs(self): self.send("Transformed data", None) self.send("Components", None) self.send("PCA", self._pca_projector) def get_model(self): if self.rpca is None: return if self.rpca.ready(): self.__timer.stop() self.start_button.setText("Restart (finished)") self._pca = self.rpca.get_state() if self._pca is None: return self._variance_ratio = self._pca.explained_variance_ratio_ self._cumulative = numpy.cumsum(self._variance_ratio) self._setup_plot() self._transformed = None self.commit() def _setup_plot(self): self.plot.clear() if self._pca is None: return explained_ratio = self._variance_ratio explained = self._cumulative p = min(len(self._variance_ratio), self.maxp) self.plot.plot(numpy.arange(p), explained_ratio[:p], pen=pg.mkPen(QColor(Qt.red), width=2), antialias=True, name="Variance") self.plot.plot(numpy.arange(p), explained[:p], pen=pg.mkPen(QColor(Qt.darkYellow), width=2), antialias=True, name="Cumulative Variance") cutpos = self._nselected_components() - 1 self._line = pg.InfiniteLine(angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) self._line.setCursor(Qt.SizeHorCursor) self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) self._line.sigPositionChanged.connect(self._on_cut_changed) self.plot.addItem(self._line) self.plot_horlines = ( pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) self.plot_horlabels = (pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) for item in self.plot_horlabels + self.plot_horlines: self.plot.addItem(item) self._set_horline_pos() self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) self._update_axis() def _set_horline_pos(self): cutidx = self.ncomponents - 1 for line, label, curve in zip( self.plot_horlines, self.plot_horlabels, (self._variance_ratio, self._cumulative)): y = curve[cutidx] line.setData([-1, cutidx], 2 * [y]) label.setPos(cutidx, y) label.setPlainText("{:.3f}".format(y)) def _on_cut_changed(self, line): # cut changed by means of a cut line over the scree plot. value = int(round(line.value())) self._line.setValue(value) current = self._nselected_components() components = value + 1 if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components self._set_horline_pos() if self._pca is not None: var = self._cumulative[components - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if current != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): # cut changed by "ncomponents" spin. if self._pca is None: self._invalidate_selection() return if self.ncomponents == 0: # Special "All" value cut = len(self._variance_ratio) else: cut = self.ncomponents var = self._cumulative[cut - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_selection_variance_spin(self): # cut changed by "max variance" spin. if self._pca is None: return cut = numpy.searchsorted(self._cumulative, self.variance_covered / 100.0) + 1 cut = min(cut, len(self._cumulative)) self.ncomponents = cut if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_normalize(self): if self.normalize: pp = self._pca_preprocessors + [Normalize()] else: pp = self._pca_preprocessors self._pca_projector.preprocessors = pp self.fit() if self.data is None: self._invalidate_selection() def _nselected_components(self): """Return the number of selected components.""" if self._pca is None: return 0 if self.ncomponents == 0: # Special "All" value max_comp = len(self._variance_ratio) else: max_comp = self.ncomponents var_max = self._cumulative[max_comp - 1] if var_max != numpy.floor(self.variance_covered / 100.0): cut = max_comp assert numpy.isfinite(var_max) self.variance_covered = int(var_max * 100) else: self.ncomponents = cut = numpy.searchsorted( self._cumulative, self.variance_covered / 100.0) + 1 return cut def _invalidate_selection(self): self.commit() def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p - 1) // (self.axis_labels - 1), 1) axis.setTicks([[(i, str(i + 1)) for i in range(0, p, d)]]) def commit(self): transformed = components = None if self._pca is not None: if self._transformed is None: # Compute the full transform (MAX_COMPONENTS components) only once. self._transformed = self._pca(self.data) transformed = self._transformed domain = Domain(transformed.domain.attributes[:self.ncomponents], self.data.domain.class_vars, self.data.domain.metas) transformed = transformed.from_table(domain, transformed) dom = Domain(self._pca.orig_domain.attributes, metas=[StringVariable(name='component')]) metas = numpy.array( [['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T components = Table(dom, self._pca.components_[:self.ncomponents], metas=metas) components.name = 'components' self._pca_projector.component = self.ncomponents self.send("Transformed data", transformed) self.send("Components", components) self.send("PCA", self._pca_projector) def send_report(self): if self.data is None: return self.report_items( (("Selected components", self.ncomponents), ("Explained variance", "{:.3f} %".format(self.variance_covered)))) self.report_plot() @classmethod def migrate_settings(cls, settings, version): if "variance_covered" in settings: # Due to the error in gh-1896 the variance_covered was persisted # as a NaN value, causing a TypeError in the widgets `__init__`. vc = settings["variance_covered"] if isinstance(vc, numbers.Real): if numpy.isfinite(vc): vc = int(vc) else: vc = 100 settings["variance_covered"] = vc if settings["ncomponents"] > MAX_COMPONENTS: settings["ncomponents"] = MAX_COMPONENTS
class OWtSNE(OWDataProjectionWidget): name = "t-SNE" description = "Two-dimensional data projection with t-SNE." icon = "icons/TSNE.svg" priority = 920 keywords = ["tsne"] settings_version = 3 max_iter = Setting(300) perplexity = Setting(30) multiscale = Setting(False) exaggeration = Setting(1) pca_components = Setting(20) normalize = Setting(True) GRAPH_CLASS = OWtSNEGraph graph = SettingProvider(OWtSNEGraph) embedding_variables_names = ("t-SNE-x", "t-SNE-y") #: Runtime state Running, Finished, Waiting, Paused = 1, 2, 3, 4 class Error(OWDataProjectionWidget.Error): not_enough_rows = Msg("Input data needs at least 2 rows") constant_data = Msg("Input data is constant") no_attributes = Msg("Data has no attributes") out_of_memory = Msg("Out of memory") optimization_error = Msg("Error during optimization\n{}") no_valid_data = Msg("No projection due to no valid data") def __init__(self): super().__init__() self.pca_data = None self.projection = None self.tsne_runner = None self.tsne_iterator = None self.__update_loop = None # timer for scheduling updates self.__timer = QTimer(self, singleShot=True, interval=1, timeout=self.__next_step) self.__state = OWtSNE.Waiting self.__in_next_step = False self.__draw_similar_pairs = False def reset_needs_to_draw(): self.needs_to_draw = True self.needs_to_draw = True self.__timer_draw = QTimer(self, interval=2000, timeout=reset_needs_to_draw) def _add_controls(self): self._add_controls_start_box() super()._add_controls() def _add_controls_start_box(self): box = gui.vBox(self.controlArea, True) form = QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10, ) self.perplexity_spin = gui.spin( box, self, "perplexity", 1, 500, step=1, alignment=Qt.AlignRight, callback=self._params_changed ) form.addRow("Perplexity:", self.perplexity_spin) self.perplexity_spin.setEnabled(not self.multiscale) form.addRow(gui.checkBox( box, self, "multiscale", label="Preserve global structure", callback=self._multiscale_changed )) sbe = gui.hBox(self.controlArea, False, addToLayout=False) gui.hSlider( sbe, self, "exaggeration", minValue=1, maxValue=4, step=1, callback=self._params_changed ) form.addRow("Exaggeration:", sbe) sbp = gui.hBox(self.controlArea, False, addToLayout=False) gui.hSlider( sbp, self, "pca_components", minValue=2, maxValue=50, step=1, callback=self._invalidate_pca_projection ) form.addRow("PCA components:", sbp) self.normalize_cbx = gui.checkBox( box, self, "normalize", "Normalize data", callback=self._invalidate_pca_projection, ) form.addRow(self.normalize_cbx) box.layout().addLayout(form) gui.separator(box, 10) self.runbutton = gui.button(box, self, "Run", callback=self._toggle_run) def _invalidate_pca_projection(self): self.pca_data = None self._params_changed() def _params_changed(self): self.__state = OWtSNE.Finished self.__set_update_loop(None) def _multiscale_changed(self): self.perplexity_spin.setEnabled(not self.multiscale) self._params_changed() def check_data(self): def error(err): err() self.data = None super().check_data() if self.data is not None: if len(self.data) < 2: error(self.Error.not_enough_rows) elif not self.data.domain.attributes: error(self.Error.no_attributes) elif not self.data.is_sparse(): if np.all(~np.isfinite(self.data.X)): error(self.Error.no_valid_data) else: with warnings.catch_warnings(): warnings.filterwarnings( "ignore", "Degrees of freedom .*", RuntimeWarning) if np.nan_to_num(np.nanstd(self.data.X, axis=0)).sum() \ == 0: error(self.Error.constant_data) def get_embedding(self): if self.data is None: self.valid_data = None return None elif self.projection is None: embedding = np.random.normal(size=(len(self.data), 2)) else: embedding = self.projection.embedding.X self.valid_data = np.ones(len(embedding), dtype=bool) return embedding def _toggle_run(self): if self.__state == OWtSNE.Running: self.stop() self.commit() elif self.__state == OWtSNE.Paused: self.resume() else: self.start() def start(self): if not self.data or self.__state == OWtSNE.Running: self.graph.update_coordinates() elif self.__state in (OWtSNE.Finished, OWtSNE.Waiting): self.__start() def stop(self): self.__state = OWtSNE.Paused self.__set_update_loop(None) def resume(self): self.__set_update_loop(self.tsne_iterator) def set_data(self, data: Table): super().set_data(data) if data is not None: # PCA doesn't support normalization on sparse data, as this would # require centering and normalizing the matrix self.normalize_cbx.setDisabled(data.is_sparse()) if data.is_sparse(): self.normalize = False self.normalize_cbx.setToolTip( "Data normalization is not supported on sparse matrices." ) else: self.normalize_cbx.setToolTip("") def pca_preprocessing(self): """Perform PCA preprocessing before passing off the data to t-SNE.""" if self.pca_data is not None: return projector = PCA(n_components=self.pca_components, random_state=0) # If the normalization box is ticked, we'll add the `Normalize` # preprocessor to PCA if self.normalize: projector.preprocessors += (preprocess.Normalize(),) model = projector(self.data) self.pca_data = model(self.data) def __start(self): self.pca_preprocessing() self.needs_to_draw = True # We call PCA through fastTSNE because it involves scaling. Instead of # worrying about this ourselves, we'll let the library worry for us. initialization = TSNE.default_initialization( self.pca_data.X, n_components=2, random_state=0) # Compute perplexity settings for multiscale n_samples = self.pca_data.X.shape[0] if self.multiscale: perplexity = min((n_samples - 1) / 3, 50), min((n_samples - 1) / 3, 500) else: perplexity = self.perplexity # Determine whether to use settings for large data sets if n_samples > 10_000: neighbor_method, gradient_method = "approx", "fft" else: neighbor_method, gradient_method = "exact", "bh" # Set number of iterations to 0 - these will be run subsequently self.projection = TSNE( n_components=2, perplexity=perplexity, multiscale=self.multiscale, early_exaggeration_iter=0, n_iter=0, initialization=initialization, exaggeration=self.exaggeration, neighbors=neighbor_method, negative_gradient_method=gradient_method, random_state=0, theta=0.8, )(self.pca_data) self.tsne_runner = TSNERunner( self.projection, step_size=20, exaggeration=self.exaggeration ) self.tsne_iterator = self.tsne_runner.run_optimization() self.__set_update_loop(self.tsne_iterator) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): if self.__update_loop is not None: if self.__state in (OWtSNE.Finished, OWtSNE.Waiting): self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.setBlocking(True) self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWtSNE.Running self.__timer.start() self.__timer_draw.start() else: self.setBlocking(False) self.setStatusMessage("") if self.__state in (OWtSNE.Finished, OWtSNE.Waiting): self.runbutton.setText("Start") if self.__state == OWtSNE.Paused: self.runbutton.setText("Resume") self.__timer.stop() self.__timer_draw.stop() def __next_step(self): if self.__update_loop is None: return assert not self.__in_next_step self.__in_next_step = True loop = self.__update_loop self.Error.out_of_memory.clear() self.Error.optimization_error.clear() try: projection, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__state = OWtSNE.Finished self.__set_update_loop(None) self.unconditional_commit() except MemoryError: self.Error.out_of_memory() self.__state = OWtSNE.Finished self.__set_update_loop(None) except Exception as exc: self.Error.optimization_error(str(exc)) self.__state = OWtSNE.Finished self.__set_update_loop(None) else: self.progressBarSet(100.0 * progress, processEvents=None) self.projection = projection if progress == 1 or self.needs_to_draw: self.graph.update_coordinates() self.graph.update_density() self.needs_to_draw = False # schedule next update self.__timer.start() self.__in_next_step = False def setup_plot(self): super().setup_plot() self.start() def _get_projection_data(self): if self.data is None: return None data = self.data.transform( Domain(self.data.domain.attributes, self.data.domain.class_vars, self.data.domain.metas + self._get_projection_variables())) data.metas[:, -2:] = self.get_embedding() if self.projection is not None: data.domain = Domain( self.data.domain.attributes, self.data.domain.class_vars, self.data.domain.metas + self.projection.domain.attributes) return data def clear(self): super().clear() self.__state = OWtSNE.Waiting self.__set_update_loop(None) self.pca_data = None self.projection = None @classmethod def migrate_settings(cls, settings, version): if version < 3: if "selection_indices" in settings: settings["selection"] = settings["selection_indices"] @classmethod def migrate_context(cls, context, version): if version < 3: values = context.values values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"]
class OWScatterPlot(OWDataProjectionWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 keywords = [] class Inputs(OWDataProjectionWidget.Inputs): features = Input("Features", AttributeList) class Outputs(OWDataProjectionWidget.Outputs): features = Output("Features", AttributeList, dynamic=False) settings_version = 3 auto_sample = Setting(True) attr_x = ContextSetting(None) attr_y = ContextSetting(None) tooltip_shows_all = Setting(True) GRAPH_CLASS = OWScatterPlotGraph graph = SettingProvider(OWScatterPlotGraph) embedding_variables_names = None class Warning(OWDataProjectionWidget.Warning): missing_coords = Msg("Plot cannot be displayed because '{}' or '{}' " "is missing for all data points") class Information(OWDataProjectionWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") missing_coords = Msg( "Points with missing '{}' or '{}' are not displayed") def __init__(self): self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) super().__init__() # manually register Matplotlib file writers self.graph_writers = self.graph_writers.copy() for w in [MatplotlibFormat, MatplotlibPDFFormat]: for ext in w.EXTENSIONS: self.graph_writers[ext] = w def _add_controls(self): self._add_controls_axis() self._add_controls_sampling() super()._add_controls() self.graph.gui.add_widget(self.graph.gui.JitterNumericValues, self._effects_box) self.graph.gui.add_widgets([ self.graph.gui.ShowGridLines, self.graph.gui.ToolTipShowsAll, self.graph.gui.RegressionLine ], self._plot_box) def _add_controls_axis(self): common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str, contentsLength=14) box = gui.vBox(self.controlArea, True) dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.attr_changed, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.attr_changed, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) def _add_controls_sampling(self): self.sampling = gui.auto_commit(self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) def _vizrank_color_change(self): self.vizrank.initialize() is_enabled = self.data is not None and not self.data.is_sparse() and \ len(self.xy_model) > 2 and len(self.data[self.valid_data]) > 1 \ and np.all(np.nan_to_num(np.nanstd(self.data.X, 0)) != 0) self.vizrank_button.setEnabled( is_enabled and self.attr_color is not None and not np.isnan( self.data.get_column_view( self.attr_color)[0].astype(float)).all()) text = "Color variable has to be selected." \ if is_enabled and self.attr_color is None else "" self.vizrank_button.setToolTip(text) def set_data(self, data): if self.data and data and self.data.checksum() == data.checksum(): return super().set_data(data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.attr_label, str): self.attr_label = findvar(self.attr_label, self.graph.gui.label_model) if isinstance(self.attr_color, str): self.attr_color = findvar(self.attr_color, self.graph.gui.color_model) if isinstance(self.attr_shape, str): self.attr_shape = findvar(self.attr_shape, self.graph.gui.shape_model) if isinstance(self.attr_size, str): self.attr_size = findvar(self.attr_size, self.graph.gui.size_model) def check_data(self): self.clear_messages() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(self.data, SqlTable): if self.data.approx_len() < 4000: self.data = Table(self.data) else: self.Information.sampled_sql() self.sql_data = self.data data_sample = self.data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) self.data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if self.data is not None and (len(self.data) == 0 or len(self.data.domain) == 0): self.data = None def get_embedding(self): self.valid_data = None if self.data is None: return None x_data = self.get_column(self.attr_x, filter_valid=False) y_data = self.get_column(self.attr_y, filter_valid=False) if x_data is None or y_data is None: return None self.Warning.missing_coords.clear() self.Information.missing_coords.clear() self.valid_data = np.isfinite(x_data) & np.isfinite(y_data) if self.valid_data is not None and not np.all(self.valid_data): msg = self.Information if np.any(self.valid_data) else self.Warning msg.missing_coords(self.attr_x.name, self.attr_y.name) return np.vstack((x_data, y_data)).T # Tooltip def _point_tooltip(self, point_id, skip_attrs=()): point_data = self.data[point_id] xy_attrs = (self.attr_x, self.attr_y) text = "<br/>".join( escape('{} = {}'.format(var.name, point_data[var])) for var in xy_attrs) if self.tooltip_shows_all: others = super()._point_tooltip(point_id, skip_attrs=xy_attrs) if others: text = "<b>{}</b><br/><br/>{}".format(text, others) return text def can_draw_regresssion_line(self): return self.data is not None and\ self.data.domain is not None and \ self.attr_x.is_continuous and \ self.attr_y.is_continuous def add_data(self, time=0.4): if self.data and len(self.data) > 2000: self.__timer.stop() return data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.handleNewSignals() def init_attr_values(self): super().init_attr_values() data = self.data domain = data.domain if data and len(data) else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None super().set_subset_data(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): if self.attribute_selection_list and self.data is not None and \ self.data.domain is not None and \ all(attr in self.data.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None super().handleNewSignals() self._vizrank_color_change() self.cb_reg_line.setEnabled(self.can_draw_regresssion_line()) @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.attr_changed() def attr_changed(self): self.cb_reg_line.setEnabled(self.can_draw_regresssion_line()) self.setup_plot() self.commit() def setup_plot(self): super().setup_plot() for axis, var in (("bottom", self.attr_x), ("left", self.attr_y)): self.graph.set_axis_title(axis, var) if var and var.is_discrete: self.graph.set_axis_labels(axis, get_variable_values_sorted(var)) else: self.graph.set_axis_labels(axis, None) def colors_changed(self): super().colors_changed() self._vizrank_color_change() def commit(self): super().commit() self.send_features() def send_features(self): features = [attr for attr in [self.attr_x, self.attr_y] if attr] self.Outputs.features.send(features or None) def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) return None def _get_send_report_caption(self): return report.render_items_vert( (("Color", self._get_caption_var_name(self.attr_color)), ("Label", self._get_caption_var_name(self.attr_label)), ("Shape", self._get_caption_var_name(self.attr_shape)), ("Size", self._get_caption_var_name(self.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]] if version < 3: if "auto_send_selection" in settings: settings["auto_commit"] = settings["auto_send_selection"] if "selection_group" in settings: settings["selection"] = settings["selection_group"] @classmethod def migrate_context(cls, context, version): if version < 3: values = context.values values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"]
class WidgetManager(QObject): """ WidgetManager class is responsible for creation, tracking and deletion of UI elements constituting an interactive workflow. It does so by reacting to changes in the underlying workflow model, creating and destroying the components when needed. This is an abstract class, subclassed MUST reimplement at least :func:`create_widget_for_node` and :func:`delete_widget_for_node`. The widgets created with :func:`create_widget_for_node` will automatically receive dispatched events: * :attr:`.WorkflowEvent.InputLinkAdded` - when a new input link is added to the workflow. * :attr:`.WorkflowEvent.InputLinkRemoved` - when a input link is removed. * :attr:`.WorkflowEvent.OutputLinkAdded` - when a new output link is added to the workflow. * :attr:`.WorkflowEvent.OutputLinkRemoved` - when a output link is removed. * :attr:`.WorkflowEvent.InputLinkStateChange` - when the input link's runtime state changes. * :attr:`.WorkflowEvent.OutputLinkStateChange` - when the output link's runtime state changes. * :attr:`.WorkflowEvent.NodeStateChange` - when the node's runtime state changes. * :attr:`.WorkflowEvent.WorkflowEnvironmentChange` - when the workflow environment changes. .. seealso:: :func:`.Scheme.add_link()`, :func:`Scheme.remove_link`, :func:`.Scheme.runtime_env`, :class:`NodeEvent`, :class:`LinkEvent` """ #: A new QWidget was created and added by the manager. widget_for_node_added = Signal(SchemeNode, QWidget) #: A QWidget was removed, hidden and will be deleted when appropriate. widget_for_node_removed = Signal(SchemeNode, QWidget) __init_queue = None # type: Deque[SchemeNode] class CreationPolicy(enum.Enum): """ Widget Creation Policy. """ #: Widgets are scheduled to be created from the event loop, or when #: first accessed with `widget_for_node` Normal = "Normal" #: Widgets are created immediately when a node is added to the #: workflow model. Immediate = "Immediate" #: Widgets are created only when first accessed with `widget_for_node` #: (e.g. when activated in the view). OnDemand = "OnDemand" Normal = CreationPolicy.Normal Immediate = CreationPolicy.Immediate OnDemand = CreationPolicy.OnDemand def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__workflow = None # type: Optional[Scheme] self.__creation_policy = WidgetManager.OnDemand self.__float_widgets_on_top = False self.__item_for_node = {} # type: Dict[SchemeNode, Item] self.__item_for_widget = {} # type: Dict[QWidget, Item] self.__init_queue = deque() self.__init_timer = QTimer(self, singleShot=True) self.__init_timer.timeout.connect(self.__process_init_queue) self.__activation_monitor = ActivationMonitor(self) self.__activation_counter = itertools.count() self.__activation_monitor.activated.connect(self.__mark_activated) def set_workflow(self, workflow): # type: (Scheme) -> None """ Set the workflow. """ if workflow is self.__workflow: return if self.__workflow is not None: # cleanup for node in self.__workflow.nodes: self.__remove_node(node) self.__workflow.node_added.disconnect(self.__on_node_added) self.__workflow.node_removed.disconnect(self.__on_node_removed) self.__workflow.removeEventFilter(self) self.__workflow = workflow workflow.node_added.connect(self.__on_node_added, Qt.UniqueConnection) workflow.node_removed.connect(self.__on_node_removed, Qt.UniqueConnection) workflow.installEventFilter(self) for node in workflow.nodes: self.__add_node(node) def workflow(self): # type: () -> Optional[Workflow] return self.__workflow scheme = workflow set_scheme = set_workflow def set_creation_policy(self, policy): # type: (CreationPolicy) -> None """ Set the widget creation policy. """ if self.__creation_policy != policy: self.__creation_policy = policy if self.__creation_policy == WidgetManager.Immediate: self.__init_timer.stop() # create all if self.__workflow is not None: for node in self.__workflow.nodes: self.ensure_created(node) elif self.__creation_policy == WidgetManager.Normal: if not self.__init_timer.isActive() and self.__init_queue: self.__init_timer.start() elif self.__creation_policy == WidgetManager.OnDemand: self.__init_timer.stop() else: assert False def creation_policy(self): """ Return the current widget creation policy. """ return self.__creation_policy def create_widget_for_node(self, node): # type: (SchemeNode) -> QWidget """ Create and initialize a widget for node. This is an abstract method. Subclasses must reimplemented it. """ raise NotImplementedError() def delete_widget_for_node(self, node, widget): # type: (SchemeNode, QWidget) -> None """ Remove and delete widget for node. This is an abstract method. Subclasses must reimplemented it. """ raise NotImplementedError() def node_for_widget(self, widget): # type: (QWidget) -> Optional[SchemeNode] """ Return the node for widget. """ item = self.__item_for_widget.get(widget) if item is not None: return item.node else: return None def widget_for_node(self, node): # type: (SchemeNode) -> Optional[QWidget] """ Return the widget for node. """ self.ensure_created(node) item = self.__item_for_node.get(node) return item.widget if item is not None else None def __add_widget_for_node(self, node): # type: (SchemeNode) -> None item = self.__item_for_node.get(node) if item is not None: return if self.__workflow is None: return if node not in self.__workflow.nodes: return if node in self.__init_queue: self.__init_queue.remove(node) item = Item(node, None, -1) # Insert on the node -> item mapping. self.__item_for_node[node] = item log.debug("Creating widget for node %s", node) try: w = self.create_widget_for_node(node) except Exception: # pylint: disable=broad-except log.critical("", exc_info=True) lines = traceback.format_exception(*sys.exc_info()) text = "".join(lines) errorwidget = QLabel(textInteractionFlags=Qt.TextSelectableByMouse, wordWrap=True, objectName="widgetmanager-error-placeholder", text="<pre>" + escape(text) + "</pre>") item.errorwidget = errorwidget node.set_state_message(UserMessage(text, UserMessage.Error, "")) raise else: item.widget = w self.__item_for_widget[w] = item self.__set_float_on_top_flag(w) if w.windowIcon().isNull(): desc = node.description w.setWindowIcon(icon_loader.from_description(desc).get(desc.icon)) if not w.windowTitle(): w.setWindowTitle(node.title) w.installEventFilter(self.__activation_monitor) raise_canvas = QAction( self.tr("Raise Canvas to Front"), w, objectName="action-canvas-raise-canvas", toolTip=self.tr("Raise containing canvas workflow window"), shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Up)) raise_canvas.triggered.connect(self.__on_activate_parent) raise_descendants = QAction( self.tr("Raise Descendants"), w, objectName="action-canvas-raise-descendants", toolTip=self.tr("Raise all immediate descendants of this node"), shortcut=QKeySequence(Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_Right)) raise_descendants.triggered.connect( partial(self.__on_raise_descendants, node)) raise_ancestors = QAction( self.tr("Raise Ancestors"), w, objectName="action-canvas-raise-ancestors", toolTip=self.tr("Raise all immediate ancestors of this node"), shortcut=QKeySequence(Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_Left)) raise_ancestors.triggered.connect( partial(self.__on_raise_ancestors, node)) w.addActions([raise_canvas, raise_descendants, raise_ancestors]) # send all the post creation notification events workflow = self.__workflow assert workflow is not None inputs = workflow.find_links(sink_node=node) for i, link in enumerate(inputs): ev = LinkEvent(LinkEvent.InputLinkAdded, link, i) QCoreApplication.sendEvent(w, ev) outputs = workflow.find_links(source_node=node) for i, link in enumerate(outputs): ev = LinkEvent(LinkEvent.OutputLinkAdded, link, i) QCoreApplication.sendEvent(w, ev) self.widget_for_node_added.emit(node, w) def ensure_created(self, node): # type: (SchemeNode) -> None """ Ensure that the widget for node is created. """ if self.__workflow is None: return if node not in self.__workflow.nodes: return item = self.__item_for_node.get(node) if item is None: self.__add_widget_for_node(node) def __on_node_added(self, node): # type: (SchemeNode) -> None assert self.__workflow is not None assert node in self.__workflow.nodes assert node not in self.__item_for_node self.__add_node(node) def __add_node(self, node): # type: (SchemeNode) -> None # add node for tracking node.installEventFilter(self) if self.__creation_policy == WidgetManager.Immediate: self.ensure_created(node) else: self.__init_queue.append(node) if self.__creation_policy == WidgetManager.Normal: self.__init_timer.start() def __on_node_removed(self, node): # type: (SchemeNode) -> None assert self.__workflow is not None assert node not in self.__workflow.nodes self.__remove_node(node) def __remove_node(self, node): # type: (SchemeNode) -> None # remove the node and its widget from tracking. node.removeEventFilter(self) if node in self.__init_queue: self.__init_queue.remove(node) item = self.__item_for_node.get(node) if item is not None and item.widget is not None: widget = item.widget assert widget in self.__item_for_widget del self.__item_for_widget[widget] widget.removeEventFilter(self.__activation_monitor) item.widget = None self.widget_for_node_removed.emit(node, widget) self.delete_widget_for_node(node, widget) if item is not None: del self.__item_for_node[node] @Slot() def __process_init_queue(self): if self.__init_queue: node = self.__init_queue.popleft() assert self.__workflow is not None assert node in self.__workflow.nodes log.debug("__process_init_queue: '%s'", node.title) try: self.ensure_created(node) finally: if self.__init_queue: self.__init_timer.start() def __mark_activated(self, widget): # type: (QWidget) -> None # Update the tracked stacking order for `widget` item = self.__item_for_widget.get(widget) if item is not None: item.activation_order = next(self.__activation_counter) def activate_widget_for_node(self, node, widget): # type: (SchemeNode, QWidget) -> None """ Activate the widget for node (show and raise above other) """ if widget.windowState() == Qt.WindowMinimized: widget.showNormal() widget.setVisible(True) widget.raise_() widget.activateWindow() def activate_window_group(self, group): # type: (Scheme.WindowGroup) -> None self.restore_window_state(group.state) def raise_widgets_to_front(self): """ Raise all current visible widgets to the front. The widgets will be stacked by activation order. """ workflow = self.__workflow if workflow is None: return items = filter( lambda item: (item.widget.isVisible() if item is not None and item.widget is not None else False), map(self.__item_for_node.get, workflow.nodes)) self.__raise_and_activate(items) def set_float_widgets_on_top(self, float_on_top): """ Set `Float Widgets on Top` flag on all widgets. """ self.__float_widgets_on_top = float_on_top for item in self.__item_for_node.values(): if item.widget is not None: self.__set_float_on_top_flag(item.widget) def save_window_state(self): # type: () -> List[Tuple[SchemeNode, bytes]] """ Save current open window arrangement. """ if self.__workflow is None: return [] workflow = self.__workflow # type: Scheme state = [] for node in workflow.nodes: # type: SchemeNode item = self.__item_for_node.get(node, None) if item is None: continue stackorder = item.activation_order if item.widget is not None and not item.widget.isHidden(): data = self.save_widget_geometry(node, item.widget) state.append((stackorder, node, data)) return [(node, data) for _, node, data in sorted(state, key=lambda t: t[0])] def restore_window_state(self, state): # type: (List[Tuple[Node, bytes]]) -> None """ Restore the window state. """ assert self.__workflow is not None workflow = self.__workflow # type: Scheme visible = {node for node, _ in state} # first hide all other widgets for node in workflow.nodes: if node not in visible: # avoid creating widgets if not needed item = self.__item_for_node.get(node, None) if item is not None and item.widget is not None: item.widget.hide() allnodes = set(workflow.nodes) # restore state for visible group; windows are stacked as they appear # in the state list. w = None for node, node_state in filter(lambda t: t[0] in allnodes, state): w = self.widget_for_node(node) # also create it if needed if w is not None: w.show() self.restore_widget_geometry(node, w, node_state) w.raise_() self.__mark_activated(w) # activate (give focus to) the last window if w is not None: w.activateWindow() def save_widget_geometry(self, node, widget): # type: (SchemeNode, QWidget) -> bytes """ Save and return the current geometry and state for node. """ return b'' def restore_widget_geometry(self, node, widget, state): # type: (SchemeNode, QWidget, bytes) -> bool """ Restore the widget geometry and state for node. Return True if the geometry was restored successfully. The default implementation does nothing. """ return False @Slot(SchemeNode) def __on_raise_ancestors(self, node): # type: (SchemeNode) -> None """ Raise all the ancestor widgets of `widget`. """ item = self.__item_for_node.get(node) if item is not None: scheme = self.scheme() assert scheme is not None ancestors = [ self.__item_for_node.get(p) for p in scheme.parents(item.node) ] self.__raise_and_activate(filter(None, reversed(ancestors))) @Slot(SchemeNode) def __on_raise_descendants(self, node): # type: (SchemeNode) -> None """ Raise all the descendants widgets of `widget`. """ item = self.__item_for_node.get(node) if item is not None: scheme = self.scheme() assert scheme is not None descendants = [ self.__item_for_node.get(p) for p in scheme.children(item.node) ] self.__raise_and_activate(filter(None, reversed(descendants))) def __raise_and_activate(self, items): # type: (Iterable[Item]) -> None """Show and raise a set of widgets.""" # preserve the tracked stacking order items = sorted(items, key=lambda item: item.activation_order) w = None for item in items: if item.widget is not None: w = item.widget elif item.errorwidget is not None: w = item.errorwidget else: continue w.show() w.raise_() if w is not None: # give focus to the last activated top window w.activateWindow() def __activate_widget_for_node(self, node): # type: (SchemeNode) -> None # activate the widget for the node. self.ensure_created(node) item = self.__item_for_node.get(node) if item is None: return if item.widget is not None: self.activate_widget_for_node(node, item.widget) elif item.errorwidget is not None: item.errorwidget.show() item.errorwidget.raise_() item.errorwidget.activateWindow() def __on_activate_parent(self): event = WorkflowEvent(WorkflowEvent.ActivateParentRequest) QCoreApplication.sendEvent(self.scheme(), event) def eventFilter(self, recv, event): # type: (QObject, QEvent) -> bool if isinstance(recv, SchemeNode): if event.type() == NodeEvent.NodeActivateRequest: self.__activate_widget_for_node(recv) self.__dispatch_events(recv, event) elif event.type() == WorkflowEvent.WorkflowEnvironmentChange \ and recv is self.__workflow: for node in self.__item_for_node: self.__dispatch_events(node, event) return False def __dispatch_events(self, node: Node, event: QEvent) -> None: """ Dispatch relevant workflow events to the GUI widget """ if event.type() in ( WorkflowEvent.InputLinkAdded, WorkflowEvent.InputLinkRemoved, WorkflowEvent.InputLinkStateChange, WorkflowEvent.OutputLinkAdded, WorkflowEvent.OutputLinkRemoved, WorkflowEvent.OutputLinkStateChange, WorkflowEvent.NodeStateChange, WorkflowEvent.WorkflowEnvironmentChange, ): item = self.__item_for_node.get(node) if item is not None and item.widget is not None: QCoreApplication.sendEvent(item.widget, event) def __set_float_on_top_flag(self, widget): # type: (QWidget) -> None """Set or unset widget's float on top flag""" should_float_on_top = self.__float_widgets_on_top float_on_top = bool(widget.windowFlags() & Qt.WindowStaysOnTopHint) if float_on_top == should_float_on_top: return widget_was_visible = widget.isVisible() if should_float_on_top: widget.setWindowFlags(widget.windowFlags() | Qt.WindowStaysOnTopHint) else: widget.setWindowFlags(widget.windowFlags() & ~Qt.WindowStaysOnTopHint) # Changing window flags hid the widget if widget_was_visible: widget.show() def actions_for_context_menu(self, node): # type: (SchemeNode) -> List[QAction] """ Return a list of extra actions that can be inserted into context menu in the workflow editor. Subclasses can reimplement this method to extend the default context menu. Parameters ---------- node: SchemeNode The node for which the context menu is requested. Return ------ actions: List[QAction] Actions that are appended to the default menu. """ return []
class OWLouvainClustering(widget.OWWidget): name = "Louvain Clustering" description = "Detects communities in a network of nearest neighbors." icon = "icons/LouvainClustering.svg" priority = 2110 want_main_area = False settingsHandler = DomainContextHandler() class Inputs: data = Input("Data", Table, default=True) if Graph is not None: class Outputs: annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, default=True) graph = Output("Network", Graph) else: class Outputs: annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, default=True) apply_pca = ContextSetting(True) pca_components = ContextSetting(_DEFAULT_PCA_COMPONENTS) normalize = ContextSetting(True) metric_idx = ContextSetting(0) k_neighbors = ContextSetting(_DEFAULT_K_NEIGHBORS) resolution = ContextSetting(1.) auto_commit = Setting(False) class Information(widget.OWWidget.Information): modified = Msg("Press commit to recompute clusters and send new data") class Error(widget.OWWidget.Error): empty_dataset = Msg("No features in data") def __init__(self): super().__init__() self.data = None # type: Optional[Table] self.preprocessed_data = None # type: Optional[Table] self.pca_projection = None # type: Optional[Table] self.graph = None # type: Optional[nx.Graph] self.partition = None # type: Optional[np.array] # Use a executor with a single worker, to limit CPU overcommitment for # cancelled tasks. The method does not have a fine cancellation # granularity so we assure that there are not N - 1 jobs executing # for no reason only to be thrown away. It would be better to use the # global pool but implement a limit on jobs from this source. self.__executor = futures.ThreadPoolExecutor(max_workers=1) self.__task = None # type: Optional[TaskState] self.__invalidated = False # coalescing commit timer self.__commit_timer = QTimer(self, singleShot=True) self.__commit_timer.timeout.connect(self.commit) # Set up UI info_box = gui.vBox(self.controlArea, "Info") self.info_label = gui.widgetLabel(info_box, "No data on input.") # type: QLabel preprocessing_box = gui.vBox(self.controlArea, "Preprocessing") self.normalize_cbx = gui.checkBox( preprocessing_box, self, "normalize", label="Normalize data", callback=self._invalidate_preprocessed_data, ) # type: QCheckBox self.apply_pca_cbx = gui.checkBox( preprocessing_box, self, "apply_pca", label="Apply PCA preprocessing", callback=self._apply_pca_changed, ) # type: QCheckBox self.pca_components_slider = gui.hSlider( preprocessing_box, self, "pca_components", label="PCA Components: ", minValue=2, maxValue=_MAX_PCA_COMPONENTS, callback=self._invalidate_pca_projection, tracking=False) # type: QSlider graph_box = gui.vBox(self.controlArea, "Graph parameters") self.metric_combo = gui.comboBox( graph_box, self, "metric_idx", label="Distance metric", items=[m[0] for m in METRICS], callback=self._invalidate_graph, orientation=Qt.Horizontal, ) # type: gui.OrangeComboBox self.k_neighbors_spin = gui.spin( graph_box, self, "k_neighbors", minv=1, maxv=_MAX_K_NEIGBOURS, label="k neighbors", controlWidth=80, alignment=Qt.AlignRight, callback=self._invalidate_graph, ) # type: gui.SpinBoxWFocusOut self.resolution_spin = gui.hSlider( graph_box, self, "resolution", minValue=0, maxValue=5., step=1e-1, label="Resolution", intOnly=False, labelFormat="%.1f", callback=self._invalidate_partition, tracking=False, ) # type: QSlider self.resolution_spin.parent().setToolTip( "The resolution parameter affects the number of clusters to find. " "Smaller values tend to produce more clusters and larger values " "retrieve less clusters.") self.apply_button = gui.auto_commit( self.controlArea, self, "auto_commit", "Apply", box=None, commit=lambda: self.commit(), callback=lambda: self._on_auto_commit_changed(), ) # type: QWidget def _preprocess_data(self): if self.preprocessed_data is None: if self.normalize: normalizer = preprocess.Normalize(center=False) self.preprocessed_data = normalizer(self.data) else: self.preprocessed_data = self.data def _apply_pca_changed(self): self.controls.pca_components.setEnabled(self.apply_pca) self._invalidate_graph() def _invalidate_preprocessed_data(self): self.preprocessed_data = None self._invalidate_pca_projection() # If we don't apply PCA, this still invalidates the graph, otherwise # this change won't be propagated further if not self.apply_pca: self._invalidate_graph() def _invalidate_pca_projection(self): self.pca_projection = None if not self.apply_pca: return self._invalidate_graph() self._set_modified(True) def _invalidate_graph(self): self.graph = None self._invalidate_partition() self._set_modified(True) def _invalidate_partition(self): self.partition = None self._invalidate_output() self.Information.modified() self._set_modified(True) def _invalidate_output(self): self.__invalidated = True if self.__task is not None: self.__cancel_task(wait=False) if self.auto_commit: self.__commit_timer.start() else: self.__set_state_ready() def _set_modified(self, state): """ Mark the widget (GUI) as containing modified state. """ if self.data is None: # does not apply when we have no data state = False elif self.auto_commit: # does not apply when auto commit is on state = False self.Information.modified(shown=state) def _on_auto_commit_changed(self): if self.auto_commit and self.__invalidated: self.commit() def cancel(self): """Cancel any running jobs.""" self.__cancel_task(wait=False) self.__set_state_ready() def commit(self): self.__commit_timer.stop() self.__invalidated = False self._set_modified(False) # Cancel current running task self.__cancel_task(wait=False) if self.data is None: self.__set_state_ready() return self.Error.clear() if self.partition is not None: self.__set_state_ready() self._send_data() return self._preprocess_data() state = TaskState(self) # Prepare/assemble the task(s) to run; reuse partial results if self.apply_pca: if self.pca_projection is not None: data = self.pca_projection pca_components = None else: data = self.preprocessed_data pca_components = self.pca_components else: data = self.preprocessed_data pca_components = None if self.graph is not None: # run on graph only; no need to do PCA and k-nn search ... graph = self.graph k_neighbors = metric = None else: k_neighbors, metric = self.k_neighbors, METRICS[self.metric_idx][1] graph = None if graph is None: task = partial( run_on_data, data, pca_components=pca_components, normalize=self.normalize, k_neighbors=k_neighbors, metric=metric, resolution=self.resolution, state=state, ) else: task = partial(run_on_graph, graph, resolution=self.resolution, state=state) self.info_label.setText("Running...") self.__set_state_busy() self.__start_task(task, state) @Slot(object) def __set_partial_results(self, result): # type: (Tuple[str, Any]) -> None which, res = result if which == "pca_projection": assert isinstance(res, Table) and len(res) == len(self.data) self.pca_projection = res elif which == "graph": assert isinstance(res, nx.Graph) self.graph = res elif which == "partition": assert isinstance(res, np.ndarray) self.partition = res else: assert False, which @Slot(object) def __on_done(self, future): # type: (Future["Results"]) -> None assert future.done() assert self.__task is not None assert self.__task.future is future assert self.__task.watcher.future() is future self.__task, task = None, self.__task task.deleteLater() self.__set_state_ready() result = future.result() self.__set_results(result) @Slot(str) def setStatusMessage(self, text): super().setStatusMessage(text) @Slot(float) def progressBarSet(self, value, *a, **kw): super().progressBarSet(value, *a, **kw) def __set_state_ready(self): self.progressBarFinished() self.setBlocking(False) self.setStatusMessage("") def __set_state_busy(self): self.progressBarInit() self.setBlocking(True) def __start_task(self, task, state): # type: (Callable[[], Any], TaskState) -> None assert self.__task is None state.status_changed.connect(self.setStatusMessage) state.progress_changed.connect(self.progressBarSet) state.partial_result_ready.connect(self.__set_partial_results) state.watcher.done.connect(self.__on_done) state.start(self.__executor, task) state.setParent(self) self.__task = state def __cancel_task(self, wait=True): # Cancel and dispose of the current task if self.__task is not None: state, self.__task = self.__task, None state.cancel() state.partial_result_ready.disconnect(self.__set_partial_results) state.status_changed.disconnect(self.setStatusMessage) state.progress_changed.disconnect(self.progressBarSet) state.watcher.done.disconnect(self.__on_done) if wait: futures.wait([state.future]) state.deleteLater() else: w = FutureWatcher(state.future, parent=state) w.done.connect(state.deleteLater) def __set_results(self, results): # type: ("Results") -> None # NOTE: All of these have already been set by __set_partial_results, # we double check that they are aliases if results.pca_projection is not None: assert self.pca_components == results.pca_components assert self.pca_projection is results.pca_projection self.pca_projection = results.pca_projection if results.graph is not None: assert results.metric == METRICS[self.metric_idx][1] assert results.k_neighbors == self.k_neighbors assert self.graph is results.graph self.graph = results.graph if results.partition is not None: assert results.resolution == self.resolution assert self.partition is results.partition self.partition = results.partition # Display the number of found clusters in the UI num_clusters = len(np.unique(self.partition)) self.info_label.setText("%d clusters found." % num_clusters) self._send_data() def _send_data(self): if self.partition is None or self.data is None: return domain = self.data.domain # Compute the frequency of each cluster index counts = np.bincount(self.partition) indices = np.argsort(counts)[::-1] index_map = {n: o for n, o in zip(indices, range(len(indices)))} new_partition = list(map(index_map.get, self.partition)) cluster_var = DiscreteVariable( get_unique_names(domain, "Cluster"), values=[ "C%d" % (i + 1) for i, _ in enumerate(np.unique(new_partition)) ]) new_domain = add_columns(domain, metas=[cluster_var]) new_table = self.data.transform(new_domain) new_table.get_column_view(cluster_var)[0][:] = new_partition self.Outputs.annotated_data.send(new_table) if Graph is not None: graph = Graph(self.graph) graph.set_items(new_table) self.Outputs.graph.send(graph) @Inputs.data def set_data(self, data): self.closeContext() self.Error.clear() prev_data, self.data = self.data, data self.openContext(self.data) # Make sure to properly enable/disable slider based on `apply_pca` setting self.controls.pca_components.setEnabled(self.apply_pca) if prev_data and self.data and ut.array_equal(prev_data.X, self.data.X): if self.auto_commit: self._send_data() return # Clear the outputs self.Outputs.annotated_data.send(None) if Graph is not None: self.Outputs.graph.send(None) # Clear internal state self.clear() self._invalidate_pca_projection() # Make sure the dataset is ok if self.data is not None and len(self.data.domain.attributes) < 1: self.Error.empty_dataset() self.data = None if self.data is None: return # Can't have more PCA components than the number of attributes n_attrs = len(data.domain.attributes) self.pca_components_slider.setMaximum(min(_MAX_PCA_COMPONENTS, n_attrs)) self.pca_components_slider.setValue( min(_DEFAULT_PCA_COMPONENTS, n_attrs)) # Can't have more k neighbors than there are data points self.k_neighbors_spin.setMaximum(min(_MAX_K_NEIGBOURS, len(data) - 1)) self.k_neighbors_spin.setValue(min(_DEFAULT_K_NEIGHBORS, len(data) - 1)) self.info_label.setText("Clustering not yet run.") self.commit() def clear(self): self.__cancel_task(wait=False) self.preprocessed_data = None self.pca_projection = None self.graph = None self.partition = None self.Error.clear() self.Information.modified.clear() self.info_label.setText("No data on input.") def onDeleteWidget(self): self.__cancel_task(wait=True) self.__executor.shutdown(True) self.clear() self.data = None super().onDeleteWidget() def send_report(self): pca = report.bool_str(self.apply_pca) if self.apply_pca: pca += report.plural(", {number} component{s}", self.pca_components) self.report_items(( ("Normalize data", report.bool_str(self.normalize)), ("PCA preprocessing", pca), ("Metric", METRICS[self.metric_idx][0]), ("k neighbors", self.k_neighbors), ("Resolution", self.resolution), ))
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 OWBouncingBalls(OWWidget, ConcurrentWidgetMixin): name = "BouncingBalls" icon = "icons/mywidget.svg" want_main_area = False class Inputs: bb = Input('BouncingBalls', list) def __init__(self): OWWidget.__init__(self) ConcurrentWidgetMixin.__init__(self) self.frame = None self.animation = BouncingBalls() self.timer = QTimer() self.timer.timeout.connect(self.pygame_loop) self.timer.start(10) self.image = None self.current_colors = None @Inputs.bb def on_input(self, colors): self.current_colors = colors (blue, blue_rad), (green, green_rad) = colors if blue: self.animation.blue.color = blue self.animation.blue.radius = int(100 * blue_rad) else: self.animation.blue = self.animation.create_new_ball() if green: self.animation.green.color = green self.animation.green.radius = int(100 * green_rad) else: self.animation.green = self.animation.create_new_ball() def pygame_loop(self): self.animation.loop() self.image = QImage(self.animation.surface.get_buffer().raw, self.animation.surface.get_width(), self.animation.surface.get_height(), QImage.Format_RGB32) # repaint Qt window self.update() def paintEvent(self, event): if self.image is not None: qp = QPainter() qp.begin(self) qp.drawImage(0, 0, self.image) qp.end() def onDeleteWidget(self): super().onDeleteWidget() def __set_state_ready(self): self.setBlocking(False) def __set_state_busy(self): self.setBlocking(True) def _connect_signals(self, state: TaskState): super()._connect_signals(state) def _disconnect_signals(self, state: TaskState): super()._disconnect_signals(state)
class OWMDS(OWWidget): name = "MDS" description = "Two-dimensional data projection by multidimensional " \ "scaling constructed from a distance matrix." icon = "icons/MDS.svg" class Inputs: data = Input("Data", Orange.data.Table, default=True) distances = Input("Distances", Orange.misc.DistMatrix) data_subset = Input("Data Subset", Orange.data.Table) class Outputs: selected_data = Output("Selected Data", Orange.data.Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table) settings_version = 2 #: Initialization type PCA, Random = 0, 1 #: Refresh rate RefreshRate = [("Every iteration", 1), ("Every 5 steps", 5), ("Every 10 steps", 10), ("Every 25 steps", 25), ("Every 50 steps", 50), ("None", -1)] #: Runtime state Running, Finished, Waiting = 1, 2, 3 settingsHandler = settings.DomainContextHandler() max_iter = settings.Setting(300) initialization = settings.Setting(PCA) refresh_rate = settings.Setting(3) # output embedding role. NoRole, AttrRole, AddAttrRole, MetaRole = 0, 1, 2, 3 auto_commit = settings.Setting(True) selection_indices = settings.Setting(None, schema_only=True) #: Percentage of all pairs displayed (ranges from 0 to 20) connected_pairs = settings.Setting(5) legend_anchor = settings.Setting(((1, 0), (1, 0))) graph = SettingProvider(OWMDSGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Error(OWWidget.Error): not_enough_rows = Msg("Input data needs at least 2 rows") matrix_too_small = Msg("Input matrix must be at least 2x2") no_attributes = Msg("Data has no attributes") mismatching_dimensions = \ Msg("Data and distances dimensions do not match.") out_of_memory = Msg("Out of memory") optimization_error = Msg("Error during optimization\n{}") def __init__(self): super().__init__() #: Input dissimilarity matrix self.matrix = None # type: Optional[Orange.misc.DistMatrix] #: Effective data used for plot styling/annotations. Can be from the #: input signal (`self.signal_data`) or the input matrix #: (`self.matrix.data`) self.data = None # type: Optional[Orange.data.Table] #: Input subset data table self.subset_data = None # type: Optional[Orange.data.Table] #: Data table from the `self.matrix.row_items` (if present) self.matrix_data = None # type: Optional[Orange.data.Table] #: Input data table self.signal_data = None self._similar_pairs = None self._subset_mask = None # type: Optional[np.ndarray] self._invalidated = False self.effective_matrix = None self._curve = None self._primitive_metas = () self._data_metas = None self.variable_x = ContinuousVariable("mds-x") self.variable_y = ContinuousVariable("mds-y") self.__update_loop = None # timer for scheduling updates self.__timer = QTimer(self, singleShot=True, interval=0) self.__timer.timeout.connect(self.__next_step) self.__state = OWMDS.Waiting self.__in_next_step = False self.__draw_similar_pairs = False box = gui.vBox(self.controlArea, "MDS Optimization") form = QFormLayout(labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 10, 10**4, step=1)) form.addRow( "Initialization:", gui.radioButtons(box, self, "initialization", btnLabels=("PCA (Torgerson)", "Random"), callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow( "Refresh:", gui.comboBox(box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) gui.separator(box, 10) self.runbutton = gui.button(box, self, "Run", callback=self._toggle_run) box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWMDSGraph(self, box, "MDSGraph", view_box=MDSInteractiveViewBox) box.layout().addWidget(self.graph.plot_widget) self.plot = self.graph.plot_widget g = self.graph.gui box = g.point_properties_box(self.controlArea) self.models = g.points_models gui.hSlider(box, self, "connected_pairs", label="Show similar pairs:", minValue=0, maxValue=20, createLabel=False, callback=self._on_connected_changed) g.add_widgets(ids=[g.JitterSizeSlider], widget=box) box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([ g.ShowLegend, g.ToolTipShowsAll, g.ClassDensity, g.LabelOnlySelected ], box) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict palette = self.graph.plot_widget.palette() self.graph.set_palette(palette) gui.rubber(self.controlArea) self.graph.box_zoom_select(self.controlArea) gui.auto_commit(box, self, "auto_commit", "Send Selected", checkbox_label="Send selected automatically", box=None) self.plot.getPlotItem().hideButtons() self.plot.setRenderHint(QPainter.Antialiasing) self.graph.jitter_continuous = True self._initialize() def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() self.connect_pairs() def update_colors(self): pass def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def init_attr_values(self): domain = self.data and len(self.data) and self.data.domain or None for model in self.models: model.set_domain(domain) self.graph.attr_color = self.data.domain.class_var if domain else None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None self.models[2][:] = self.models[2][0:1] + ["Stress" ] + self.models[2][1:] def prepare_data(self): pass def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.variable_x, self.variable_y, True) def selection_changed(self): self.commit() @Inputs.data @check_sql_input def set_data(self, data): """Set the input data set. Parameters ---------- data : Optional[Orange.data.Table] """ if data is not None and len(data) < 2: self.Error.not_enough_rows() data = None else: self.Error.not_enough_rows.clear() self.signal_data = data if self.matrix is not None and data is not None and len( self.matrix) == len(data): self.closeContext() self.data = data self.openContext(data) else: self._invalidated = True if data is not None: self._primitive_metas = tuple(a for a in data.domain.metas if a.is_primitive()) keys = [ k for k, a in enumerate(data.domain.metas) if a.is_primitive() ] self._data_metas = data.metas[:, keys] else: self._primitive_metas = () self._data_metas = None @Inputs.distances def set_disimilarity(self, matrix): """Set the dissimilarity (distance) matrix. Parameters ---------- matrix : Optional[Orange.misc.DistMatrix] """ if matrix is not None and len(matrix) < 2: self.Error.matrix_too_small() matrix = None else: self.Error.matrix_too_small.clear() self.matrix = matrix if matrix is not None and matrix.row_items: self.matrix_data = matrix.row_items if matrix is None: self.matrix_data = None self._invalidated = True @Inputs.data_subset def set_subset_data(self, subset_data): """Set a subset of `data` input to highlight in the plot. Parameters ---------- subset_data: Optional[Orange.data.Table] """ self.subset_data = subset_data # invalidate the pen/brush when the subset is changed self._subset_mask = None # type: Optional[np.ndarray] self.controls.graph.alpha_value.setEnabled(subset_data is None) self._invalidated = True def _clear(self): self._similar_pairs = None self.__set_update_loop(None) self.__state = OWMDS.Waiting def _clear_plot(self): self.graph.plot_widget.clear() def _initialize(self): # clear everything self.closeContext() self._clear() self.Error.clear() self.data = None self.effective_matrix = None self.embedding = None self.init_attr_values() # if no data nor matrix is present reset plot if self.signal_data is None and self.matrix is None: return if self.signal_data is not None and self.matrix is not None and \ len(self.signal_data) != len(self.matrix): self.Error.mismatching_dimensions() self._update_plot() return if self.signal_data is not None: self.data = self.signal_data elif self.matrix_data is not None: self.data = self.matrix_data if self.matrix is not None: self.effective_matrix = self.matrix if self.matrix.axis == 0 and self.data is self.matrix_data: self.data = None elif self.data.domain.attributes: preprocessed_data = Orange.projection.MDS().preprocess(self.data) self.effective_matrix = Orange.distance.Euclidean( preprocessed_data) else: self.Error.no_attributes() return self.init_attr_values() self.openContext(self.data) def _toggle_run(self): if self.__state == OWMDS.Running: self.stop() self._invalidate_output() else: self.start() def start(self): if self.__state == OWMDS.Running: return elif self.__state == OWMDS.Finished: # Resume/continue from a previous run self.__start() elif self.__state == OWMDS.Waiting and \ self.effective_matrix is not None: self.__start() def stop(self): if self.__state == OWMDS.Running: self.__set_update_loop(None) def __start(self): self.__draw_similar_pairs = False X = self.effective_matrix init = self.embedding # number of iterations per single GUI update step _, step_size = OWMDS.RefreshRate[self.refresh_rate] if step_size == -1: step_size = self.max_iter def update_loop(X, max_iter, step, init): """ return an iterator over successive improved MDS point embeddings. """ # NOTE: this code MUST NOT call into QApplication.processEvents done = False iterations_done = 0 oldstress = np.finfo(np.float).max init_type = "PCA" if self.initialization == OWMDS.PCA else "random" while not done: step_iter = min(max_iter - iterations_done, step) mds = Orange.projection.MDS(dissimilarity="precomputed", n_components=2, n_init=1, max_iter=step_iter, init_type=init_type, init_data=init) mdsfit = mds(X) iterations_done += step_iter embedding, stress = mdsfit.embedding_, mdsfit.stress_ stress /= np.sqrt(np.sum(embedding**2, axis=1)).sum() if iterations_done >= max_iter: done = True elif (oldstress - stress) < mds.params["eps"]: done = True init = embedding oldstress = stress yield embedding, mdsfit.stress_, iterations_done / max_iter self.__set_update_loop(update_loop(X, self.max_iter, step_size, init)) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): """ Set the update `loop` coroutine. The `loop` is a generator yielding `(embedding, stress, progress)` tuples where `embedding` is a `(N, 2) ndarray` of current updated MDS points, `stress` is the current stress and `progress` a float ratio (0 <= progress <= 1) If an existing update coroutine loop is already in palace it is interrupted (i.e. closed). .. note:: The `loop` must not explicitly yield control flow to the event loop (i.e. call `QApplication.processEvents`) """ if self.__update_loop is not None: self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.setBlocking(True) self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWMDS.Running self.__timer.start() else: self.setBlocking(False) self.setStatusMessage("") self.runbutton.setText("Start") self.__state = OWMDS.Finished self.__timer.stop() def __next_step(self): if self.__update_loop is None: return assert not self.__in_next_step self.__in_next_step = True loop = self.__update_loop self.Error.out_of_memory.clear() try: embedding, _, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__set_update_loop(None) self.unconditional_commit() self.__draw_similar_pairs = True self._update_plot() except MemoryError: self.Error.out_of_memory() self.__set_update_loop(None) self.__draw_similar_pairs = True except Exception as exc: self.Error.optimization_error(str(exc)) self.__set_update_loop(None) self.__draw_similar_pairs = True else: self.progressBarSet(100.0 * progress, processEvents=None) self.embedding = embedding self._update_plot() # schedule next update self.__timer.start() self.__in_next_step = False def __invalidate_embedding(self): # reset/invalidate the MDS embedding, to the default initialization # (Random or PCA), restarting the optimization if necessary. if self.embedding is None: return state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) X = self.effective_matrix if self.initialization == OWMDS.PCA: self.embedding = torgerson(X) else: self.embedding = np.random.rand(len(X), 2) self._update_plot() # restart the optimization if it was interrupted. if state == OWMDS.Running: self.__start() def __invalidate_refresh(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) # restart the optimization if it was interrupted. # TODO: decrease the max iteration count by the already # completed iterations count. if state == OWMDS.Running: self.__start() def handleNewSignals(self): if self._invalidated: self._invalidated = False self._initialize() self.start() self.__draw_similar_pairs = False if self._subset_mask is None and self.subset_data is not None and \ self.data is not None: self._subset_mask = np.in1d(self.data.ids, self.subset_data.ids) self._update_plot(new=True) self.unconditional_commit() def _invalidate_output(self): self.commit() def _on_connected_changed(self): self._similar_pairs = None self._update_plot() def _update_plot(self, new=False): self._clear_plot() if self.embedding is not None: self._setup_plot(new=new) else: self.graph.new_data(None) def connect_pairs(self): if not (self.connected_pairs and self.__draw_similar_pairs): return emb_x, emb_y = self.graph.get_xy_data_positions( self.variable_x, self.variable_y, self.graph.valid_data) if self._similar_pairs is None: # This code requires storing lower triangle of X (n x n / 2 # doubles), n x n / 2 * 2 indices to X, n x n / 2 indices for # argsort result. If this becomes an issue, it can be reduced to # n x n argsort indices by argsorting the entire X. Then we # take the first n + 2 * p indices. We compute their coordinates # i, j in the original matrix. We keep those for which i < j. # n + 2 * p will suffice to exclude the diagonal (i = j). If the # number of those for which i < j is smaller than p, we instead # take i > j. Among those that remain, we take the first p. # Assuming that MDS can't show so many points that memory could # become an issue, I preferred using simpler code. m = self.effective_matrix n = len(m) p = min(n * (n - 1) // 2 * self.connected_pairs // 100, MAX_N_PAIRS * self.connected_pairs // 20) indcs = np.triu_indices(n, 1) sorted = np.argsort(m[indcs])[:p] self._similar_pairs = fpairs = np.empty(2 * p, dtype=int) fpairs[::2] = indcs[0][sorted] fpairs[1::2] = indcs[1][sorted] emb_x_pairs = emb_x[self._similar_pairs].reshape((-1, 2)) emb_y_pairs = emb_y[self._similar_pairs].reshape((-1, 2)) # Filter out zero distance lines (in embedding coords). # Null (zero length) line causes bad rendering artifacts # in Qt when using the raster graphics system (see gh-issue: 1668). (x1, x2), (y1, y2) = (emb_x_pairs.T, emb_y_pairs.T) pairs_mask = ~(np.isclose(x1, x2) & np.isclose(y1, y2)) emb_x_pairs = emb_x_pairs[pairs_mask, :] emb_y_pairs = emb_y_pairs[pairs_mask, :] if self._curve: self.graph.plot_widget.removeItem(self._curve) self._curve = pg.PlotCurveItem(emb_x_pairs.ravel(), emb_y_pairs.ravel(), pen=pg.mkPen(0.8, width=2, cosmetic=True), connect="pairs", antialias=True) self.graph.plot_widget.addItem(self._curve) def _setup_plot(self, new=False): emb_x, emb_y = self.embedding[:, 0], self.embedding[:, 1] coords = np.vstack((emb_x, emb_y)).T attributes = self.data.domain.attributes + (self.variable_x, self.variable_y) + \ self._primitive_metas domain = Domain(attributes=attributes, class_vars=self.data.domain.class_vars) if self._data_metas is not None: data_x = (self.data.X, coords, self._data_metas) else: data_x = (self.data.X, coords) data = Table.from_numpy(domain, X=np.hstack(data_x), Y=self.data.Y) subset_data = data[ self._subset_mask] if self._subset_mask is not None else None self.graph.new_data(data, subset_data=subset_data, new=new) self.graph.update_data(self.variable_x, self.variable_y, True) self.connect_pairs() def commit(self): if self.embedding is not None: names = get_unique_names([v.name for v in self.data.domain], ["mds-x", "mds-y"]) output = embedding = Orange.data.Table.from_numpy( Orange.data.Domain([ ContinuousVariable(names[0]), ContinuousVariable(names[1]) ]), self.embedding) else: output = embedding = None if self.embedding is not None and self.data is not None: domain = self.data.domain domain = Orange.data.Domain( domain.attributes, domain.class_vars, domain.metas + embedding.domain.attributes) output = self.data.transform(domain) output.metas[:, -2:] = embedding.X selection = self.graph.get_selection() if output is not None and len(selection) > 0: selected = output[selection] else: selected = None self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send( create_annotated_table(output, selection)) def onDeleteWidget(self): super().onDeleteWidget() self._clear_plot() self._clear() def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert( (("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", self.graph.jitter_size != 0 and "{} %".format(self.graph.jitter_size)))) self.report_plot() if caption: self.report_caption(caption) @classmethod def migrate_settings(cls, settings_, version): if version < 2: settings_graph = {} for old, new in (("label_only_selected", "label_only_selected"), ("symbol_opacity", "alpha_value"), ("symbol_size", "point_width"), ("jitter", "jitter_size")): settings_graph[new] = settings_[old] settings_["graph"] = settings_graph settings_["auto_commit"] = settings_["autocommit"] @classmethod def migrate_context(cls, context, version): if version < 2: domain = context.ordered_domain n_domain = [t for t in context.ordered_domain if t[1] == 2] c_domain = [t for t in context.ordered_domain if t[1] == 1] context_values_graph = {} for _, old_val, new_val in ((domain, "color_value", "attr_color"), (c_domain, "shape_value", "attr_shape"), (n_domain, "size_value", "attr_size"), (domain, "label_value", "attr_label")): tmp = context.values[old_val] if tmp[1] >= 0: context_values_graph[new_val] = (tmp[0], tmp[1] + 100) elif tmp[0] != "Stress": context_values_graph[new_val] = None else: context_values_graph[new_val] = tmp context.values["graph"] = context_values_graph
def test(self): window = QWidget() layout = QVBoxLayout() window.setLayout(layout) stack = stackedwidget.AnimatedStackedWidget() stack.transitionFinished.connect(self.app.exit) layout.addStretch(2) layout.addWidget(stack) layout.addStretch(2) window.show() widget1 = QLabel("A label " * 10) widget1.setWordWrap(True) widget2 = QGroupBox("Group") widget3 = QListView() self.assertEqual(stack.count(), 0) self.assertEqual(stack.currentIndex(), -1) stack.addWidget(widget1) self.assertEqual(stack.count(), 1) self.assertEqual(stack.currentIndex(), 0) stack.addWidget(widget2) stack.addWidget(widget3) self.assertEqual(stack.count(), 3) self.assertEqual(stack.currentIndex(), 0) def widgets(): return [stack.widget(i) for i in range(stack.count())] self.assertSequenceEqual([widget1, widget2, widget3], widgets()) stack.show() stack.removeWidget(widget2) self.assertEqual(stack.count(), 2) self.assertEqual(stack.currentIndex(), 0) self.assertSequenceEqual([widget1, widget3], widgets()) stack.setCurrentIndex(1) # wait until animation finished self.app.exec_() self.assertEqual(stack.currentIndex(), 1) widget2 = QGroupBox("Group") stack.insertWidget(1, widget2) self.assertEqual(stack.count(), 3) self.assertEqual(stack.currentIndex(), 2) self.assertSequenceEqual([widget1, widget2, widget3], widgets()) stack.transitionFinished.disconnect(self.app.exit) def toogle(): idx = stack.currentIndex() stack.setCurrentIndex((idx + 1) % stack.count()) timer = QTimer(stack, interval=1000) timer.timeout.connect(toogle) timer.start() self.app.exec_()
class OWScatterPlot(OWDataProjectionWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 keywords = [] class Inputs(OWDataProjectionWidget.Inputs): features = Input("Features", AttributeList) class Outputs(OWDataProjectionWidget.Outputs): features = Output("Features", AttributeList, dynamic=False) settings_version = 4 auto_sample = Setting(True) attr_x = ContextSetting(None) attr_y = ContextSetting(None) tooltip_shows_all = Setting(True) GRAPH_CLASS = OWScatterPlotGraph graph = SettingProvider(OWScatterPlotGraph) embedding_variables_names = None xy_changed_manually = Signal(Variable, Variable) class Warning(OWDataProjectionWidget.Warning): missing_coords = Msg( "Plot cannot be displayed because '{}' or '{}' " "is missing for all data points") no_continuous_vars = Msg("Data has no continuous variables") class Information(OWDataProjectionWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") missing_coords = Msg( "Points with missing '{}' or '{}' are not displayed") def __init__(self): self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) super().__init__() # manually register Matplotlib file writers self.graph_writers = self.graph_writers.copy() for w in [MatplotlibFormat, MatplotlibPDFFormat]: self.graph_writers.append(w) def _add_controls(self): self._add_controls_axis() self._add_controls_sampling() super()._add_controls() self.gui.add_widgets( [self.gui.ShowGridLines, self.gui.ToolTipShowsAll, self.gui.RegressionLine], self._plot_box) gui.checkBox( gui.indentedBox(self._plot_box), self, value="graph.orthonormal_regression", label="Treat variables as independent", callback=self.graph.update_regression_line, tooltip= "If checked, fit line to group (minimize distance from points);\n" "otherwise fit y as a function of x (minimize vertical distances)") def _add_controls_axis(self): common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, contentsLength=14 ) self.attr_box = gui.vBox(self.controlArea, True) dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=ContinuousVariable) self.cb_attr_x = gui.comboBox( self.attr_box, self, "attr_x", label="Axis x:", callback=self.set_attr_from_combo, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( self.attr_box, self, "attr_y", label="Axis y:", callback=self.set_attr_from_combo, model=self.xy_model, **common_options) vizrank_box = gui.hBox(self.attr_box) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) def _add_controls_sampling(self): self.sampling = gui.auto_commit( self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) @property def effective_variables(self): return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else [] def _vizrank_color_change(self): self.vizrank.initialize() err_msg = "" if self.data is None: err_msg = "No data on input" elif self.data.is_sparse(): err_msg = "Data is sparse" elif len(self.xy_model) < 3: err_msg = "Not enough features for ranking" elif self.attr_color is None: err_msg = "Color variable is not selected" elif np.isnan(self.data.get_column_view( self.attr_color)[0].astype(float)).all(): err_msg = "Color variable has no values" self.vizrank_button.setEnabled(not err_msg) self.vizrank_button.setToolTip(err_msg) def set_data(self, data): super().set_data(data) self._vizrank_color_change() def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.attr_label, str): self.attr_label = findvar(self.attr_label, self.gui.label_model) if isinstance(self.attr_color, str): self.attr_color = findvar(self.attr_color, self.gui.color_model) if isinstance(self.attr_shape, str): self.attr_shape = findvar(self.attr_shape, self.gui.shape_model) if isinstance(self.attr_size, str): self.attr_size = findvar(self.attr_size, self.gui.size_model) def check_data(self): super().check_data() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(self.data, SqlTable): if self.data.approx_len() < 4000: self.data = Table(self.data) else: self.Information.sampled_sql() self.sql_data = self.data data_sample = self.data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) self.data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if self.data is not None: if not self.data.domain.has_continuous_attributes(True, True): self.Warning.no_continuous_vars() self.data = None if self.data is not None and (len(self.data) == 0 or len(self.data.domain) == 0): self.data = None def get_embedding(self): self.valid_data = None if self.data is None: return None x_data = self.get_column(self.attr_x, filter_valid=False) y_data = self.get_column(self.attr_y, filter_valid=False) if x_data is None or y_data is None: return None self.Warning.missing_coords.clear() self.Information.missing_coords.clear() self.valid_data = np.isfinite(x_data) & np.isfinite(y_data) if self.valid_data is not None and not np.all(self.valid_data): msg = self.Information if np.any(self.valid_data) else self.Warning msg.missing_coords(self.attr_x.name, self.attr_y.name) return np.vstack((x_data, y_data)).T # Tooltip def _point_tooltip(self, point_id, skip_attrs=()): point_data = self.data[point_id] xy_attrs = (self.attr_x, self.attr_y) text = "<br/>".join( escape('{} = {}'.format(var.name, point_data[var])) for var in xy_attrs) if self.tooltip_shows_all: others = super()._point_tooltip(point_id, skip_attrs=xy_attrs) if others: text = "<b>{}</b><br/><br/>{}".format(text, others) return text def add_data(self, time=0.4): if self.data and len(self.data) > 2000: self.__timer.stop() return data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.handleNewSignals() def init_attr_values(self): super().init_attr_values() data = self.data domain = data.domain if data and len(data) else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None super().set_subset_data(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.attr_box.setEnabled(True) self.vizrank.setEnabled(True) if self.attribute_selection_list and self.data is not None and \ self.data.domain is not None and \ all(attr in self.data.domain for attr in self.attribute_selection_list): self.attr_x, self.attr_y = self.attribute_selection_list[:2] self.attr_box.setEnabled(False) self.vizrank.setEnabled(False) super().handleNewSignals() if self._domain_invalidated: self.graph.update_axes() self._domain_invalidated = False @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] self._invalidated = self._invalidated \ or self.attr_x != attributes[0] \ or self.attr_y != attributes[1] else: self.attribute_selection_list = None def set_attr(self, attr_x, attr_y): if attr_x != self.attr_x or attr_y != self.attr_y: self.attr_x, self.attr_y = attr_x, attr_y self.attr_changed() def set_attr_from_combo(self): self.attr_changed() self.xy_changed_manually.emit(self.attr_x, self.attr_y) def attr_changed(self): self.setup_plot() self.commit() def get_axes(self): return {"bottom": self.attr_x, "left": self.attr_y} def colors_changed(self): super().colors_changed() self._vizrank_color_change() def commit(self): super().commit() self.send_features() def send_features(self): features = [attr for attr in [self.attr_x, self.attr_y] if attr] self.Outputs.features.send(features or None) def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) return None @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]] if version < 3: if "auto_send_selection" in settings: settings["auto_commit"] = settings["auto_send_selection"] if "selection_group" in settings: settings["selection"] = settings["selection_group"] @classmethod def migrate_context(cls, context, version): values = context.values if version < 3: values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"] if version < 4: if values["attr_x"][1] % 100 == 1 or values["attr_y"][1] % 100 == 1: raise IncompatibleContext()
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table, Default), (ANNOTATED_DATA_SIGNAL_NAME, Table), ("Features", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting(None) attr_y = ContextSetting(None) graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QPen(self.palette().color(QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) gui.valueSlider( box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox( gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) self.sampling = gui.auto_commit( self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) box = gui.vBox(self.controlArea, "Points") self.color_model = DomainModel( placeholder="(Same color)", valid_types=dmod.PRIMITIVE) self.cb_attr_color = gui.comboBox( box, self, "graph.attr_color", label="Color:", callback=self.update_colors, model=self.color_model, **common_options) self.label_model = DomainModel( placeholder="(No labels)", valid_types=dmod.PRIMITIVE) self.cb_attr_label = gui.comboBox( box, self, "graph.attr_label", label="Label:", callback=self.graph.update_labels, model=self.label_model, **common_options) self.shape_model = DomainModel( placeholder="(Same shape)", valid_types=DiscreteVariable) self.cb_attr_shape = gui.comboBox( box, self, "graph.attr_shape", label="Shape:", callback=self.graph.update_shapes, model=self.shape_model, **common_options) self.size_model = DomainModel( placeholder="(Same size)", valid_types=ContinuousVariable) self.cb_attr_size = gui.comboBox( box, self, "graph.attr_size", label="Size:", callback=self.graph.update_sizes, model=self.size_model, **common_options) self.models = [self.xy_model, self.color_model, self.label_model, self.shape_model, self.size_model] g = self.graph.gui g.point_properties_box(self.controlArea, box) box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox( box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') self.cb_class_density = gui.checkBox( box, self, value='graph.class_density', label='Show class density', callback=self.update_density) gui.checkBox( box, self, 'graph.label_only_selected', 'Label only selected points', callback=self.graph.update_labels) self.zoom_select_toolbar = g.zoom_select_toolbar( gui.vBox(self.controlArea, "Zoom/Select"), nomargin=True, buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset] ) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect(self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QAction( "Zoom in", self, triggered=lambda: zoom(1.25) ) zoom_in.setShortcuts([QKeySequence(QKeySequence.ZoomIn), QKeySequence(self.tr("Ctrl+="))]) zoom_out = QAction( "Zoom out", self, shortcut=QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25) ) zoom_fit = QAction( "Fit in view", self, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view ) self.addActions([zoom_in, zoom_out, zoom_fit]) # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.update_graph() def set_data(self, data): self.clear_messages() self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank.initialize() self.vizrank.attrs = self.data.domain.attributes if self.data is not None else [] self.vizrank_button.setEnabled( self.data is not None and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) if self.data is not None and self.data.domain.class_var is None \ and len(self.data.domain.attributes) > 1 and len(self.data) > 1: self.vizrank_button.setToolTip( "Data with a class variable is required.") else: self.vizrank_button.setToolTip("") self.openContext(self.data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Orange.data.Variable) and el.name == name: return el else: return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.graph.attr_label, str): self.graph.attr_label = findvar( self.graph.attr_label, self.label_model) if isinstance(self.graph.attr_color, str): self.graph.attr_color = findvar( self.graph.attr_color, self.color_model) if isinstance(self.graph.attr_shape, str): self.graph.attr_shape = findvar( self.graph.attr_shape, self.shape_model) if isinstance(self.graph.attr_size, str): self.graph.attr_size = findvar( self.graph.attr_size, self.size_model) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [a for a in data.domain.attributes + data.domain.metas if a.is_primitive()] new_metas = [m for m in data.domain.metas if not m.is_primitive()] data = Table.from_table(Domain(new_attrs, data.domain.class_vars, new_metas), data) return data def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) self.controls.graph.alpha_value.setEnabled(subset_data is None) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data_metas_X, self.subset_data) if self.attribute_selection_list and \ all(attr in self.graph.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.unconditional_commit() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x self.graph.attr_color = domain and self.data.domain.class_var or None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.send_features() def update_colors(self): self.graph.update_colors() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() def send_data(self): selected = None selection = None # TODO: Implement selection for sql data if isinstance(self.data, SqlTable): selected = self.data elif self.data is not None: selection = self.graph.get_selection() if len(selection) > 0: selected = self.data[selection] self.send("Selected Data", selected) self.send(ANNOTATED_DATA_SIGNAL_NAME, create_annotated_table(self.data, selection)) def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"),)) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.send("Features", features) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): def name(var): return var and var.name caption = report.render_items_vert(( ("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear()
class OWGOBrowser(widget.OWWidget): name = "GO Browser" description = "Enrichment analysis for Gene Ontology terms." icon = "../widgets/icons/OWGOBrowser.svg" priority = 7 inputs = [ ("Cluster Data", Orange.data.Table, "set_dataset", widget.Single + widget.Default), ("Reference Data", Orange.data.Table, "set_reference_dataset"), ] outputs = [("Data on Selected Genes", Orange.data.Table), ("Enrichment Report", Orange.data.Table)] settingsHandler = settings.DomainContextHandler() gene_attr_index = settings.ContextSetting(0) use_attr_names = settings.ContextSetting(False) use_reference_dataset = settings.Setting(False) aspect_index = settings.Setting(0) use_evidence_type = settings.Setting( {et: True for et in go.evidence_types_ordered}) filter_by_num_of_instances = settings.Setting(False) min_num_of_instances = settings.Setting(1) filter_by_p_value = settings.Setting(True) max_p_value = settings.Setting(0.2) filter_by_p_value_nofdr = settings.Setting(False) max_p_value_no_fdr = settings.Setting(0.01) prob_func = settings.Setting(0) selection_direct_annotation = settings.Setting(0) selection_disjoint = settings.Setting(0) class Error(widget.OWWidget.Error): serverfiles_unavailable = widget.Msg( 'Can not locate annotation files, ' 'please check your connection and try again.') missing_annotation = widget.Msg(ERROR_ON_MISSING_ANNOTATION) missing_gene_id = widget.Msg(ERROR_ON_MISSING_GENE_ID) missing_tax_id = widget.Msg(ERROR_ON_MISSING_TAX_ID) def __init__(self, parent=None): super().__init__(self, parent) self.input_data = None self.gene_info = None self.ref_data = None self.ontology = None self.annotations = None self.loaded_annotation_code = None self.treeStructRootKey = None self.probFunctions = [ statistics.Binomial(), statistics.Hypergeometric() ] self.selectedTerms = [] self.selectionChanging = 0 self.__state = State.Ready self.__scheduletimer = QTimer(self, singleShot=True) self.__scheduletimer.timeout.connect(self.__update) ############# # GUI ############# self.tabs = gui.tabWidget(self.controlArea) # Input tab self.inputTab = gui.createTabPage(self.tabs, "Input") box = gui.widgetBox(self.inputTab, "Info") self.infoLabel = gui.widgetLabel(box, "No data on input\n") gui.button( box, self, "Ontology/Annotation Info", callback=self.show_info, tooltip="Show information on loaded ontology and annotations", ) self.referenceRadioBox = gui.radioButtonsInBox( self.inputTab, self, "use_reference_dataset", ["Entire genome", "Reference set (input)"], tooltips=[ "Use entire genome for reference", "Use genes from Referece Examples input signal as reference" ], box="Reference", callback=self.__invalidate, ) self.referenceRadioBox.buttons[1].setDisabled(True) gui.radioButtonsInBox( self.inputTab, self, "aspect_index", ["Biological process", "Cellular component", "Molecular function"], box="Aspect", callback=self.__invalidate, ) # Filter tab self.filterTab = gui.createTabPage(self.tabs, "Filter") box = gui.widgetBox(self.filterTab, "Filter GO Term Nodes") gui.checkBox( box, self, "filter_by_num_of_instances", "Genes", callback=self.filter_and_display_graph, tooltip="Filter by number of input genes mapped to a term", ) ibox = gui.indentedBox(box) gui.spin( ibox, self, 'min_num_of_instances', 1, 100, step=1, label='#:', labelWidth=15, callback=self.filter_and_display_graph, callbackOnReturn=True, tooltip="Min. number of input genes mapped to a term", ) gui.checkBox( box, self, "filter_by_p_value_nofdr", "p-value", callback=self.filter_and_display_graph, tooltip="Filter by term p-value", ) gui.doubleSpin( gui.indentedBox(box), self, 'max_p_value_no_fdr', 1e-8, 1, step=1e-8, label='p:', labelWidth=15, callback=self.filter_and_display_graph, callbackOnReturn=True, tooltip="Max term p-value", ) # use filter_by_p_value for FDR, as it was the default in prior versions gui.checkBox(box, self, "filter_by_p_value", "FDR", callback=self.filter_and_display_graph, tooltip="Filter by term FDR") gui.doubleSpin( gui.indentedBox(box), self, 'max_p_value', 1e-8, 1, step=1e-8, label='p:', labelWidth=15, callback=self.filter_and_display_graph, callbackOnReturn=True, tooltip="Max term p-value", ) box = gui.widgetBox(box, "Significance test") gui.radioButtonsInBox( box, self, "prob_func", ["Binomial", "Hypergeometric"], tooltips=[ "Use binomial distribution test", "Use hypergeometric distribution test" ], callback=self.__invalidate, ) # TODO: only update the p values box = gui.widgetBox(self.filterTab, "Evidence codes in annotation", addSpace=True) self.evidenceCheckBoxDict = {} for etype in go.evidence_types_ordered: ecb = QCheckBox(etype, toolTip=go.evidence_types[etype], checked=self.use_evidence_type[etype]) ecb.toggled.connect(self.__on_evidence_changed) box.layout().addWidget(ecb) self.evidenceCheckBoxDict[etype] = ecb # Select tab self.selectTab = gui.createTabPage(self.tabs, "Select") box = gui.radioButtonsInBox( self.selectTab, self, "selection_direct_annotation", ["Directly or Indirectly", "Directly"], box="Annotated genes", callback=self.example_selection, ) box = gui.widgetBox(self.selectTab, "Output", addSpace=True) gui.radioButtonsInBox( box, self, "selection_disjoint", btnLabels=[ "All selected genes", "Term-specific genes", "Common term genes" ], tooltips=[ "Outputs genes annotated to all selected GO terms", "Outputs genes that appear in only one of selected GO terms", "Outputs genes common to all selected GO terms", ], callback=self.example_selection, ) # ListView for DAG, and table for significant GOIDs self.DAGcolumns = [ 'GO term', 'Cluster', 'Reference', 'p-value', 'FDR', 'Genes', 'Enrichment' ] self.splitter = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitter) # list view self.listView = GOTreeWidget(self.splitter) self.listView.setSelectionMode(QTreeView.ExtendedSelection) self.listView.setAllColumnsShowFocus(1) self.listView.setColumnCount(len(self.DAGcolumns)) self.listView.setHeaderLabels(self.DAGcolumns) self.listView.header().setSectionsClickable(True) self.listView.header().setSortIndicatorShown(True) self.listView.header().setSortIndicator( self.DAGcolumns.index('p-value'), Qt.AscendingOrder) self.listView.setSortingEnabled(True) self.listView.setItemDelegateForColumn( 6, EnrichmentColumnItemDelegate(self)) self.listView.setRootIsDecorated(True) self.listView.itemSelectionChanged.connect(self.view_selection_changed) # table of significant GO terms self.sigTerms = QTreeWidget(self.splitter) self.sigTerms.setColumnCount(len(self.DAGcolumns)) self.sigTerms.setHeaderLabels(self.DAGcolumns) self.sigTerms.setSortingEnabled(True) self.sigTerms.setSelectionMode(QTreeView.ExtendedSelection) self.sigTerms.header().setSortIndicator( self.DAGcolumns.index('p-value'), Qt.AscendingOrder) self.sigTerms.setItemDelegateForColumn( 6, EnrichmentColumnItemDelegate(self)) self.sigTerms.itemSelectionChanged.connect( self.table_selection_changed) self.sigTableTermsSorted = [] self.graph = {} self.originalGraph = None self.inputTab.layout().addStretch(1) self.filterTab.layout().addStretch(1) self.selectTab.layout().addStretch(1) class AnnotationSlot(SimpleNamespace): taxid = ... # type: str name = ... # type: str filename = ... # type:str @staticmethod def parse_tax_id(f_name): return f_name.split('.')[0] try: remote_files = serverfiles.ServerFiles().listfiles(DOMAIN) except (ConnectTimeout, RequestException, ConnectionError): # TODO: Warn user about failed connection to the remote server remote_files = [] self.available_annotations = [ AnnotationSlot( taxid=AnnotationSlot.parse_tax_id(annotation_file), name=taxonomy.common_taxid_to_name( AnnotationSlot.parse_tax_id(annotation_file)), filename=FILENAME_ANNOTATION.format( AnnotationSlot.parse_tax_id(annotation_file)), ) for _, annotation_file in set(remote_files + serverfiles.listfiles(DOMAIN)) if annotation_file != FILENAME_ONTOLOGY ] self._executor = ThreadExecutor() def sizeHint(self): return QSize(1000, 700) def __on_evidence_changed(self): for etype, cb in self.evidenceCheckBoxDict.items(): self.use_evidence_type[etype] = cb.isChecked() self.__invalidate() def clear(self): self.infoLabel.setText("No data on input\n") self.warning(0) self.warning(1) self.clear_graph() self.send("Data on Selected Genes", None) self.send("Enrichment Report", None) def set_dataset(self, data=None): self.closeContext() self.clear() self.Error.clear() if data: self.input_data = data self.tax_id = str(self.input_data.attributes.get(TAX_ID, None)) self.use_attr_names = self.input_data.attributes.get( GENE_AS_ATTRIBUTE_NAME, None) self.gene_id_attribute = self.input_data.attributes.get( GENE_ID_ATTRIBUTE, None) self.gene_id_column = self.input_data.attributes.get( GENE_ID_COLUMN, None) self.annotation_index = None if not (self.use_attr_names is not None and ((self.gene_id_attribute is None) ^ (self.gene_id_column is None))): if self.tax_id is None: self.Error.missing_annotation() return self.Error.missing_gene_id() return elif self.tax_id is None: self.Error.missing_tax_id() return _c2i = { a.taxid: i for i, a in enumerate(self.available_annotations) } try: self.annotation_index = _c2i[self.tax_id] except KeyError: self.Error.serverfiles_unavailable() # raise ValueError('Taxonomy {} not supported.'.format(self.tax_id)) return self.gene_info = gene.GeneInfo(self.tax_id) self.__invalidate() def set_reference_dataset(self, data=None): self.Error.clear() if data: self.ref_data = data self.ref_tax_id = str(self.ref_data.attributes.get(TAX_ID, None)) self.ref_use_attr_names = self.ref_data.attributes.get( GENE_AS_ATTRIBUTE_NAME, None) self.ref_gene_id_attribute = self.ref_data.attributes.get( GENE_ID_ATTRIBUTE, None) self.ref_gene_id_column = self.ref_data.attributes.get( GENE_ID_COLUMN, None) if not (self.ref_use_attr_names is not None and ((self.ref_gene_id_attribute is None) ^ (self.ref_gene_id_column is None))): if self.ref_tax_id is None: self.Error.missing_annotation() return self.Error.missing_gene_id() return elif self.ref_tax_id is None: self.Error.missing_tax_id() return self.referenceRadioBox.buttons[1].setDisabled(not bool(data)) self.referenceRadioBox.buttons[1].setText("Reference set") if self.input_data is not None and self.use_reference_dataset: self.use_reference_dataset = 0 if not data else 1 self.__invalidate() @Slot() def __invalidate(self): # Invalidate the current results or pending task and schedule an # update. self.__scheduletimer.start() if self.__state != State.Ready: self.__state |= State.Stale self.set_graph({}) self.ref_genes = None self.input_genes = None def __invalidate_annotations(self): self.annotations = None self.loaded_annotation_code = None if self.input_data: self.infoLabel.setText("...\n") self.__invalidate() @Slot() def __update(self): self.__scheduletimer.stop() if self.input_data is None: return if self.__state & State.Running: self.__state |= State.Stale elif self.__state & State.Downloading: self.__state |= State.Stale elif self.__state & State.Ready: if self.__ensure_data(): self.load() self.enrichment() else: assert self.__state & State.Downloading assert self.isBlocking() def __get_ref_genes(self): self.ref_genes = [] if self.ref_use_attr_names: for variable in self.input_data.domain.attributes: self.ref_genes.append( str( variable.attributes.get(self.ref_gene_id_attribute, '?'))) else: genes, _ = self.ref_data.get_column_view(self.ref_gene_id_column) self.ref_genes = [str(g) for g in genes] def __get_input_genes(self): self.input_genes = [] if self.use_attr_names: for variable in self.input_data.domain.attributes: self.input_genes.append( str(variable.attributes.get(self.gene_id_attribute, '?'))) else: genes, _ = self.input_data.get_column_view(self.gene_id_column) self.input_genes = [str(g) for g in genes] def filter_annotated_genes(self, genes): matchedgenes = self.annotations.get_gene_names_translator( genes).values() return matchedgenes, [ gene for gene in genes if gene not in matchedgenes ] def __start_download(self, files_list): # type: (List[Tuple[str, str]]) -> None task = EnsureDownloaded(files_list) task.progress.connect(self._progress_bar_set) f = self._executor.submit(task) fw = FutureWatcher(f, self) fw.finished.connect(self.__download_finish) fw.finished.connect(fw.deleteLater) fw.resultReady.connect(self.__invalidate) self.progressBarInit(processEvents=None) self.setBlocking(True) self.setStatusMessage("Downloading") self.__state = State.Downloading @Slot(Future) def __download_finish(self, result): # type: (Future[None]) -> None assert QThread.currentThread() is self.thread() assert result.done() self.setBlocking(False) self.setStatusMessage("") self.progressBarFinished(processEvents=False) try: result.result() except ConnectTimeout: logging.getLogger(__name__).error("Error:") self.error( 2, "Internet connection error, unable to load data. " + "Check connection and create a new GO Browser widget.", ) except RequestException as err: logging.getLogger(__name__).error("Error:") self.error(2, "Internet error:\n" + str(err)) except BaseException as err: logging.getLogger(__name__).error("Error:") self.error(2, "Error:\n" + str(err)) raise else: self.error(2) finally: self.__state = State.Ready def __ensure_data(self): # Ensure that all required database (ontology and annotations for # the current selected organism are present. If not start a download in # the background. Return True if all dbs are present and false # otherwise assert self.__state == State.Ready annotation = self.available_annotations[self.annotation_index] go_files = [fname for domain, fname in serverfiles.listfiles(DOMAIN)] files = [] if annotation.filename not in go_files: files.append(("go", annotation.filename)) if FILENAME_ONTOLOGY not in go_files: files.append((DOMAIN, FILENAME_ONTOLOGY)) if files: self.__start_download(files) assert self.__state == State.Downloading return False else: return True def load(self): a = self.available_annotations[self.annotation_index] if self.ontology is None: self.ontology = go.Ontology() if a.taxid != self.loaded_annotation_code: self.annotations = None gc.collect() # Force run garbage collection self.annotations = go.Annotations(a.taxid) self.loaded_annotation_code = a.taxid count = defaultdict(int) gene_sets = defaultdict(set) for anno in self.annotations.annotations: count[anno.evidence] += 1 gene_sets[anno.evidence].add(anno.gene_id) for etype in go.evidence_types_ordered: ecb = self.evidenceCheckBoxDict[etype] ecb.setEnabled(bool(count[etype])) ecb.setText(etype + ": %i annots(%i genes)" % (count[etype], len(gene_sets[etype]))) def enrichment(self): assert self.input_data is not None assert self.__state == State.Ready if not self.annotations.ontology: self.annotations.ontology = self.ontology self.error(1) self.warning([0, 1]) self.__get_input_genes() self.input_genes = set(self.input_genes) self.known_input_genes = self.annotations.get_genes_with_known_annotation( self.input_genes) # self.clusterGenes = clusterGenes = self.annotations.map_to_ncbi_id(self.input_genes).values() self.infoLabel.setText( "%i unique genes on input\n%i (%.1f%%) genes with known annotations" % ( len(self.input_genes), len(self.known_input_genes), 100.0 * len(self.known_input_genes) / len(self.input_genes) if len(self.input_genes) else 0.0, )) if not self.use_reference_dataset or self.ref_data is None: self.information(2) self.information(1) self.ref_genes = set(self.gene_info.keys()) elif self.ref_data is not None: self.__get_ref_genes() self.ref_genes = set(self.ref_genes) ref_count = len(self.ref_genes) if ref_count == 0: self.ref_genes = self.annotations.genes() self.referenceRadioBox.buttons[1].setText("Reference set") self.referenceRadioBox.buttons[1].setDisabled(True) self.information( 2, "Unable to extract gene names from reference dataset. " "Using entire genome for reference") self.use_reference_dataset = 0 else: self.referenceRadioBox.buttons[1].setText( "Reference set ({} genes)".format(ref_count)) self.referenceRadioBox.buttons[1].setDisabled(False) self.information(2) else: self.use_reference_dataset = 0 self.ref_genes = [] if not self.ref_genes: self.error(1, "No valid reference set") return {} evidences = [] for etype in go.evidence_types_ordered: if self.use_evidence_type[etype]: evidences.append(etype) aspect = ['Process', 'Component', 'Function'][self.aspect_index] self.progressBarInit(processEvents=False) self.setBlocking(True) self.__state = State.Running if self.input_genes: f = self._executor.submit( self.annotations.get_enriched_terms, self.input_genes, self.ref_genes, evidences, aspect=aspect, prob=self.probFunctions[self.prob_func], use_fdr=False, progress_callback=methodinvoke(self, "_progress_bar_set", (float, )), ) fw = FutureWatcher(f, parent=self) fw.done.connect(self.__on_enrichment_done) fw.done.connect(fw.deleteLater) return else: f = Future() f.set_result({}) self.__on_enrichment_done(f) def __on_enrichment_done(self, results): # type: (Future[Dict[str, tuple]]) -> None self.progressBarFinished(processEvents=False) self.setBlocking(False) self.setStatusMessage("") if self.__state & State.Stale: self.__state = State.Ready self.__invalidate() return self.__state = State.Ready try: results = results.result() # type: Dict[str, tuple] except Exception as ex: results = {} error = str(ex) self.error(1, error) if results: terms = list(results.items()) fdr_vals = statistics.FDR([d[1] for _, d in terms]) terms = [(key, d + (fdr, )) for (key, d), fdr in zip(terms, fdr_vals)] terms = dict(terms) else: terms = {} self.terms = terms if not self.terms: self.warning(0, "No enriched terms found.") else: self.warning(0) self.treeStructDict = {} ids = self.terms.keys() self.treeStructRootKey = None parents = {} for _id in ids: parents[_id] = {term for _, term in self.ontology[_id].related} children = {} for term in self.terms: children[term] = {id for id in ids if term in parents[id]} for term in self.terms: self.treeStructDict[term] = TreeNode(self.terms[term], children[term]) if not self.ontology[term].related and not getattr( self.ontology[term], "is_obsolete", False): self.treeStructRootKey = term self.set_graph(terms) self._update_enrichment_report_output() self.commit() def _update_enrichment_report_output(self): terms = sorted(self.terms.items(), key=lambda item: item[1][1]) # Create and send the enrichemnt report table. terms_domain = Orange.data.Domain( [], [], # All is meta! [ Orange.data.StringVariable("GO Term Id"), Orange.data.StringVariable("GO Term Name"), Orange.data.ContinuousVariable("Cluster Frequency"), Orange.data.ContinuousVariable("Genes in Cluster", number_of_decimals=0), Orange.data.ContinuousVariable("Reference Frequency"), Orange.data.ContinuousVariable("Genes in Reference", number_of_decimals=0), Orange.data.ContinuousVariable("p-value"), Orange.data.ContinuousVariable("FDR"), Orange.data.ContinuousVariable("Enrichment"), Orange.data.StringVariable("Genes"), ], ) terms = [[ t_id, self.ontology[t_id].name, len(genes) / len(self.input_genes), len(genes), r_count / len(self.ref_genes), r_count, p_value, fdr, len(genes) / len(self.input_genes) * len(self.ref_genes) / r_count, ",".join(genes), ] for t_id, (genes, p_value, r_count, fdr) in terms if genes and r_count] if terms: x = numpy.empty((len(terms), 0)) m = numpy.array(terms, dtype=object) terms_table = Orange.data.Table.from_numpy(terms_domain, x, metas=m) else: terms_table = None self.send("Enrichment Report", terms_table) @Slot(float) def _progress_bar_set(self, value): assert QThread.currentThread() is self.thread() self.progressBarSet(value, processEvents=None) @Slot() def _progress_bar_finish(self): assert QThread.currentThread() is self.thread() self.progressBarFinished(processEvents=None) def filter_graph(self, graph): if self.filter_by_p_value_nofdr: graph = go.filter_by_p_value(graph, self.max_p_value_no_fdr) if self.filter_by_p_value: # FDR graph = dict( filter(lambda item: item[1][3] <= self.max_p_value, graph.items())) if self.filter_by_num_of_instances: graph = dict( filter( lambda item: len(item[1][0]) >= self.min_num_of_instances, graph.items())) return graph def filter_and_display_graph(self): if self.input_data and self.originalGraph is not None: self.graph = self.filter_graph(self.originalGraph) if self.originalGraph and not self.graph: self.warning(1, "All found terms were filtered out.") else: self.warning(1) self.clear_graph() self.display_graph() def set_graph(self, graph=None): self.originalGraph = graph if graph: self.filter_and_display_graph() else: self.graph = {} self.clear_graph() def clear_graph(self): self.listView.clear() self.listViewItems = [] self.sigTerms.clear() def display_graph(self): from_parent_dict = {} self.termListViewItemDict = {} self.listViewItems = [] def enrichment(t): try: return len(t[0]) / t[2] * (len(self.ref_genes) / len(self.input_genes)) except ZeroDivisionError: # TODO: find out why this happens return 0 max_fold_enrichment = max( [enrichment(term) for term in self.graph.values()] or [1]) def add_node(term, parent, parent_display_node): if (parent, term) in from_parent_dict: return if term in self.graph: display_node = GOTreeWidgetItem( self.ontology[term], self.graph[term], len(self.input_genes), len(self.ref_genes), max_fold_enrichment, parent_display_node, ) display_node.goId = term self.listViewItems.append(display_node) if term in self.termListViewItemDict: self.termListViewItemDict[term].append(display_node) else: self.termListViewItemDict[term] = [display_node] from_parent_dict[(parent, term)] = True parent = term else: display_node = parent_display_node for c in self.treeStructDict[term].children: add_node(c, parent, display_node) if self.treeStructDict: add_node(self.treeStructRootKey, None, self.listView) terms = self.graph.items() terms = sorted(terms, key=lambda item: item[1][1]) self.sigTableTermsSorted = [t[0] for t in terms] self.sigTerms.clear() for i, (t_id, (genes, p_value, ref_count, fdr)) in enumerate(terms): item = GOTreeWidgetItem( self.ontology[t_id], (genes, p_value, ref_count, fdr), len(self.input_genes), len(self.ref_genes), max_fold_enrichment, self.sigTerms, ) item.goId = t_id self.listView.expandAll() for i in range(5): self.listView.resizeColumnToContents(i) self.sigTerms.resizeColumnToContents(i) self.sigTerms.resizeColumnToContents(6) width = min(self.listView.columnWidth(0), 350) self.listView.setColumnWidth(0, width) self.sigTerms.setColumnWidth(0, width) def view_selection_changed(self): if self.selectionChanging: return self.selectionChanging = 1 self.selectedTerms = [] selected = self.listView.selectedItems() self.selectedTerms = list({lvi.term.id for lvi in selected}) self.example_selection() self.selectionChanging = 0 def table_selection_changed(self): if self.selectionChanging: return self.selectionChanging = 1 self.selectedTerms = [] selected_ids = { self.sigTerms.itemFromIndex(index).goId for index in self.sigTerms.selectedIndexes() } for i in range(self.sigTerms.topLevelItemCount()): item = self.sigTerms.topLevelItem(i) selected = item.goId in selected_ids term = item.goId if selected: self.selectedTerms.append(term) for lvi in self.termListViewItemDict[term]: try: lvi.setSelected(selected) if selected: lvi.setExpanded(True) except RuntimeError: # Underlying C/C++ object deleted pass self.selectionChanging = 0 self.example_selection() def example_selection(self): self.commit() def commit(self): if self.input_data is None or self.originalGraph is None or self.annotations is None: return if self.__state & State.Stale: return terms = set(self.selectedTerms) genes = reduce(operator.ior, (set(self.graph[term][0]) for term in terms), set()) evidences = [] for etype in go.evidence_types_ordered: if self.use_evidence_type[etype]: evidences.append(etype) all_terms = self.annotations.get_annotated_terms( genes, direct_annotation_only=self.selection_direct_annotation, evidence_codes=evidences) if self.selection_disjoint > 0: count = defaultdict(int) for term in self.selectedTerms: for g in all_terms.get(term, []): count[g] += 1 ccount = 1 if self.selection_disjoint == 1 else len( self.selectedTerms) selected_genes = [ gene for gene, c in count.items() if c == ccount and gene in genes ] else: selected_genes = reduce(operator.ior, (set(all_terms.get(term, [])) for term in self.selectedTerms), set()) if self.use_attr_names: selected = [ column for column in self.input_data.domain.attributes if self.gene_id_attribute in column.attributes and str(column.attributes[self.gene_id_attribute]) in set( selected_genes) ] domain = Orange.data.Domain(selected, self.input_data.domain.class_vars, self.input_data.domain.metas) new_data = self.input_data.from_table(domain, self.input_data) self.send("Data on Selected Genes", new_data) else: selected_rows = [] for row_index, row in enumerate(self.input_data): gene_in_row = str(row[self.gene_id_column]) if gene_in_row in self.input_genes and gene_in_row in selected_genes: selected_rows.append(row_index) if selected_rows: selected = self.input_data[selected_rows] else: selected = None self.send("Data on Selected Genes", selected) def show_info(self): dialog = QDialog(self) dialog.setModal(False) dialog.setLayout(QVBoxLayout()) label = QLabel(dialog) label.setText( "Ontology:\n" + self.ontology.header if self.ontology else "Ontology not loaded!") dialog.layout().addWidget(label) label = QLabel(dialog) label.setText("Annotations:\n" + self.annotations.header.replace("!", "") if self. annotations else "Annotations not loaded!") dialog.layout().addWidget(label) dialog.show() def onDeleteWidget(self): """Called before the widget is removed from the canvas. """ self.annotations = None self.ontology = None gc.collect() # Force collection
class OWNxExplorer(widget.OWWidget): name = "Network Explorer" description = "Visually explore the network and its properties." icon = "icons/NetworkExplorer.svg" priority = 6420 class Inputs: network = Input("Network", network.Graph, default=True) node_subset = Input("Node Subset", Table) node_data = Input("Node Data", Table) node_distances = Input("Node Distances", Orange.misc.DistMatrix) class Outputs: subgraph = Output("Selected sub-network", network.Graph) unselected_subgraph = Output("Remaining sub-network", network.Graph) distances = Output("Distance matrix", Orange.misc.DistMatrix) selected = Output("Selected items", Table) highlighted = Output("Highlighted items", Table) remaining = Output("Remaining items", Table) settingsList = [ "lastVertexSizeColumn", "lastColorColumn", "lastLabelColumns", "lastTooltipColumns", ] # TODO: set settings UserAdviceMessages = [ widget.Message( 'When selecting nodes on the Marking tab, ' 'press <b><tt>Enter</tt></b> key to add ' '<b><font color="{}">highlighted</font></b> nodes to ' '<b><font color="{}">selection</font></b>.'.format( Node.Pen.HIGHLIGHTED.color().name(), Node.Pen.SELECTED.color().name()), 'marking-info', widget.Message.Information), widget.Message( 'Left-click to select nodes ' '(hold <b><tt>Shift</tt></b> to append to selection). ' 'Right-click to pan/move the view. Scroll to zoom.', 'mouse-info', widget.Message.Information), ] do_auto_commit = settings.Setting(True) maxNodeSize = settings.Setting(50) minNodeSize = settings.Setting(8) selectionMode = settings.Setting(SelectionMode.FROM_INPUT) tabIndex = settings.Setting(0) showEdgeWeights = settings.Setting(False) relativeEdgeWidths = settings.Setting(False) invertNodeSize = settings.Setting(False) markDistance = settings.Setting(1) markSearchString = settings.Setting("") markNBest = settings.Setting(1) markNConnections = settings.Setting(2) graph_name = 'view' class Warning(widget.OWWidget.Warning): distance_matrix_size = widget.Msg( "Distance matrix size doesn't match the number of network nodes. Not using it." ) no_graph_found = widget.Msg('No graph found!') no_graph_or_items = widget.Msg( 'No graph provided or no items attached to the graph.') class Error(widget.OWWidget.Error): instance_for_each_node = widget.Msg( 'Items table must have one instance for each network node.') network_too_large = widget.Msg( 'Network is too large to visualize. Sorry.') def __init__(self): super().__init__() #self.contextHandlers = {"": DomainContextHandler("", [ContextField("attributes", selected="node_label_attrs"), ContextField("attributes", selected="tooltipAttributes"), "color"])} self.view = GraphView(self) self.mainArea.layout().addWidget(self.view) self.graph_attrs = [] self.acceptingEnterKeypress = False self.node_label_attrs = [] self.tooltipAttributes = [] self.searchStringTimer = QTimer(self) self.markInputItems = None self.node_color_attr = 0 self.node_size_attr = 0 self.nHighlighted = 0 self.nSelected = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self.lastVertexSizeColumn = '' self.lastColorColumn = '' self.lastLabelColumns = set() self.lastTooltipColumns = set() self.items_matrix = None self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.graph = None self.setMinimumWidth(600) self.tabs = gui.tabWidget(self.controlArea) self.displayTab = gui.createTabPage(self.tabs, "Display") self.markTab = gui.createTabPage(self.tabs, "Marking") def on_tab_changed(index): self.tabIndex = index self.set_selection_mode() self.tabs.currentChanged.connect(on_tab_changed) self.tabs.setCurrentIndex(self.tabIndex) ib = gui.widgetBox(self.displayTab, "Info") gui.label( ib, self, "Nodes: %(number_of_nodes_label)i (%(verticesPerEdge).2f per edge)" ) gui.label( ib, self, "Edges: %(number_of_edges_label)i (%(edgesPerVertex).2f per node)") box = gui.widgetBox(self.displayTab, "Nodes") self.relayout_button = gui.button(box, self, 'Re-layout', callback=self.relayout, autoDefault=False) self.view.positionsChanged.connect( lambda positions, progress: self.progressbar.widget.progressBarSet( int(round(100 * progress)))) def animationFinished(): self.relayout_button.setEnabled(True) self.progressbar.finish() self.view.animationFinished.connect(animationFinished) self.colorCombo = gui.comboBox(box, self, "node_color_attr", label='Color:', orientation='horizontal', callback=self.set_node_colors) self.invertNodeSizeCheck = self.maxNodeSizeSpin = QWidget( ) # Forward declaration self.nodeSizeCombo = gui.comboBox(box, self, "node_size_attr", label='Size:', orientation='horizontal', callback=self.set_node_sizes) hb = gui.widgetBox(box, orientation="horizontal") hb.layout().addStretch(1) self.minNodeSizeSpin = gui.spin(hb, self, "minNodeSize", 1, 50, step=1, label="Min:", callback=self.set_node_sizes) self.minNodeSizeSpin.setValue(8) gui.separator(hb) self.maxNodeSizeSpin = gui.spin(hb, self, "maxNodeSize", 10, 200, step=5, label="Max:", callback=self.set_node_sizes) self.maxNodeSizeSpin.setValue(50) gui.separator(hb) self.invertNodeSizeCheck = gui.checkBox(hb, self, "invertNodeSize", "Invert", callback=self.set_node_sizes) hb = gui.widgetBox(self.displayTab, box="Node labels | tooltips", orientation="horizontal", addSpace=False) self.attListBox = gui.listBox( hb, self, "node_label_attrs", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._on_node_label_attrs_changed) self.tooltipListBox = gui.listBox( hb, self, "tooltipAttributes", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._clicked_tooltip_lstbox) eb = gui.widgetBox(self.displayTab, "Edges", orientation="vertical") self.checkbox_relative_edges = gui.checkBox( eb, self, 'relativeEdgeWidths', 'Relative edge widths', callback=self.set_edge_sizes) self.checkbox_show_weights = gui.checkBox( eb, self, 'showEdgeWeights', 'Show edge weights', callback=self.set_edge_labels) ib = gui.widgetBox(self.markTab, "Info", orientation="vertical") gui.label(ib, self, "Nodes: %(number_of_nodes_label)i") gui.label(ib, self, "Selected: %(nSelected)i") gui.label(ib, self, "Highlighted: %(nHighlighted)i") def on_selection_change(): self.nSelected = len(self.view.getSelected()) self.nHighlighted = len(self.view.getHighlighted()) self.set_selection_mode() self.commit() self.view.selectionChanged.connect(on_selection_change) ib = gui.widgetBox(self.markTab, "Highlight nodes ...") ribg = gui.radioButtonsInBox(ib, self, "selectionMode", callback=self.set_selection_mode) gui.appendRadioButton(ribg, "None") gui.appendRadioButton(ribg, "... whose attributes contain:") self.ctrlMarkSearchString = gui.lineEdit( gui.indentedBox(ribg), self, "markSearchString", callback=self._set_search_string_timer, callbackOnType=True) self.searchStringTimer.timeout.connect(self.set_selection_mode) gui.appendRadioButton(ribg, "... neighbours of selected, ≤ N hops away") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkDistance = gui.spin( ib, self, "markDistance", 1, 100, 1, label="Hops:", callback=lambda: self.set_selection_mode(SelectionMode.NEIGHBORS)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with at least N connections") gui.appendRadioButton(ribg, "... with at most N connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNConnections = gui.spin( ib, self, "markNConnections", 0, 1000000, 1, label="Connections:", callback=lambda: self.set_selection_mode( SelectionMode.AT_MOST_N if self.selectionMode == SelectionMode. AT_MOST_N else SelectionMode.AT_LEAST_N)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with more connections than any neighbor") gui.appendRadioButton( ribg, "... with more connections than average neighbor") gui.appendRadioButton(ribg, "... with most connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNumber = gui.spin( ib, self, "markNBest", 1, 1000000, 1, label="Number of nodes:", callback=lambda: self.set_selection_mode(SelectionMode.MOST_CONN)) ib.layout().addStretch(1) self.markInputRadioButton = gui.appendRadioButton( ribg, "... from Node Subset input signal") self.markInputRadioButton.setEnabled(True) gui.auto_commit(ribg, self, 'do_auto_commit', 'Output changes') self.markTab.layout().addStretch(1) self.set_graph(None) self.set_selection_mode() def commit(self): self.send_data() @Inputs.node_distances def set_items_distance_matrix(self, matrix): assert matrix is None or isinstance(matrix, Orange.misc.DistMatrix) self.items_matrix = matrix self.relayout() def _set_search_string_timer(self): self.selectionMode = SelectionMode.SEARCH self.searchStringTimer.stop() self.searchStringTimer.start(300) def switchTab(self, index=None): index = index or self.tabs.currentIndex() curTab = self.tabs.widget(index) self.acceptingEnterKeypress = False if curTab == self.markTab and self.selectionMode != SelectionMode.NONE: self.acceptingEnterKeypress = True @non_reentrant def set_selection_mode(self, selectionMode=None): self.searchStringTimer.stop() selectionMode = self.selectionMode = selectionMode or self.selectionMode self.switchTab() if (self.graph is None or self.tabs.widget(self.tabs.currentIndex()) != self.markTab and selectionMode != SelectionMode.FROM_INPUT): return if selectionMode == SelectionMode.NONE: self.view.setHighlighted([]) elif selectionMode == SelectionMode.SEARCH: table, txt = self.graph.items(), self.markSearchString.lower() if not table or not txt: return toMark = set(i for i, instance in enumerate(table) if txt in " ".join(map(str, instance.list)).lower()) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.NEIGHBORS: selected = set(self.view.getSelected()) neighbors = selected.copy() for _ in range(self.markDistance): for neigh in list(neighbors): neighbors |= set(self.graph[neigh].keys()) neighbors -= selected self.view.setHighlighted(neighbors) elif selectionMode == SelectionMode.AT_LEAST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree >= self.markNConnections)) elif selectionMode == SelectionMode.AT_MOST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree <= self.markNConnections)) elif selectionMode == SelectionMode.ANY_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree > max(dict(self.graph.degree(self.graph[node])).values(), default=0))) elif selectionMode == SelectionMode.AVG_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree > np.nan_to_num( np.mean( list( dict(self.graph.degree( self.graph[node])).values()))))) elif selectionMode == SelectionMode.MOST_CONN: degrees = np.array( sorted(self.graph.degree(), key=lambda i: i[1], reverse=True)) cut_ind = max(1, min(self.markNBest, self.graph.number_of_nodes())) cut_degree = degrees[cut_ind - 1, 1] toMark = set(degrees[degrees[:, 1] >= cut_degree, 0]) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.FROM_INPUT: tomark = {} if self.markInputItems: ids = set(self.markInputItems.ids) tomark = { x for x in self.graph if self.graph.items()[x].id in ids } self.view.setHighlighted(tomark) def keyReleaseEvent(self, ev): """On Enter, expand the selected set with the highlighted""" if (not self.acceptingEnterKeypress or ev.key() not in (Qt.Key_Return, Qt.Key_Enter)): super().keyReleaseEvent(ev) return highlighted = self.view.getHighlighted() self.view.setSelected(highlighted, extend=True) self.view.setHighlighted([]) self.set_selection_mode() def save_network(self): # TODO: this was never reviewed since Orange2 if self.view is None or self.graph is None: return filename = QFileDialog.getSaveFileName( self, 'Save Network', '', 'NetworkX graph as Python pickle (*.gpickle)\n' 'NetworkX edge list (*.edgelist)\n' 'Pajek network (*.net *.pajek)\n' 'GML network (*.gml)') if filename: _, ext = os.path.splitext(filename) if not ext: filename += ".net" items = self.graph.items() for i in range(self.graph.number_of_nodes()): graph_node = self.graph.node[i] plot_node = self.networkCanvas.networkCurve.nodes()[i] if items is not None: ex = items[i] if 'x' in ex.domain: ex['x'] = plot_node.x() if 'y' in ex.domain: ex['y'] = plot_node.y() graph_node['x'] = plot_node.x() graph_node['y'] = plot_node.y() network.readwrite.write(self.graph, filename) def send_data(self): if not self.graph: for output in dir(self.Outputs): if not output.startswith('__'): getattr(self.Outputs, output).send(None) return selected = self.view.getSelected() self.Outputs.subgraph.send( self.graph.subgraph(selected) if selected else None) self.Outputs.unselected_subgraph.send( self.graph.subgraph(self.view.getUnselected() ) if selected else self.graph) self.Outputs.distances.send( self.items_matrix.submatrix(sorted(selected)) if self.items_matrix is not None and selected else None) items = self.graph.items() if not items: self.Outputs.selected.send(None) self.Outputs.highlighted.send(None) self.Outputs.remaining.send(None) else: highlighted = self.view.getHighlighted() self.Outputs.selected.send(items[ sorted(selected), :] if selected else None) self.Outputs.highlighted.send(items[ sorted(highlighted), :] if highlighted else None) remaining = sorted( set(self.graph) - set(selected) - set(highlighted)) self.Outputs.remaining.send(items[ remaining, :] if remaining else None) def _set_combos(self): self._clear_combos() self.graph_attrs = self.graph.items_vars() lastLabelColumns = self.lastLabelColumns lastTooltipColumns = self.lastTooltipColumns for var in self.graph_attrs: if var.is_discrete or var.is_continuous: self.colorCombo.addItem( gui.attributeIconDict[gui.vartype(var)], var.name, var) if var.is_continuous: self.nodeSizeCombo.addItem( gui.attributeIconDict[gui.vartype(var)], var.name, var) self.nodeSizeCombo.setDisabled(not self.graph_attrs) self.colorCombo.setDisabled(not self.graph_attrs) for i in range(self.nodeSizeCombo.count()): if self.lastVertexSizeColumn == \ self.nodeSizeCombo.itemText(i): self.node_size_attr = i self.set_node_sizes() break for i in range(self.colorCombo.count()): if self.lastColorColumn == self.colorCombo.itemText(i): self.node_color_attr = i self.set_node_colors() break if lastLabelColumns: selection = QItemSelection() model = self.attListBox.model() for i in range(self.attListBox.count()): if str(self.attListBox.item(i).text()) in lastLabelColumns: selection.append(QItemSelectionRange(model.index(i, 0))) selmodel = self.attListBox.selectionModel() selmodel.select(selection, selmodel.Select | selmodel.Clear) else: self.attListBox.selectionModel().clearSelection() self._on_node_label_attrs_changed() if lastTooltipColumns: selection = QItemSelection() model = self.tooltipListBox.model() for i in range(self.tooltipListBox.count()): if self.tooltipListBox.item(i).text() in lastTooltipColumns: selection.append(QItemSelectionRange(model.index(i, 0))) selmodel = self.tooltipListBox.selectionModel() selmodel.select(selection, selmodel.Select | selmodel.Clear) else: self.tooltipListBox.selectionModel().clearSelection() self._clicked_tooltip_lstbox() self.lastLabelColumns = lastLabelColumns self.lastTooltipColumns = lastTooltipColumns def _clear_combos(self): self.graph_attrs = [] self.colorCombo.clear() self.nodeSizeCombo.clear() self.colorCombo.addItem('(none)', None) self.nodeSizeCombo.addItem("(uniform)") def set_graph_none(self): self.graph = None self.graph_base = None self._clear_combos() self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self._items = None self.view.set_graph(None) @Inputs.network def set_graph(self, graph): if not graph: return self.set_graph_none() if graph.number_of_nodes() < 2: self.set_graph_none() self.information( 'I\'m not really in a mood to visualize just one node. Try again tomorrow.' ) return if graph.number_of_nodes() + graph.number_of_edges() > 30000: self.set_graph_none() self.Error.network_too_large() return self.information() all_edges_equal = bool( 1 == len(set(w for u, v, w in graph.edges(data='weight')))) self.checkbox_show_weights.setEnabled(not all_edges_equal) self.checkbox_relative_edges.setEnabled(not all_edges_equal) self.graph_base = graph self.graph = graph.copy() # Set items table from the separate signal if self._items: self.set_items(self._items) self.view.set_graph(self.graph, relayout=False) # Set labels self.number_of_nodes_label = self.graph.number_of_nodes() self.number_of_edges_label = self.graph.number_of_edges() self.verticesPerEdge = self.graph.number_of_nodes() / max( 1, self.graph.number_of_edges()) self.edgesPerVertex = self.graph.number_of_edges() / max( 1, self.graph.number_of_nodes()) self._set_combos() self.Error.clear() self.set_selection_mode() self.relayout() @Inputs.node_data def set_items(self, items=None): self._items = items if items is None: return self.set_graph(self.graph_base) if not self.graph: self.Warning.no_graph_found() return self.Warning.clear() if len(items) != self.graph.number_of_nodes(): self.Error.instance_for_each_node() return self.Error.instance_for_each_node.clear() self.graph.set_items(items) self._set_combos() @Inputs.node_subset def set_marking_items(self, items): self.markInputRadioButton.setEnabled(False) self.markInputItems = items self.Warning.clear() if items is None: self.view.selectionChanged.emit() return if self.graph is None or self.graph.items() is None: self.Warning.no_graph_or_items() return graph_items = self.graph.items() domain = graph_items.domain if len(items) > 0: commonVars = ( set(x.name for x in chain(items.domain.variables, items.domain.metas)) & set(x.name for x in chain(domain.variables, domain.metas))) self.markInputRadioButton.setEnabled(True) self.view.selectionChanged.emit() def relayout(self): if self.graph is None or self.graph.number_of_nodes() <= 1: return self.progressbar = gui.ProgressBar(self, FR_ITERATIONS) distmatrix = self.items_matrix if distmatrix is not None and distmatrix.shape[ 0] != self.graph.number_of_nodes(): self.Warning.distance_matrix_size() distmatrix = None self.Warning.distance_matrix_size.clear() self.relayout_button.setDisabled(True) self.view.relayout(randomize=False, weight=distmatrix) def _on_node_label_attrs_changed(self): if not self.graph: return attributes = self.lastLabelColumns = [ self.graph_attrs[i] for i in self.node_label_attrs ] if attributes: table = self.graph.items() if not table: return for i, node in enumerate(self.view.nodes): text = ', '.join(map(str, table[i, attributes][0].list)) node.setText(text) else: for node in self.view.nodes: node.setText('') def _clicked_tooltip_lstbox(self): if not self.graph: return attributes = self.lastTooltipColumns = [ self.graph_attrs[i] for i in self.tooltipAttributes ] if attributes: table = self.graph.items() if not table: return assert self.view.nodes for i, node in enumerate(self.view.nodes): node.setTooltip( lambda row=i, attributes=attributes, table=table: '<br>'. join('<b>{.name}:</b> {}'.format( i[0], str(i[1]).replace('<', '<')) for i in zip( attributes, table[row, attributes][0].list))) else: for node in self.view.nodes: node.setTooltip(None) def set_edge_labels(self): if self.showEdgeWeights: weights = (str(w or '') for u, v, w in self.graph.edges(data='weight')) else: weights = ('' for i in range(self.graph.number_of_edges())) for edge, weight in zip(self.view.edges, weights): edge.setText(weight) def set_node_colors(self): if not self.graph: return self.lastColorColumn = self.colorCombo.currentText() attribute = self.colorCombo.itemData(self.colorCombo.currentIndex()) assert not attribute or isinstance(attribute, Orange.data.Variable) if not attribute: for node in self.view.nodes: node.setColor(None) return table = self.graph.items() if not table: return if attribute in table.domain.class_vars: values = table[:, attribute].Y if values.ndim > 1: values = values.T elif attribute in table.domain.metas: values = table[:, attribute].metas[:, 0] elif attribute in table.domain.attributes: values = table[:, attribute].X[:, 0] else: raise RuntimeError("Shouldn't be able to select this column") if attribute.is_continuous: colors = CONTINUOUS_PALETTE[scale(values)] elif attribute.is_discrete: DISCRETE_PALETTE = ColorPaletteGenerator(len(attribute.values)) colors = DISCRETE_PALETTE[values] for node, color in zip(self.view.nodes, colors): node.setColor(color) def set_node_sizes(self): attribute = self.nodeSizeCombo.itemData( self.nodeSizeCombo.currentIndex()) depending_widgets = (self.invertNodeSizeCheck, self.maxNodeSizeSpin) for w in depending_widgets: w.setDisabled(not attribute) if not self.graph: return table = self.graph.items() if table is None: return try: values = table.get_column_view(attribute)[0] except Exception: for node in self.view.nodes: node.setSize(self.minNodeSize) return if self.invertNodeSize: values += np.nanmin(values) + 1 values = 1 / values nodemin, nodemax = np.nanmin(values), np.nanmax(values) if nodemin == nodemax: # np.polyfit borks on this condition sizes = (self.minNodeSize for i in range(len(self.view.nodes))) else: k, n = np.polyfit([nodemin, nodemax], [self.minNodeSize, self.maxNodeSize], 1) sizes = values * k + n sizes[np.isnan(sizes)] = np.nanmean(sizes) for node, size in zip(self.view.nodes, sizes): node.setSize(size) def set_edge_sizes(self): if not self.graph: return if self.relativeEdgeWidths: widths = [ self.graph.adj[u][v].get('weight', 1) for u, v in self.graph.edges() ] widths = scale(widths, .7, 8) else: widths = (.7 for i in range(self.graph.number_of_edges())) for edge, width in zip(self.view.edges, widths): edge.setSize(width) def send_report(self): self.report_data("Data", self.graph.items()) self.report_items('Graph info', [ ("Number of vertices", self.graph.number_of_nodes()), ("Number of edges", self.graph.number_of_edges()), ("Vertices per edge", "%.3f" % self.verticesPerEdge), ("Edges per vertex", "%.3f" % self.edgesPerVertex), ]) if self.node_color_attr or self.node_size_attr or self.node_label_attrs: self.report_items("Visual settings", [ ("Vertex color", self.colorCombo.currentText()), ("Vertex size", str(self.nodeSizeCombo.currentText()) + " (inverted)" if self.invertNodeSize else ""), ("Labels", ", ".join(self.graph_attrs[i].name for i in self.node_label_attrs)), ]) self.report_plot("Graph", self.view)
class OWMDS(OWDataProjectionWidget): name = "MDS" description = "Two-dimensional data projection by multidimensional " \ "scaling constructed from a distance matrix." icon = "icons/MDS.svg" keywords = ["multidimensional scaling", "multi dimensional scaling"] class Inputs(OWDataProjectionWidget.Inputs): distances = Input("Distances", DistMatrix) settings_version = 3 #: Initialization type PCA, Random, Jitter = 0, 1, 2 #: Refresh rate RefreshRate = [("Every iteration", 1), ("Every 5 steps", 5), ("Every 10 steps", 10), ("Every 25 steps", 25), ("Every 50 steps", 50), ("None", -1)] #: Runtime state Running, Finished, Waiting = 1, 2, 3 max_iter = settings.Setting(300) initialization = settings.Setting(PCA) refresh_rate = settings.Setting(3) GRAPH_CLASS = OWMDSGraph graph = SettingProvider(OWMDSGraph) embedding_variables_names = ("mds-x", "mds-y") class Error(OWDataProjectionWidget.Error): not_enough_rows = Msg("Input data needs at least 2 rows") matrix_too_small = Msg("Input matrix must be at least 2x2") no_attributes = Msg("Data has no attributes") mismatching_dimensions = \ Msg("Data and distances dimensions do not match.") out_of_memory = Msg("Out of memory") optimization_error = Msg("Error during optimization\n{}") def __init__(self): super().__init__() #: Input dissimilarity matrix self.matrix = None # type: Optional[DistMatrix] #: Data table from the `self.matrix.row_items` (if present) self.matrix_data = None # type: Optional[Table] #: Input data table self.signal_data = None self._invalidated = False self.embedding = None self.effective_matrix = None self.__update_loop = None # timer for scheduling updates self.__timer = QTimer(self, singleShot=True, interval=0) self.__timer.timeout.connect(self.__next_step) self.__state = OWMDS.Waiting self.__in_next_step = False self.graph.pause_drawing_pairs() g = self.graph.gui self.size_model = g.points_models[2] self.size_model.order = g.points_models[2].order[:1] + ("Stress", ) + \ g.points_models[2].order[1:] # self._initialize() def _add_controls(self): self._add_controls_optimization() super()._add_controls() self.graph.gui.add_control(self._effects_box, gui.hSlider, "Show similar pairs:", master=self.graph, value="connected_pairs", minValue=0, maxValue=20, createLabel=False, callback=self._on_connected_changed) def _add_controls_optimization(self): box = gui.vBox(self.controlArea, box=True) self.runbutton = gui.button(box, self, "Run optimization", callback=self._toggle_run) gui.comboBox(box, self, "refresh_rate", label="Refresh: ", orientation=Qt.Horizontal, items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh) hbox = gui.hBox(box, margin=0) gui.button(hbox, self, "PCA", callback=self.do_PCA) gui.button(hbox, self, "Randomize", callback=self.do_random) gui.button(hbox, self, "Jitter", callback=self.do_jitter) def set_data(self, data): """Set the input dataset. Parameters ---------- data : Optional[Table] """ if data is not None and len(data) < 2: self.Error.not_enough_rows() data = None else: self.Error.not_enough_rows.clear() self.signal_data = data if self.matrix is not None and data is not None and \ len(self.matrix) == len(data): self.closeContext() self.data = data self.init_attr_values() self.openContext(data) else: self._invalidated = True @Inputs.distances def set_disimilarity(self, matrix): """Set the dissimilarity (distance) matrix. Parameters ---------- matrix : Optional[Orange.misc.DistMatrix] """ if matrix is not None and len(matrix) < 2: self.Error.matrix_too_small() matrix = None else: self.Error.matrix_too_small.clear() self.matrix = matrix self.matrix_data = matrix.row_items if matrix is not None else None self._invalidated = True def clear(self): super().clear() self.embedding = None self.effective_matrix = None self.graph.set_effective_matrix(None) self.__set_update_loop(None) self.__state = OWMDS.Waiting def _initialize(self): self.closeContext() self.clear() self.clear_messages() # if no data nor matrix is present reset plot if self.signal_data is None and self.matrix is None: self.data = None self.init_attr_values() return if self.signal_data is not None and self.matrix is not None and \ len(self.signal_data) != len(self.matrix): self.Error.mismatching_dimensions() self.init_attr_values() return if self.signal_data is not None: self.data = self.signal_data elif self.matrix_data is not None: self.data = self.matrix_data if self.matrix is not None: self.effective_matrix = self.matrix if self.matrix.axis == 0 and self.data is self.matrix_data: self.data = None elif self.data.domain.attributes: preprocessed_data = MDS().preprocess(self.data) self.effective_matrix = Euclidean(preprocessed_data) else: self.Error.no_attributes() self.init_attr_values() return self.init_attr_values() self.openContext(self.data) self.graph.set_effective_matrix(self.effective_matrix) def _toggle_run(self): if self.__state == OWMDS.Running: self.stop() self._invalidate_output() else: self.start() def start(self): if self.__state == OWMDS.Running: return elif self.__state == OWMDS.Finished: # Resume/continue from a previous run self.__start() elif self.__state == OWMDS.Waiting and \ self.effective_matrix is not None: self.__start() def stop(self): if self.__state == OWMDS.Running: self.__set_update_loop(None) def __start(self): self.graph.pause_drawing_pairs() X = self.effective_matrix init = self.embedding # number of iterations per single GUI update step _, step_size = OWMDS.RefreshRate[self.refresh_rate] if step_size == -1: step_size = self.max_iter def update_loop(X, max_iter, step, init): """ return an iterator over successive improved MDS point embeddings. """ # NOTE: this code MUST NOT call into QApplication.processEvents done = False iterations_done = 0 oldstress = np.finfo(np.float).max init_type = "PCA" if self.initialization == OWMDS.PCA else "random" while not done: step_iter = min(max_iter - iterations_done, step) mds = MDS(dissimilarity="precomputed", n_components=2, n_init=1, max_iter=step_iter, init_type=init_type, init_data=init) mdsfit = mds(X) iterations_done += step_iter embedding, stress = mdsfit.embedding_, mdsfit.stress_ stress /= np.sqrt(np.sum(embedding**2, axis=1)).sum() if iterations_done >= max_iter: done = True elif (oldstress - stress) < mds.params["eps"]: done = True init = embedding oldstress = stress yield embedding, mdsfit.stress_, iterations_done / max_iter self.__set_update_loop(update_loop(X, self.max_iter, step_size, init)) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): """ Set the update `loop` coroutine. The `loop` is a generator yielding `(embedding, stress, progress)` tuples where `embedding` is a `(N, 2) ndarray` of current updated MDS points, `stress` is the current stress and `progress` a float ratio (0 <= progress <= 1) If an existing update coroutine loop is already in place it is interrupted (i.e. closed). .. note:: The `loop` must not explicitly yield control flow to the event loop (i.e. call `QApplication.processEvents`) """ if self.__update_loop is not None: self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.setBlocking(True) self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWMDS.Running self.__timer.start() else: self.setBlocking(False) self.setStatusMessage("") self.runbutton.setText("Start") self.__state = OWMDS.Finished self.__timer.stop() def __next_step(self): if self.__update_loop is None: return assert not self.__in_next_step self.__in_next_step = True loop = self.__update_loop self.Error.out_of_memory.clear() try: embedding, _, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__set_update_loop(None) self.unconditional_commit() self.graph.resume_drawing_pairs() self.graph.update_coordinates() except MemoryError: self.Error.out_of_memory() self.__set_update_loop(None) self.graph.resume_drawing_pairs() except Exception as exc: self.Error.optimization_error(str(exc)) self.__set_update_loop(None) self.graph.resume_drawing_pairs() else: self.progressBarSet(100.0 * progress, processEvents=None) self.embedding = embedding self.graph.update_coordinates() # schedule next update self.__timer.start() self.__in_next_step = False def do_PCA(self): self.__invalidate_embedding(self.PCA) def do_random(self): self.__invalidate_embedding(self.Random) def do_jitter(self): self.__invalidate_embedding(self.Jitter) def __invalidate_embedding(self, initialization=PCA): def jitter_coord(part): span = np.max(part) - np.min(part) part += np.random.uniform(-span / 20, span / 20, len(part)) # reset/invalidate the MDS embedding, to the default initialization # (Random or PCA), restarting the optimization if necessary. state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) if self.effective_matrix is None: return X = self.effective_matrix if initialization == OWMDS.PCA: self.embedding = torgerson(X) elif initialization == OWMDS.Random: self.embedding = np.random.rand(len(X), 2) else: jitter_coord(self.embedding[:, 0]) jitter_coord(self.embedding[:, 1]) self.setup_plot() # restart the optimization if it was interrupted. if state == OWMDS.Running: self.__start() def __invalidate_refresh(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) # restart the optimization if it was interrupted. # TODO: decrease the max iteration count by the already # completed iterations count. if state == OWMDS.Running: self.__start() def handleNewSignals(self): if self._invalidated: self.graph.pause_drawing_pairs() self._invalidated = False self._initialize() self.__invalidate_embedding() self.cb_class_density.setEnabled(self.can_draw_density()) self.start() super().handleNewSignals() def _invalidate_output(self): self.commit() def _on_connected_changed(self): self.graph.set_effective_matrix(self.effective_matrix) self.graph.update_pairs(reconnect=True) def setup_plot(self): super().setup_plot() if self.embedding is not None: self.graph.update_pairs(reconnect=True) def get_size_data(self): if self.attr_size == "Stress": return stress(self.embedding, self.effective_matrix) else: return super().get_size_data() def get_embedding(self): self.valid_data = np.ones(len(self.embedding), dtype=bool) \ if self.embedding is not None else None return self.embedding def _get_projection_data(self): if self.embedding is None: return None if self.data is None: x_name, y_name = self.embedding_variables_names variables = ContinuousVariable(x_name), ContinuousVariable(y_name) return Table(Domain(variables), self.embedding) return super()._get_projection_data() @classmethod def migrate_settings(cls, settings_, version): if version < 2: settings_graph = {} for old, new in (("label_only_selected", "label_only_selected"), ("symbol_opacity", "alpha_value"), ("symbol_size", "point_width"), ("jitter", "jitter_size")): settings_graph[new] = settings_[old] settings_["graph"] = settings_graph settings_["auto_commit"] = settings_["autocommit"] if version < 3: if "connected_pairs" in settings_: connected_pairs = settings_["connected_pairs"] settings_["graph"]["connected_pairs"] = connected_pairs @classmethod def migrate_context(cls, context, version): if version < 2: domain = context.ordered_domain n_domain = [t for t in context.ordered_domain if t[1] == 2] c_domain = [t for t in context.ordered_domain if t[1] == 1] context_values = {} for _, old_val, new_val in ((domain, "color_value", "attr_color"), (c_domain, "shape_value", "attr_shape"), (n_domain, "size_value", "attr_size"), (domain, "label_value", "attr_label")): tmp = context.values[old_val] if tmp[1] >= 0: context_values[new_val] = (tmp[0], tmp[1] + 100) elif tmp[0] != "Stress": context_values[new_val] = None else: context_values[new_val] = tmp context.values = context_values if version < 3 and "graph" in context.values: values = context.values values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"]
class RotaryEncoderModuleGUI(RotaryEncoderModule, BaseWidget): TITLE = 'Rotary encoder module' def __init__(self, parent_win=None): BaseWidget.__init__(self, self.TITLE, parent_win=parent_win) RotaryEncoderModule.__init__(self) self._port = ControlCombo( 'Serial port', changed_event=self.__combo_serial_ports_changed_evt) self._refresh_serial_ports = ControlButton( '', icon=QtGui.QIcon(conf.REFRESH_SMALL_ICON), default=self.__refresh_serial_ports_btn_pressed, helptext="Press here to refresh the list of available devices.") self._connect_btn = ControlButton('Connect', checkable=True) self._filename = ControlText('Stream Filename', '') self._saveas_btn = ControlButton('Save As...') self._events = ControlCheckBox('Enable events') self._output_stream = ControlCheckBox('Output stream') self._stream = ControlCheckBox('Stream data') self._stream_file = ControlCheckBox('Stream to file') self._zero_btn = ControlButton('Reset position') self._start_reading = ControlButton('Start Reading') self._reset_threshs = ControlButton('Reset thresholds') self._thresh_lower = ControlNumber('Lower threshold (deg)', 0, minimum=-360, maximum=360) self._thresh_upper = ControlNumber('Upper threshold (deg)', 0, minimum=-360, maximum=360) self._graph = ControlMatplotlib('Value') self._clear_btn = ControlButton('Clear') self.set_margin(10) self.formset = [('_port', '_refresh_serial_ports', '_connect_btn'), ('_filename', '_saveas_btn'), ('_events', '_output_stream', '_stream', '_stream_file', '_zero_btn'), '_start_reading', ('_thresh_lower', '_thresh_upper', '_reset_threshs'), '=', '_graph', '_clear_btn'] self._stream.enabled = False self._stream_file.enabled = False self._events.enabled = False self._output_stream.enabled = False self._zero_btn.enabled = False self._reset_threshs.enabled = False self._thresh_lower.enabled = False self._thresh_upper.enabled = False self._start_reading.enabled = False self._connect_btn.value = self.__toggle_connection_evt self._saveas_btn.value = self.__prompt_savig_evt self._stream_file.changed_event = self.__stream_file_changed_evt self._events.changed_event = self.__events_changed_evt self._output_stream.changed_event = self.__output_stream_changed_evt self._thresh_upper.changed_event = self.__thresh_evt self._thresh_lower.changed_event = self.__thresh_evt self._reset_threshs.value = self.__reset_thresholds_evt self._zero_btn.value = self.__zero_btn_evt self._start_reading.value = self.__start_reading_evt self._graph.on_draw = self.__on_draw_evt self._clear_btn.value = self.__clear_btn_evt self._filename.changed_event = self.__filename_changed_evt self.history_x = [] self.history_y = [] self._timer = QTimer() self._timer.timeout.connect(self.__update_readings) self._fill_serial_ports() def _fill_serial_ports(self): self._port.add_item('', '') for n, port in enumerate(sorted(serial.tools.list_ports.comports()), 1): self._port.add_item("{device}".format(device=port.device), str(port.device)) def __filename_changed_evt(self): if not self._filename.value: self._stream_file.value = False self._stream_file.enabled = False def __prompt_savig_evt(self): ''' Opens a window for user to select where to save the csv file ''' self._filename.value, _ = QFileDialog.getSaveFileName() if self._filename.value: self._stream_file.enabled = True else: self._stream_file.value = False self._stream_file.enabled = False def __stream_file_changed_evt(self): ''' User wants to store rotary encoder measurements in a CSV file. Create it ''' if self._stream_file.value is True: self._csvfile = open(self._filename.value, 'w') self._csvwriter = csv.writer( self._csvfile, def_text= 'This file has all the rotary encoder data recorded during a PyBpod session.', columns_headers=['PC_TIME', 'DATA_TYPE', 'EVT_TIME', 'VALUE' ]) # Check if we need something else after def __start_reading_evt(self): ''' Toggle timer ''' if self._timer.isActive(): self.disable_stream() self._start_reading.label = 'Start Reading' self._timer.stop() else: self.enable_stream() self.history_x = [] self.history_y = [] self._start_reading.label = 'Stop Reading' self._timer.start(30) def __clear_btn_evt(self): ''' Clear recorded data ''' self.history_x = [] self.history_y = [] self._graph.draw() def __on_draw_evt(self, figure): ''' The actual draw function. Pick just the last 200 measurements in order to avoid app freezing ''' axes = figure.add_subplot(111) axes.clear() totallen = len(self.history_x) if totallen > 200: x = self.history_x[totallen - 201:] y = self.history_y[totallen - 201:] axes.plot(x, y) if len(x) >= 2: x_range = [x[0], x[-1]] axes.plot(x_range, [self._thresh_upper.value, self._thresh_upper.value], linestyle='dotted', color='red') axes.plot(x_range, [self._thresh_lower.value, self._thresh_lower.value], linestyle='dotted', color='blue') else: axes.plot(self.history_x, self.history_y) if len(self.history_x) >= 2: x_range = [self.history_x[0], self.history_x[-1]] axes.plot(x_range, [self._thresh_upper.value, self._thresh_upper.value], linestyle='dotted', color='red') axes.plot(x_range, [self._thresh_lower.value, self._thresh_lower.value], linestyle='dotted', color='blue') self._graph.repaint() def __update_graph(self, readings): ''' Add new data to the reading history and update the graph ''' for data in readings: if data[0] == 'P': self.history_x.append(data[1]) self.history_y.append(data[2]) self._graph.draw() def __update_readings(self): ''' Get new measurements and channel them to the graph or the file being written ''' data = self.read_stream() if self._stream.value: self.__update_graph(data) if self._stream_file.value: self.__write_to_file(data) def __write_to_file(self, readings): ''' Write new readings to the file ''' now = datetime_now.now() for data in readings: self._csvwriter.writerow([now.strftime('%Y%m%d%H%M%S')] + data) def __zero_btn_evt(self): self.set_zero_position() def __reset_thresholds_evt(self): self._thresh_lower.value = 0 self._thresh_upper.value = 0 def __thresh_evt(self): thresholds = [ int(self._thresh_lower.value), int(self._thresh_upper.value) ] self.set_thresholds(thresholds) def __events_changed_evt(self): if self._stream.value: self.enable_evt_transmission() else: self.disable_evt_transmission() def __output_stream_changed_evt(self): if self._stream.value: self.enable_module_outputstream() else: self.disable_module_outputstream() def __toggle_connection_evt(self): if not self._connect_btn.checked: if hasattr(self, 'arcom'): self.disable_stream() self._timer.stop() self.close() self._connect_btn.label = 'Connect' self._stream.enabled = False self._events.enabled = False self._output_stream.enabled = False self._zero_btn.enabled = False self._reset_threshs.enabled = False self._thresh_lower.enabled = False self._thresh_upper.enabled = False self._start_reading.enabled = False self._stream_file.enabled = False self._port.enabled = True self._refresh_serial_ports.enabled = True else: try: self.open(self._port.value) self._connect_btn.label = 'Disconnect' self._stream.enabled = True self._events.enabled = True self._output_stream.enabled = True self._zero_btn.enabled = True self._reset_threshs.enabled = True self._thresh_lower.enabled = True self._thresh_upper.enabled = True self._start_reading.enabled = True self._port.enabled = False self._refresh_serial_ports.enabled = False if self._filename.value: self._stream_file.enabled = True else: self._stream_file.value = False self._stream_file.enabled = False except Exception as err: self.critical(str(err), "Error") self._connect_btn.checked = False def __combo_serial_ports_changed_evt(self): self._connect_btn.enabled = True def __refresh_serial_ports_btn_pressed(self): tmp = self._port.value self._port.clear() self._fill_serial_ports() self._port.value = tmp
class OWSelectAttributes(widget.OWWidget): # pylint: disable=too-many-instance-attributes name = "Select Columns" description = "Select columns from the data table and assign them to " \ "data features, classes or meta variables." icon = "icons/SelectColumns.svg" priority = 100 keywords = ["filter"] class Inputs: data = Input("Data", Table, default=True) features = Input("Features", AttributeList) class Outputs: data = Output("Data", Table) features = Output("Features", AttributeList, dynamic=False) want_main_area = False want_control_area = True settingsHandler = SelectAttributesDomainContextHandler() domain_role_hints = ContextSetting({}) use_input_features = Setting(False) auto_commit = Setting(True) class Warning(widget.OWWidget.Warning): mismatching_domain = Msg("Features and data domain do not match") def __init__(self): super().__init__() self.data = None self.features = None # Schedule interface updates (enabled buttons) using a coalescing # single shot timer (complex interactions on selection and filtering # updates in the 'available_attrs_view') self.__interface_update_timer = QTimer(self, interval=0, singleShot=True) self.__interface_update_timer.timeout.connect( self.__update_interface_state) # The last view that has the selection for move operation's source self.__last_active_view = None # type: Optional[QListView] def update_on_change(view): # Schedule interface state update on selection change in `view` self.__last_active_view = view self.__interface_update_timer.start() self.controlArea = QWidget(self.controlArea) self.layout().addWidget(self.controlArea) layout = QGridLayout() self.controlArea.setLayout(layout) layout.setContentsMargins(4, 4, 4, 4) box = gui.vBox(self.controlArea, "Available Variables", addToLayout=False) self.available_attrs = VariablesListItemModel() filter_edit, self.available_attrs_view = variables_filter( parent=self, model=self.available_attrs) box.layout().addWidget(filter_edit) def dropcompleted(action): if action == Qt.MoveAction: self.commit() self.available_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.available_attrs_view)) self.available_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.available_attrs_view) layout.addWidget(box, 0, 0, 3, 1) box = gui.vBox(self.controlArea, "Features", addToLayout=False) self.used_attrs = VariablesListItemModel() filter_edit, self.used_attrs_view = variables_filter( parent=self, model=self.used_attrs, accepted_type=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.used_attrs.rowsInserted.connect(self.__used_attrs_changed) self.used_attrs.rowsRemoved.connect(self.__used_attrs_changed) self.used_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.used_attrs_view)) self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.use_features_box = gui.auto_commit( self.controlArea, self, "use_input_features", "Use input features", "Always use input features", box=False, commit=self.__use_features_clicked, callback=self.__use_features_changed, addToLayout=False ) self.enable_use_features_box() box.layout().addWidget(self.use_features_box) box.layout().addWidget(filter_edit) box.layout().addWidget(self.used_attrs_view) layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False) self.class_attrs = VariablesListItemModel() self.class_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) self.class_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.class_attrs_view)) self.class_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.class_attrs_view.setMaximumHeight(72) box.layout().addWidget(self.class_attrs_view) layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False) self.meta_attrs = VariablesListItemModel() self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) self.meta_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.meta_attrs_view)) self.meta_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.meta_attrs_view) layout.addWidget(box, 2, 2, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 0, 1, 1, 1) self.up_attr_button = gui.button(bbox, self, "Up", callback=partial(self.move_up, self.used_attrs_view)) self.move_attr_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.used_attrs_view) ) self.down_attr_button = gui.button(bbox, self, "Down", callback=partial(self.move_down, self.used_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 1, 1, 1, 1) self.up_class_button = gui.button(bbox, self, "Up", callback=partial(self.move_up, self.class_attrs_view)) self.move_class_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.class_attrs_view, exclusive=False) ) self.down_class_button = gui.button(bbox, self, "Down", callback=partial(self.move_down, self.class_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 2, 1, 1, 1) self.up_meta_button = gui.button(bbox, self, "Up", callback=partial(self.move_up, self.meta_attrs_view)) self.move_meta_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.meta_attrs_view) ) self.down_meta_button = gui.button(bbox, self, "Down", callback=partial(self.move_down, self.meta_attrs_view)) autobox = gui.auto_commit(None, self, "auto_commit", "Send") layout.addWidget(autobox, 3, 0, 1, 3) reset = gui.button(None, self, "Reset", callback=self.reset, width=120) autobox.layout().insertWidget(0, reset) autobox.layout().insertStretch(1, 20) layout.setRowStretch(0, 4) layout.setRowStretch(1, 0) layout.setRowStretch(2, 2) layout.setHorizontalSpacing(0) self.controlArea.setLayout(layout) self.output_data = None self.original_completer_items = [] self.resize(600, 600) @property def features_from_data_attributes(self): if self.data is None or self.features is None: return [] domain = self.data.domain return [domain[feature.name] for feature in self.features if feature.name in domain and domain[feature.name] in domain.attributes] def can_use_features(self): return bool(self.features_from_data_attributes) and \ self.features_from_data_attributes != self.used_attrs[:] def __use_features_changed(self): # Use input features check box # Needs a check since callback is invoked before object is created if not hasattr(self, "use_features_box"): return self.enable_used_attrs(not self.use_input_features) if self.use_input_features and self.can_use_features(): self.use_features() if not self.use_input_features: self.enable_use_features_box() def __use_features_clicked(self): # Use input features button self.use_features() def __used_attrs_changed(self): self.enable_use_features_box() @Inputs.data def set_data(self, data=None): self.update_domain_role_hints() self.closeContext() self.data = data if data is not None: self.openContext(data) all_vars = data.domain.variables + data.domain.metas var_sig = lambda attr: (attr.name, vartype(attr)) domain_hints = {var_sig(attr): ("attribute", i) for i, attr in enumerate(data.domain.attributes)} domain_hints.update({var_sig(attr): ("meta", i) for i, attr in enumerate(data.domain.metas)}) if data.domain.class_vars: domain_hints.update( {var_sig(attr): ("class", i) for i, attr in enumerate(data.domain.class_vars)}) # update the hints from context settings domain_hints.update(self.domain_role_hints) attrs_for_role = lambda role: [ (domain_hints[var_sig(attr)][1], attr) for attr in all_vars if domain_hints[var_sig(attr)][0] == role] attributes = [ attr for place, attr in sorted(attrs_for_role("attribute"), key=lambda a: a[0])] classes = [ attr for place, attr in sorted(attrs_for_role("class"), key=lambda a: a[0])] metas = [ attr for place, attr in sorted(attrs_for_role("meta"), key=lambda a: a[0])] available = [ attr for place, attr in sorted(attrs_for_role("available"), key=lambda a: a[0])] self.used_attrs[:] = attributes self.class_attrs[:] = classes self.meta_attrs[:] = metas self.available_attrs[:] = available else: self.used_attrs[:] = [] self.class_attrs[:] = [] self.meta_attrs[:] = [] self.available_attrs[:] = [] def update_domain_role_hints(self): """ Update the domain hints to be stored in the widgets settings. """ hints_from_model = lambda role, model: [ ((attr.name, vartype(attr)), (role, i)) for i, attr in enumerate(model)] hints = dict(hints_from_model("available", self.available_attrs)) hints.update(hints_from_model("attribute", self.used_attrs)) hints.update(hints_from_model("class", self.class_attrs)) hints.update(hints_from_model("meta", self.meta_attrs)) self.domain_role_hints = hints @Inputs.features def set_features(self, features): self.features = features def handleNewSignals(self): self.check_data() self.enable_used_attrs() self.enable_use_features_box() if self.use_input_features and len(self.features_from_data_attributes): self.enable_used_attrs(False) self.use_features() self.unconditional_commit() def check_data(self): self.Warning.mismatching_domain.clear() if self.data is not None and self.features is not None and \ not len(self.features_from_data_attributes): self.Warning.mismatching_domain() def enable_used_attrs(self, enable=True): self.up_attr_button.setEnabled(enable) self.move_attr_button.setEnabled(enable) self.down_attr_button.setEnabled(enable) self.used_attrs_view.setEnabled(enable) self.used_attrs_view.repaint() def enable_use_features_box(self): self.use_features_box.button.setEnabled(self.can_use_features()) enable_checkbox = bool(self.features_from_data_attributes) self.use_features_box.setHidden(not enable_checkbox) self.use_features_box.repaint() def use_features(self): attributes = self.features_from_data_attributes available, used = self.available_attrs[:], self.used_attrs[:] self.available_attrs[:] = [attr for attr in used + available if attr not in attributes] self.used_attrs[:] = attributes self.commit() def selected_rows(self, view): """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() model = view.model() if isinstance(model, QSortFilterProxyModel): rows = [model.mapToSource(r) for r in rows] return [r.row() for r in rows] def move_rows(self, view, rows, offset): model = view.model() newrows = [min(max(0, row + offset), len(model) - 1) for row in rows] for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): model[row], model[newrow] = model[newrow], model[row] selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select( selection, QItemSelectionModel.ClearAndSelect) self.commit() def move_up(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, -1) def move_down(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, 1) def move_selected(self, view, exclusive=False): if self.selected_rows(view): self.move_selected_from_to(view, self.available_attrs_view) elif self.selected_rows(self.available_attrs_view): self.move_selected_from_to(self.available_attrs_view, view, exclusive) def move_selected_from_to(self, src, dst, exclusive=False): self.move_from_to(src, dst, self.selected_rows(src), exclusive) def move_from_to(self, src, dst, rows, exclusive=False): src_model = source_model(src) attrs = [src_model[r] for r in rows] for s1, s2 in reversed(list(slices(rows))): del src_model[s1:s2] dst_model = source_model(dst) dst_model.extend(attrs) self.commit() def __update_interface_state(self): last_view = self.__last_active_view if last_view is not None: self.update_interface_state(last_view) def update_interface_state(self, focus=None, selected=None, deselected=None): for view in [self.available_attrs_view, self.used_attrs_view, self.class_attrs_view, self.meta_attrs_view]: if view is not focus and not view.hasFocus() \ and view.selectionModel().hasSelection(): view.selectionModel().clear() def selected_vars(view): model = source_model(view) return [model[i] for i in self.selected_rows(view)] available_selected = selected_vars(self.available_attrs_view) attrs_selected = selected_vars(self.used_attrs_view) class_selected = selected_vars(self.class_attrs_view) meta_selected = selected_vars(self.meta_attrs_view) available_types = set(map(type, available_selected)) all_primitive = all(var.is_primitive() for var in available_types) move_attr_enabled = \ ((available_selected and all_primitive) or attrs_selected) and \ self.used_attrs_view.isEnabled() self.move_attr_button.setEnabled(bool(move_attr_enabled)) if move_attr_enabled: self.move_attr_button.setText(">" if available_selected else "<") move_class_enabled = (all_primitive and available_selected) or class_selected self.move_class_button.setEnabled(bool(move_class_enabled)) if move_class_enabled: self.move_class_button.setText(">" if available_selected else "<") move_meta_enabled = available_selected or meta_selected self.move_meta_button.setEnabled(bool(move_meta_enabled)) if move_meta_enabled: self.move_meta_button.setText(">" if available_selected else "<") self.__last_active_view = None self.__interface_update_timer.stop() def commit(self): self.update_domain_role_hints() if self.data is not None: attributes = list(self.used_attrs) class_var = list(self.class_attrs) metas = list(self.meta_attrs) domain = Orange.data.Domain(attributes, class_var, metas) newdata = self.data.transform(domain) self.output_data = newdata self.Outputs.data.send(newdata) self.Outputs.features.send(AttributeList(attributes)) else: self.output_data = None self.Outputs.data.send(None) self.Outputs.features.send(None) def reset(self): self.enable_used_attrs() self.use_features_box.checkbox.setChecked(False) if self.data is not None: self.available_attrs[:] = [] self.used_attrs[:] = self.data.domain.attributes self.class_attrs[:] = self.data.domain.class_vars self.meta_attrs[:] = self.data.domain.metas self.update_domain_role_hints() self.commit() def send_report(self): if not self.data or not self.output_data: return in_domain, out_domain = self.data.domain, self.output_data.domain self.report_domain("Input data", self.data.domain) if (in_domain.attributes, in_domain.class_vars, in_domain.metas) == ( out_domain.attributes, out_domain.class_vars, out_domain.metas): self.report_paragraph("Output data", "No changes.") else: self.report_domain("Output data", self.output_data.domain) diff = list(set(in_domain.variables + in_domain.metas) - set(out_domain.variables + out_domain.metas)) if diff: text = "%i (%s)" % (len(diff), ", ".join(x.name for x in diff)) self.report_items((("Removed", text),))
class OWGenes(OWWidget): name = "Genes" description = "Tool for working with genes" icon = "../widgets/icons/OWGeneInfo.svg" priority = 5 want_main_area = True selected_organism: int = Setting(11) search_pattern: str = Setting('') exclude_unmatched = Setting(True) replace_id_with_symbol = Setting(True) auto_commit = Setting(True) settingsHandler = DomainContextHandler() selected_gene_col = ContextSetting(None) use_attr_names = ContextSetting(True) replaces = [ 'orangecontrib.bioinformatics.widgets.OWGeneNameMatcher.OWGeneNameMatcher' ] class Inputs: data_table = Input("Data", Table) class Outputs: data_table = Output("Data", Table) gene_matcher_results = Output("Genes", Table) class Information(OWWidget.Information): pass def sizeHint(self): return QSize(1280, 960) def __init__(self): super().__init__() # ATTRIBUTES # self.target_database = NCBI_ID # input data self.input_data = None self.input_genes = None self.tax_id = None self.column_candidates = [] # input options self.organisms = [] # gene matcher self.gene_matcher = None # threads self.threadpool = QThreadPool(self) self.workers = None # progress bar self.progress_bar = None self._timer = QTimer() self._timer.timeout.connect(self._apply_filter) self._timer.setSingleShot(True) # GUI SECTION # # Control area self.info_box = widgetLabel( widgetBox(self.controlArea, "Info", addSpace=True), 'No data on input.\n') organism_box = vBox(self.controlArea, 'Organism') self.organism_select_combobox = comboBox( organism_box, self, 'selected_organism', callback=self.on_input_option_change) self.get_available_organisms() self.organism_select_combobox.setCurrentIndex(self.selected_organism) box = widgetBox(self.controlArea, 'Gene IDs in the input data') self.gene_columns_model = itemmodels.DomainModel( valid_types=(StringVariable, DiscreteVariable)) self.gene_column_combobox = comboBox( box, self, 'selected_gene_col', label='Stored in data column', model=self.gene_columns_model, sendSelectedValue=True, callback=self.on_input_option_change) self.attr_names_checkbox = checkBox( box, self, 'use_attr_names', 'Stored as feature (column) names', disables=[(-1, self.gene_column_combobox)], callback=self.on_input_option_change) self.gene_column_combobox.setDisabled(bool(self.use_attr_names)) output_box = vBox(self.controlArea, 'Output') # separator(output_box) # output_box.layout().addWidget(horizontal_line()) # separator(output_box) self.exclude_radio = checkBox(output_box, self, 'exclude_unmatched', 'Exclude unmatched genes', callback=self.commit) self.replace_radio = checkBox(output_box, self, 'replace_id_with_symbol', 'Replace feature IDs with gene names', callback=self.commit) auto_commit(self.controlArea, self, "auto_commit", "&Commit", box=False) rubber(self.controlArea) # Main area self.filter = lineEdit(self.mainArea, self, 'search_pattern', 'Filter:', callbackOnType=True, callback=self.handle_filter_callback) # rubber(self.radio_group) self.mainArea.layout().addWidget(self.filter) # set splitter self.splitter = QSplitter() self.splitter.setOrientation(Qt.Vertical) self.table_model = GeneInfoModel() self.table_view = QTableView() self.table_view.setAlternatingRowColors(True) self.table_view.viewport().setMouseTracking(True) self.table_view.setSortingEnabled(True) self.table_view.setShowGrid(False) self.table_view.verticalHeader().hide() # self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.unknown_model = UnknownGeneInfoModel() self.unknown_view = QTableView() self.unknown_view.setModel(self.unknown_model) self.unknown_view.verticalHeader().hide() self.unknown_view.setShowGrid(False) self.unknown_view.setSelectionMode(QAbstractItemView.NoSelection) self.unknown_view.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) self.splitter.addWidget(self.table_view) self.splitter.addWidget(self.unknown_view) self.splitter.setStretchFactor(0, 90) self.splitter.setStretchFactor(1, 10) self.mainArea.layout().addWidget(self.splitter) def handle_filter_callback(self): self._timer.stop() self._timer.start(500) def _apply_filter(self): # filter only if input data is present and model is populated if self.table_model.table is not None: self.table_model.update_model( filter_pattern=str(self.search_pattern)) self.commit() def __reset_widget_state(self): self.table_view.clearSpans() self.table_view.setModel(None) self.table_model.clear() self.unknown_model.clear() self._update_info_box() def _update_info_box(self): if self.input_genes and self.gene_matcher: num_genes = len(self.gene_matcher.genes) known_genes = len(self.gene_matcher.get_known_genes()) info_text = '{} genes in input data\n' \ '{} genes match Entrez database\n' \ '{} genes with match conflicts\n'.format(num_genes, known_genes, num_genes - known_genes) else: info_text = 'No data on input.' self.info_box.setText(info_text) def _progress_advance(self): # GUI should be updated in main thread. That's why we are calling advance method here if self.progress_bar: self.progress_bar.advance() def _handle_matcher_results(self): assert threading.current_thread() == threading.main_thread() if self.progress_bar: self.progress_bar.finish() self.setStatusMessage('') # update info box self._update_info_box() # set output options self.toggle_radio_options() # set known genes self.table_model.initialize(self.gene_matcher.genes) self.table_view.setModel(self.table_model) self.table_view.selectionModel().selectionChanged.connect(self.commit) self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows) self.table_view.setItemDelegateForColumn( self.table_model.entrez_column_index, LinkStyledItemDelegate(self.table_view)) v_header = self.table_view.verticalHeader() option = self.table_view.viewOptions() size = self.table_view.style().sizeFromContents( QStyle.CT_ItemViewItem, option, QSize(20, 20), self.table_view) v_header.setDefaultSectionSize(size.height() + 2) v_header.setMinimumSectionSize(5) self.table_view.horizontalHeader().setStretchLastSection(True) # set unknown genes self.unknown_model.initialize(self.gene_matcher.genes) self.unknown_view.verticalHeader().setStretchLastSection(True) self._apply_filter() def get_available_organisms(self): available_organism = sorted([(tax_id, taxonomy.name(tax_id)) for tax_id in taxonomy.common_taxids()], key=lambda x: x[1]) self.organisms = [tax_id[0] for tax_id in available_organism] self.organism_select_combobox.addItems( [tax_id[1] for tax_id in available_organism]) def gene_names_from_table(self): """ Extract and return gene names from `Orange.data.Table`. """ self.input_genes = [] if self.input_data: if self.use_attr_names: self.input_genes = [ str(attr.name).strip() for attr in self.input_data.domain.attributes ] else: if self.selected_gene_col is None: self.selected_gene_col = self.gene_column_identifier() self.input_genes = [ str(e[self.selected_gene_col]) for e in self.input_data if not np.isnan(e[self.selected_gene_col]) ] def _update_gene_matcher(self): self.gene_names_from_table() self.gene_matcher = GeneMatcher(self.get_selected_organism(), case_insensitive=True) self.gene_matcher.genes = self.input_genes self.gene_matcher.organism = self.get_selected_organism() def get_selected_organism(self): return self.organisms[self.selected_organism] def match_genes(self): if self.gene_matcher: # init progress bar self.progress_bar = ProgressBar(self, iterations=len( self.gene_matcher.genes)) # status message self.setStatusMessage('Gene matcher running') worker = Worker(self.gene_matcher.run_matcher, progress_callback=True) worker.signals.progress.connect(self._progress_advance) worker.signals.finished.connect(self._handle_matcher_results) # move download process to worker thread self.threadpool.start(worker) def on_input_option_change(self): self.__reset_widget_state() self._update_gene_matcher() self.match_genes() def gene_column_identifier(self): """ Get most suitable column that stores genes. If there are several suitable columns, select the one with most unique values. Take the best one. """ # candidates -> (variable, num of unique values) candidates = ((col, np.unique(self.input_data.get_column_view(col)[0]).size) for col in self.gene_columns_model if isinstance(col, DiscreteVariable) or isinstance(col, StringVariable)) best_candidate, _ = sorted(candidates, key=lambda x: x[1])[-1] return best_candidate def find_genes_location(self): """ Try locate the genes in the input data when we first load the data. Proposed rules: - when no suitable feature names are present, check the columns. - find the most suitable column, that is, the one with most unique values. """ domain = self.input_data.domain if not domain.attributes: if self.selected_gene_col is None: self.selected_gene_col = self.gene_column_identifier() self.use_attr_names = False @Inputs.data_table def handle_input(self, data): self.closeContext() self.input_data = None self.input_genes = None self.__reset_widget_state() self.gene_columns_model.set_domain(None) self.selected_gene_col = None if data: self.input_data = data self.gene_columns_model.set_domain(self.input_data.domain) # check if input table has tax_id, human is used if tax_id is not found self.tax_id = str(self.input_data.attributes.get(TAX_ID, '9606')) # check for gene location. Default is that genes are attributes in the input table. self.use_attr_names = self.input_data.attributes.get( GENE_AS_ATTRIBUTE_NAME, self.use_attr_names) if self.tax_id in self.organisms and not self.selected_organism: self.selected_organism = self.organisms.index(self.tax_id) self.openContext(self.input_data.domain) self.find_genes_location() self.on_input_option_change() def commit(self): selection = self.table_view.selectionModel().selectedRows( self.table_model.entrez_column_index) selected_genes = [row.data() for row in selection] if not len(selected_genes): selected_genes = self.table_model.get_filtered_genes() gene_ids = self.get_target_ids() known_genes = [gid for gid in gene_ids if gid != '?'] table = None gm_table = None if known_genes: # Genes are in rows (we have a column with genes). if not self.use_attr_names: if self.target_database in self.input_data.domain: gene_var = self.input_data.domain[self.target_database] metas = self.input_data.domain.metas else: gene_var = StringVariable(self.target_database) metas = self.input_data.domain.metas + (gene_var, ) domain = Domain(self.input_data.domain.attributes, self.input_data.domain.class_vars, metas) table = self.input_data.transform(domain) col, _ = table.get_column_view(gene_var) col[:] = gene_ids # filter selected rows selected_rows = [ row_index for row_index, row in enumerate(table) if str(row[gene_var]) in selected_genes ] # handle table attributes table.attributes[TAX_ID] = self.get_selected_organism() table.attributes[GENE_AS_ATTRIBUTE_NAME] = False table.attributes[GENE_ID_COLUMN] = self.target_database table = table[selected_rows] if selected_rows else table if self.exclude_unmatched: # create filter from selected column for genes only_known = table_filter.FilterStringList( gene_var, known_genes) # apply filter to the data table = table_filter.Values([only_known])(table) self.Outputs.data_table.send(table) # genes are are in columns (genes are features). else: domain = self.input_data.domain.copy() table = self.input_data.transform(domain) for gene in self.gene_matcher.genes: if gene.input_name in table.domain: table.domain[gene.input_name].attributes[self.target_database] = \ str(gene.ncbi_id) if gene.ncbi_id else '?' if self.replace_id_with_symbol: try: table.domain[gene.input_name].name = str( gene.symbol) except AttributeError: # TODO: missing gene symbol, need to handle this? pass # filter selected columns selected = [ column for column in table.domain.attributes if self.target_database in column.attributes and str(column.attributes[ self.target_database]) in selected_genes ] output_attrs = table.domain.attributes if selected: output_attrs = selected if self.exclude_unmatched: output_attrs = [ col for col in output_attrs if col.attributes[self.target_database] in known_genes ] domain = Domain(output_attrs, table.domain.class_vars, table.domain.metas) table = table.from_table(domain, table) # handle table attributes table.attributes[TAX_ID] = self.get_selected_organism() table.attributes[GENE_AS_ATTRIBUTE_NAME] = True table.attributes[GENE_ID_ATTRIBUTE] = self.target_database gm_table = self.gene_matcher.to_data_table( selected_genes=selected_genes if selected_genes else None) self.Outputs.data_table.send(table) self.Outputs.gene_matcher_results.send(gm_table) def toggle_radio_options(self): self.replace_radio.setEnabled(bool(self.use_attr_names)) if self.gene_matcher.genes: # enable checkbox if unknown genes are detected self.exclude_radio.setEnabled( len(self.gene_matcher.genes) != len( self.gene_matcher.get_known_genes())) self.exclude_unmatched = len(self.gene_matcher.genes) != len( self.gene_matcher.get_known_genes()) def get_target_ids(self): return [ str(gene.ncbi_id) if gene.ncbi_id else '?' for gene in self.gene_matcher.genes ]
class OWtSNE(OWWidget): name = "t-SNE" description = "Two-dimensional data projection with t-SNE." icon = "icons/TSNE.svg" priority = 3055 class Inputs: data = Input("Data", Orange.data.Table, default=True) data_subset = Input("Data Subset", Orange.data.Table) class Outputs: selected_data = Output("Selected Data", Orange.data.Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table) settings_version = 2 #: Runtime state Running, Finished, Waiting = 1, 2, 3 settingsHandler = settings.DomainContextHandler() max_iter = settings.Setting(300) perplexity = settings.Setting(30) pca_components = settings.Setting(20) # output embedding role. NoRole, AttrRole, AddAttrRole, MetaRole = 0, 1, 2, 3 auto_commit = settings.Setting(True) selection_indices = settings.Setting(None, schema_only=True) legend_anchor = settings.Setting(((1, 0), (1, 0))) graph = SettingProvider(OWMDSGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Error(OWWidget.Error): not_enough_rows = Msg("Input data needs at least 2 rows") constant_data = Msg("Input data is constant") no_attributes = Msg("Data has no attributes") out_of_memory = Msg("Out of memory") optimization_error = Msg("Error during optimization\n{}") def __init__(self): super().__init__() #: Effective data used for plot styling/annotations. self.data = None # type: Optional[Orange.data.Table] #: Input subset data table self.subset_data = None # type: Optional[Orange.data.Table] #: Input data table self.signal_data = None self._subset_mask = None # type: Optional[np.ndarray] self.pca_data = None self._curve = None self._data_metas = None self.variable_x = ContinuousVariable("tsne-x") self.variable_y = ContinuousVariable("tsne-y") self.__update_loop = None # timer for scheduling updates self.__timer = QTimer(self, singleShot=True, interval=1, timeout=self.__next_step) self.__state = OWtSNE.Waiting self.__in_next_step = False self.__draw_similar_pairs = False box = gui.vBox(self.controlArea, "t-SNE") form = QFormLayout(labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 250, 2000, step=50)) form.addRow("Perplexity:", gui.spin(box, self, "perplexity", 1, 100, step=1)) box.layout().addLayout(form) gui.separator(box, 10) self.runbutton = gui.button(box, self, "Run", callback=self._toggle_run) box = gui.vBox(self.controlArea, "PCA Preprocessing") gui.hSlider(box, self, 'pca_components', label="Components: ", minValue=2, maxValue=50, step=1) #, callback=self._initialize) box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWMDSGraph(self, box, "MDSGraph", view_box=MDSInteractiveViewBox) box.layout().addWidget(self.graph.plot_widget) self.plot = self.graph.plot_widget g = self.graph.gui box = g.point_properties_box(self.controlArea) self.models = g.points_models # Because sc data frequently has many genes, # showing all attributes in combo boxes can cause problems # QUICKFIX: Remove a separator and attributes from order # (leaving just the class and metas) for model in self.models: model.order = model.order[:-2] g.add_widgets(ids=[g.JitterSizeSlider], widget=box) box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([ g.ShowLegend, g.ToolTipShowsAll, g.ClassDensity, g.LabelOnlySelected ], box) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict palette = self.graph.plot_widget.palette() self.graph.set_palette(palette) gui.rubber(self.controlArea) self.graph.box_zoom_select(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") self.plot.getPlotItem().hideButtons() self.plot.setRenderHint(QPainter.Antialiasing) self.graph.jitter_continuous = True self._initialize() def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() def update_colors(self): pass def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def init_attr_values(self): domain = self.data and len(self.data) and self.data.domain or None for model in self.models: model.set_domain(domain) self.graph.attr_color = self.data.domain.class_var if domain else None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def prepare_data(self): pass def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.variable_x, self.variable_y, True) def selection_changed(self): self.commit() @Inputs.data @check_sql_input def set_data(self, data): """Set the input data set. Parameters ---------- data : Optional[Orange.data.Table] """ self.signal_data = data @Inputs.data_subset def set_subset_data(self, subset_data): """Set a subset of `data` input to highlight in the plot. Parameters ---------- subset_data: Optional[Orange.data.Table] """ self.subset_data = subset_data # invalidate the pen/brush when the subset is changed self._subset_mask = None # type: Optional[np.ndarray] self.controls.graph.alpha_value.setEnabled(subset_data is None) def _clear(self): self.__set_update_loop(None) self.__state = OWtSNE.Waiting def _clear_plot(self): self.graph.plot_widget.clear() def _initialize(self): # clear everything self.closeContext() self._clear() self.Error.clear() self.data = None self.pca_data = None self.embedding = None self.init_attr_values() # if no data, reset plot if self.signal_data is None: return if len(self.signal_data) < 2: self.Error.not_enough_rows() elif not self.signal_data.domain.attributes: self.Error.no_attributes() elif np.allclose(self.signal_data.X - self.signal_data.X[0], 0): self.Error.constant_data() else: self.data = self.signal_data self.init_attr_values() self.openContext(self.data) def _toggle_run(self): if self.__state == OWtSNE.Running: self.stop() self._invalidate_output() else: self.start() def start(self): if not self.data or self.__state == OWtSNE.Running: self._update_plot() return elif self.__state in (OWtSNE.Finished, OWtSNE.Waiting): self.__start() def stop(self): if self.__state == OWtSNE.Running: self.__set_update_loop(None) def pca_preprocessing(self): if self.pca_data is not None and \ self.pca_data.X.shape[1] == self.pca_components: return pca = Orange.projection.PCA(n_components=self.pca_components, random_state=0) model = pca(self.data) self.pca_data = model(self.data) def __start(self): self.pca_preprocessing() embedding = 'random' if self.embedding is None else self.embedding step_size = self.max_iter def update_loop(data, max_iter, step, embedding): """ return an iterator over successive improved MDS point embeddings. """ # NOTE: this code MUST NOT call into QApplication.processEvents done = False iterations_done = 0 while not done: step_iter = min(max_iter - iterations_done, step) embedding = compute_tsne_embedding(data.X, self.perplexity, step_iter, embedding) iterations_done += step_iter if iterations_done >= max_iter: done = True yield embedding, iterations_done / max_iter self.__set_update_loop( update_loop(self.pca_data, self.max_iter, step_size, embedding)) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): """ Set the update `loop` coroutine. The `loop` is a generator yielding `(embedding, progress)` tuples where `embedding` is a `(N, 2) ndarray` of current updated MDS points, and `progress` a float ratio (0 <= progress <= 1) If an existing update coroutine loop is already in place it is interrupted (i.e. closed). .. note:: The `loop` must not explicitly yield control flow to the event loop (i.e. call `QApplication.processEvents`) """ if self.__update_loop is not None: self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.setBlocking(True) self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWtSNE.Running self.__timer.start() else: self.setBlocking(False) self.setStatusMessage("") self.runbutton.setText("Start") self.__state = OWtSNE.Finished self.__timer.stop() def __next_step(self): if self.__update_loop is None: return assert not self.__in_next_step self.__in_next_step = True loop = self.__update_loop self.Error.out_of_memory.clear() self.Error.optimization_error.clear() try: embedding, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__set_update_loop(None) self.unconditional_commit() except MemoryError: self.Error.out_of_memory() self.__set_update_loop(None) except Exception as exc: self.Error.optimization_error(str(exc)) self.__set_update_loop(None) else: self.progressBarSet(100.0 * progress, processEvents=None) self.embedding = embedding self._update_plot() # schedule next update self.__timer.start() self.__in_next_step = False def __invalidate_refresh(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) # restart the optimization if it was interrupted. # TODO: decrease the max iteration count by the already # completed iterations count. if state == OWtSNE.Running: self.__start() def handleNewSignals(self): if self.data and self.signal_data and np.array_equal( self.data.X, self.signal_data.X): invalidated = False self.closeContext() self.data = self.signal_data self.init_attr_values() self.openContext(self.data) else: invalidated = True self._initialize() if self._subset_mask is None and self.subset_data is not None and \ self.data is not None: self._subset_mask = np.in1d(self.data.ids, self.subset_data.ids) if invalidated: self.start() else: self._update_plot(new=True) self.unconditional_commit() def _invalidate_output(self): self.commit() def _update_plot(self, new=False): self._clear_plot() if self.embedding is not None: self._setup_plot(new=new) else: self.graph.new_data(None) def _setup_plot(self, new=False): emb_x, emb_y = self.embedding[:, 0], self.embedding[:, 1] coords = np.vstack((emb_x, emb_y)).T domain = Domain(attributes=self.data.domain.attributes + (self.variable_x, self.variable_y), class_vars=self.data.domain.class_vars, metas=self.data.domain.metas) data = Table.from_numpy(domain, X=np.hstack((self.data.X, coords)), Y=self.data.Y, metas=self.data.metas) subset_data = data[ self._subset_mask] if self._subset_mask is not None else None self.graph.new_data(data, subset_data=subset_data, new=new) self.graph.update_data(self.variable_x, self.variable_y, True) def commit(self): if self.embedding is not None: names = get_unique_names( [v.name for v in self.data.domain.variables], ["tsne-x", "tsne-y"]) output = embedding = Orange.data.Table.from_numpy( Orange.data.Domain([ ContinuousVariable(names[0]), ContinuousVariable(names[1]) ]), self.embedding) else: output = embedding = None if self.embedding is not None and self.data is not None: domain = self.data.domain domain = Orange.data.Domain( domain.attributes, domain.class_vars, domain.metas + embedding.domain.attributes) output = self.data.transform(domain) output.metas[:, -2:] = embedding.X selection = self.graph.get_selection() if output is not None and len(selection) > 0: selected = create_groups_table(output, self.graph.selection, False, "Group") else: selected = None if self.graph.selection is not None and np.max( self.graph.selection) > 1: annotated = create_groups_table(output, self.graph.selection) else: annotated = create_annotated_table(output, selection) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) def onDeleteWidget(self): super().onDeleteWidget() self._clear_plot() self._clear() def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert( (("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", self.graph.jitter_size != 0 and "{} %".format(self.graph.jitter_size)))) self.report_plot() if caption: self.report_caption(caption)
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 OWSelectAttributes(widget.OWWidget): # pylint: disable=too-many-instance-attributes name = "选择列(Select Columns)" description = "从数据表选择列, 并将它们设为特征, 目标或者元属性." category = "数据(Data)" icon = "icons/SelectColumns.svg" priority = 100 keywords = ["filter", "attributes", "target", "variable", 'xuanzelie'] class Inputs: data = Input("数据(Data)", Table, default=True, replaces=['Data']) features = Input("特征(Features)", AttributeList, replaces=['Features']) class Outputs: data = Output("数据(Data)", Table, replaces=['Data']) features = Output("特征(Features)", AttributeList, dynamic=False, replaces=['Features']) want_main_area = False want_control_area = True settingsHandler = SelectAttributesDomainContextHandler(first_match=False) domain_role_hints = ContextSetting({}) use_input_features = Setting(False) ignore_new_features = Setting(False) auto_commit = Setting(True) class Warning(widget.OWWidget.Warning): mismatching_domain = Msg("Features and data domain do not match") multiple_targets = Msg("Most widgets do not support multiple targets") def __init__(self): super().__init__() self.data = None self.features = None # Schedule interface updates (enabled buttons) using a coalescing # single shot timer (complex interactions on selection and filtering # updates in the 'available_attrs_view') self.__interface_update_timer = QTimer(self, interval=0, singleShot=True) self.__interface_update_timer.timeout.connect( self.__update_interface_state) # The last view that has the selection for move operation's source self.__last_active_view = None # type: Optional[QListView] def update_on_change(view): # Schedule interface state update on selection change in `view` self.__last_active_view = view self.__interface_update_timer.start() new_control_area = QWidget(self.controlArea) self.controlArea.layout().addWidget(new_control_area) self.controlArea = new_control_area # init grid layout = QGridLayout() self.controlArea.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) box = gui.vBox(self.controlArea, "可用变量", addToLayout=False) self.available_attrs = VariablesListItemModel() filter_edit, self.available_attrs_view = variables_filter( parent=self, model=self.available_attrs) box.layout().addWidget(filter_edit) def dropcompleted(action): if action == Qt.MoveAction: self.commit.deferred() self.available_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.available_attrs_view)) self.available_attrs_view.dragDropActionDidComplete.connect( dropcompleted) box.layout().addWidget(self.available_attrs_view) layout.addWidget(box, 0, 0, 3, 1) # 3rd column box = gui.vBox(self.controlArea, "特征", addToLayout=False) self.used_attrs = VariablesListItemModel() filter_edit, self.used_attrs_view = variables_filter( parent=self, model=self.used_attrs, accepted_type=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.used_attrs.rowsInserted.connect(self.__used_attrs_changed) self.used_attrs.rowsRemoved.connect(self.__used_attrs_changed) self.used_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.used_attrs_view)) self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.use_features_box = gui.auto_commit( self.controlArea, self, "use_input_features", "使用输入特征", "总是使用输入特征", box=False, commit=self.__use_features_clicked, callback=self.__use_features_changed, addToLayout=False) self.enable_use_features_box() box.layout().addWidget(self.use_features_box) box.layout().addWidget(filter_edit) box.layout().addWidget(self.used_attrs_view) layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "目标", addToLayout=False) self.class_attrs = VariablesListItemModel() self.class_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) self.class_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.class_attrs_view)) self.class_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.class_attrs_view) layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "元属性", addToLayout=False) self.meta_attrs = VariablesListItemModel() self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) self.meta_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.meta_attrs_view)) self.meta_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.meta_attrs_view) layout.addWidget(box, 2, 2, 1, 1) # 2nd column bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) self.move_attr_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.used_attrs_view)) layout.addWidget(bbox, 0, 1, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) self.move_class_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.class_attrs_view)) layout.addWidget(bbox, 1, 1, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False) self.move_meta_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.meta_attrs_view)) layout.addWidget(bbox, 2, 1, 1, 1) # footer gui.button(self.buttonsArea, self, "重置", callback=self.reset) bbox = gui.vBox(self.buttonsArea) gui.checkBox( widget=bbox, master=self, value="ignore_new_features", label="Ignore new variables by default", tooltip="When the widget receives data with additional columns " "they are added to the available attributes column if " "<i>Ignore new variables by default</i> is checked.") gui.rubber(self.buttonsArea) gui.auto_send(self.buttonsArea, self, "auto_commit") layout.setRowStretch(0, 2) layout.setRowStretch(1, 0) layout.setRowStretch(2, 1) layout.setHorizontalSpacing(0) self.controlArea.setLayout(layout) self.output_data = None self.original_completer_items = [] self.resize(600, 600) @property def features_from_data_attributes(self): if self.data is None or self.features is None: return [] domain = self.data.domain return [ domain[feature.name] for feature in self.features if feature.name in domain and domain[feature.name] in domain.attributes ] def can_use_features(self): return bool(self.features_from_data_attributes) and \ self.features_from_data_attributes != self.used_attrs[:] def __use_features_changed(self): # Use input features check box # Needs a check since callback is invoked before object is created if not hasattr(self, "use_features_box"): return self.enable_used_attrs(not self.use_input_features) if self.use_input_features and self.can_use_features(): self.use_features() if not self.use_input_features: self.enable_use_features_box() @gui.deferred def __use_features_clicked(self): # Use input features button self.use_features() def __used_attrs_changed(self): self.enable_use_features_box() @Inputs.data def set_data(self, data=None): self.update_domain_role_hints() self.closeContext() self.domain_role_hints = {} self.data = data if data is None: self.used_attrs[:] = [] self.class_attrs[:] = [] self.meta_attrs[:] = [] self.available_attrs[:] = [] return self.openContext(data) all_vars = data.domain.variables + data.domain.metas def attrs_for_role(role): selected_attrs = [ attr for attr in all_vars if domain_hints[attr][0] == role ] return sorted(selected_attrs, key=lambda attr: domain_hints[attr][1]) domain_hints = self.restore_hints(data.domain) self.used_attrs[:] = attrs_for_role("attribute") self.class_attrs[:] = attrs_for_role("class") self.meta_attrs[:] = attrs_for_role("meta") self.available_attrs[:] = attrs_for_role("available") self.update_interface_state(self.class_attrs_view) def restore_hints(self, domain: Domain) -> Dict[Variable, Tuple[str, int]]: """ Define hints for selected/unselected features. Rules: - if context available, restore new features based on checked/unchecked ignore_new_features, context hint should be took into account - in no context, restore features based on the domain (as selected) Parameters ---------- domain Data domain Returns ------- Dictionary with hints about order and model in which each feature should appear """ domain_hints = {} if not self.ignore_new_features or len(self.domain_role_hints) == 0: # select_new_features selected or no context - restore based on domain domain_hints.update( self._hints_from_seq("attribute", domain.attributes)) domain_hints.update(self._hints_from_seq("meta", domain.metas)) domain_hints.update( self._hints_from_seq("class", domain.class_vars)) else: # if context restored and ignore_new_features selected - restore # new features as available d = domain.attributes + domain.metas + domain.class_vars domain_hints.update(self._hints_from_seq("available", d)) domain_hints.update(self.domain_role_hints) return domain_hints def update_domain_role_hints(self): """ Update the domain hints to be stored in the widgets settings. """ hints = {} hints.update(self._hints_from_seq("available", self.available_attrs)) hints.update(self._hints_from_seq("attribute", self.used_attrs)) hints.update(self._hints_from_seq("class", self.class_attrs)) hints.update(self._hints_from_seq("meta", self.meta_attrs)) self.domain_role_hints = hints @staticmethod def _hints_from_seq(role, model): return [(attr, (role, i)) for i, attr in enumerate(model)] @Inputs.features def set_features(self, features): self.features = features def handleNewSignals(self): self.check_data() self.enable_used_attrs() self.enable_use_features_box() if self.use_input_features and self.features_from_data_attributes: self.enable_used_attrs(False) self.use_features() self.commit.now() def check_data(self): self.Warning.mismatching_domain.clear() if self.data is not None and self.features is not None and \ not self.features_from_data_attributes: self.Warning.mismatching_domain() def enable_used_attrs(self, enable=True): self.move_attr_button.setEnabled(enable) self.used_attrs_view.setEnabled(enable) self.used_attrs_view.repaint() def enable_use_features_box(self): self.use_features_box.button.setEnabled(self.can_use_features()) enable_checkbox = bool(self.features_from_data_attributes) self.use_features_box.setHidden(not enable_checkbox) self.use_features_box.repaint() def use_features(self): attributes = self.features_from_data_attributes available, used = self.available_attrs[:], self.used_attrs[:] self.available_attrs[:] = [ attr for attr in used + available if attr not in attributes ] self.used_attrs[:] = attributes self.commit.deferred() @staticmethod def selected_rows(view): """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() model = view.model() if isinstance(model, QSortFilterProxyModel): rows = [model.mapToSource(r) for r in rows] return [r.row() for r in rows] def move_rows(self, view: QListView, offset: int, roles=(Qt.EditRole, )): rows = [idx.row() for idx in view.selectionModel().selectedRows()] model = view.model() # type: QAbstractItemModel rowcount = model.rowCount() newrows = [min(max(0, row + offset), rowcount - 1) for row in rows] def itemData(index): return {role: model.data(index, role) for role in roles} for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): d1 = itemData(model.index(row, 0)) d2 = itemData(model.index(newrow, 0)) model.setItemData(model.index(row, 0), d2) model.setItemData(model.index(newrow, 0), d1) selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect) self.commit.deferred() def move_up(self, view: QListView): self.move_rows(view, -1) def move_down(self, view: QListView): self.move_rows(view, 1) def move_selected(self, view): if self.selected_rows(view): self.move_selected_from_to(view, self.available_attrs_view) elif self.selected_rows(self.available_attrs_view): self.move_selected_from_to(self.available_attrs_view, view) def move_selected_from_to(self, src, dst): self.move_from_to(src, dst, self.selected_rows(src)) def move_from_to(self, src, dst, rows): src_model = source_model(src) attrs = [src_model[r] for r in rows] for s1, s2 in reversed(list(slices(rows))): del src_model[s1:s2] dst_model = source_model(dst) dst_model.extend(attrs) self.commit.deferred() def __update_interface_state(self): last_view = self.__last_active_view if last_view is not None: self.update_interface_state(last_view) def update_interface_state(self, focus=None): for view in [ self.available_attrs_view, self.used_attrs_view, self.class_attrs_view, self.meta_attrs_view ]: if view is not focus and not view.hasFocus() \ and view.selectionModel().hasSelection(): view.selectionModel().clear() def selected_vars(view): model = source_model(view) return [model[i] for i in self.selected_rows(view)] available_selected = selected_vars(self.available_attrs_view) attrs_selected = selected_vars(self.used_attrs_view) class_selected = selected_vars(self.class_attrs_view) meta_selected = selected_vars(self.meta_attrs_view) available_types = set(map(type, available_selected)) all_primitive = all(var.is_primitive() for var in available_types) move_attr_enabled = \ ((available_selected and all_primitive) or attrs_selected) and \ self.used_attrs_view.isEnabled() self.move_attr_button.setEnabled(bool(move_attr_enabled)) if move_attr_enabled: self.move_attr_button.setText(">" if available_selected else "<") move_class_enabled = bool(all_primitive and available_selected) or class_selected self.move_class_button.setEnabled(bool(move_class_enabled)) if move_class_enabled: self.move_class_button.setText(">" if available_selected else "<") move_meta_enabled = available_selected or meta_selected self.move_meta_button.setEnabled(bool(move_meta_enabled)) if move_meta_enabled: self.move_meta_button.setText(">" if available_selected else "<") # update class_vars height if self.class_attrs.rowCount() == 0: height = 22 else: height = ((self.class_attrs.rowCount() or 1) * self.class_attrs_view.sizeHintForRow(0)) + 2 self.class_attrs_view.setFixedHeight(height) self.__last_active_view = None self.__interface_update_timer.stop() @gui.deferred def commit(self): self.update_domain_role_hints() self.Warning.multiple_targets.clear() if self.data is not None: attributes = list(self.used_attrs) class_var = list(self.class_attrs) metas = list(self.meta_attrs) domain = Orange.data.Domain(attributes, class_var, metas) newdata = self.data.transform(domain) self.output_data = newdata self.Outputs.data.send(newdata) self.Outputs.features.send(AttributeList(attributes)) self.Warning.multiple_targets(shown=len(class_var) > 1) else: self.output_data = None self.Outputs.data.send(None) self.Outputs.features.send(None) def reset(self): self.enable_used_attrs() self.use_features_box.checkbox.setChecked(False) if self.data is not None: self.available_attrs[:] = [] self.used_attrs[:] = self.data.domain.attributes self.class_attrs[:] = self.data.domain.class_vars self.meta_attrs[:] = self.data.domain.metas self.update_domain_role_hints() self.commit.now() def send_report(self): if not self.data or not self.output_data: return in_domain, out_domain = self.data.domain, self.output_data.domain self.report_domain("Input data", self.data.domain) if (in_domain.attributes, in_domain.class_vars, in_domain.metas) == (out_domain.attributes, out_domain.class_vars, out_domain.metas): self.report_paragraph("Output data", "No changes.") else: self.report_domain("Output data", self.output_data.domain) diff = list( set(in_domain.variables + in_domain.metas) - set(out_domain.variables + out_domain.metas)) if diff: text = "%i (%s)" % (len(diff), ", ".join(x.name for x in diff)) self.report_items((("Removed", text), ))
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) features = Input("Features", AttributeList) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) features = Output("Features", Table, dynamic=False) settings_version = 2 settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting(None) attr_y = ContextSetting(None) selection_group = Setting(None, schema_only=True) graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QPen(self.palette().color(QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) g = self.graph.gui g.add_widgets([g.JitterSizeSlider, g.JitterNumericValues], box) self.sampling = gui.auto_commit(self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) g.point_properties_box(self.controlArea) self.models = [self.xy_model] + g.points_models box_plot_prop = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([ g.ShowLegend, g.ShowGridLines, g.ToolTipShowsAll, g.ClassDensity, g.RegressionLine, g.LabelOnlySelected ], box_plot_prop) self.graph.box_zoom_select(self.controlArea) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") self.graph.zoom_actions(self) def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() @Inputs.data def set_data(self, data): self.clear_messages() self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank.initialize() self.vizrank.attrs = self.data.domain.attributes if self.data is not None else [] self.vizrank_button.setEnabled( self.data is not None and not self.data.is_sparse() and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) if self.data is not None and self.data.domain.class_var is None \ and len(self.data.domain.attributes) > 1 and len(self.data) > 1: self.vizrank_button.setToolTip( "Data with a class variable is required.") else: self.vizrank_button.setToolTip("") self.openContext(self.data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Orange.data.Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.graph.attr_label, str): self.graph.attr_label = findvar(self.graph.attr_label, self.graph.gui.label_model) if isinstance(self.graph.attr_color, str): self.graph.attr_color = findvar(self.graph.attr_color, self.graph.gui.color_model) if isinstance(self.graph.attr_shape, str): self.graph.attr_shape = findvar(self.graph.attr_shape, self.graph.gui.shape_model) if isinstance(self.graph.attr_size, str): self.graph.attr_size = findvar(self.graph.attr_size, self.graph.gui.size_model) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [ a for a in data.domain.attributes + data.domain.metas if a.is_primitive() ] new_metas = [m for m in data.domain.metas if not m.is_primitive()] new_domain = Domain(new_attrs, data.domain.class_vars, new_metas) data = data.transform(new_domain) return data @Inputs.data_subset def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) self.controls.graph.alpha_value.setEnabled(subset_data is None) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.sparse_to_dense(self.data_metas_X), self.sparse_to_dense(self.subset_data)) if self.attribute_selection_list and self.graph.domain and \ all(attr in self.graph.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.apply_selection() self.unconditional_commit() def prepare_data(self): """ Only when dealing with sparse matrices. GH-2152 """ self.graph.new_data(self.sparse_to_dense(self.data_metas_X), self.sparse_to_dense(self.subset_data), new=False) def sparse_to_dense(self, input_data=None): if input_data is None or not input_data.is_sparse(): return input_data keys = [] attrs = { self.attr_x, self.attr_y, self.graph.attr_color, self.graph.attr_shape, self.graph.attr_size, self.graph.attr_label } for i, attr in enumerate(input_data.domain): if attr in attrs: keys.append(i) new_domain = input_data.domain.select_columns(keys) dmx = input_data.transform(new_domain) dmx.X = dmx.X.toarray() # TODO: remove once we make sure Y is always dense. if sp.issparse(dmx.Y): dmx.Y = dmx.Y.toarray() return dmx def apply_selection(self): """Apply selection saved in workflow.""" if self.data is not None and self.selection_group is not None: self.graph.selection = np.zeros(len(self.data), dtype=np.uint8) self.selection_group = [ x for x in self.selection_group if x[0] < len(self.data) ] selection_array = np.array(self.selection_group).T self.graph.selection[selection_array[0]] = selection_array[1] self.graph.update_colors(keep_colors=True) @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x self.graph.attr_color = self.data.domain.class_var if domain else None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): self.prepare_data() self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.send_features() def update_colors(self): self.prepare_data() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() def send_data(self): selected = None selection = None # TODO: Implement selection for sql data graph = self.graph if isinstance(self.data, SqlTable): selected = self.data elif self.data is not None: selection = graph.get_selection() if len(selection) > 0: selected = self.data[selection] if graph.selection is not None and np.max(graph.selection) > 1: annotated = create_groups_table(self.data, graph.selection) else: annotated = create_annotated_table(self.data, selection) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) # Store current selection in a setting that is stored in workflow if selection is not None and len(selection): self.selection_group = list( zip(selection, graph.selection[selection])) else: self.selection_group = None def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"), )) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.Outputs.features.send(features) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert( (("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]]
class TabBarWidget(QWidget): """ A tab bar widget using tool buttons as tabs. """ # TODO: A uniform size box layout. currentChanged = Signal(int) def __init__(self, parent=None, **kwargs): QWidget.__init__(self, parent, **kwargs) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.__tabs = [] self.__currentIndex = -1 self.__changeOnHover = False self.__iconSize = QSize(26, 26) self.__group = QButtonGroup(self, exclusive=True) self.__group.buttonPressed[QAbstractButton].connect( self.__onButtonPressed ) self.setMouseTracking(True) self.__sloppyButton = None self.__sloppyRegion = QRegion() self.__sloppyTimer = QTimer(self, singleShot=True) self.__sloppyTimer.timeout.connect(self.__onSloppyTimeout) def setChangeOnHover(self, changeOnHover): """ If set to ``True`` the tab widget will change the current index when the mouse hovers over a tab button. """ if self.__changeOnHover != changeOnHover: self.__changeOnHover = changeOnHover def changeOnHover(self): """ Does the current tab index follow the mouse cursor. """ return self.__changeOnHover def count(self): """ Return the number of tabs in the widget. """ return len(self.__tabs) def addTab(self, text, icon=None, toolTip=None): """ Add a new tab and return it's index. """ return self.insertTab(self.count(), text, icon, toolTip) def insertTab(self, index, text, icon=None, toolTip=None): """ Insert a tab at `index` """ button = TabButton(self, objectName="tab-button") button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) button.setIconSize(self.__iconSize) button.setMouseTracking(True) self.__group.addButton(button) button.installEventFilter(self) tab = _Tab(text, icon, toolTip, button, None, None) self.layout().insertWidget(index, button) self.__tabs.insert(index, tab) self.__updateTab(index) if self.currentIndex() == -1: self.setCurrentIndex(0) return index def removeTab(self, index): """ Remove a tab at `index`. """ if index >= 0 and index < self.count(): self.layout().takeItem(index) tab = self.__tabs.pop(index) self.__group.removeButton(tab.button) tab.button.removeEventFilter(self) if tab.button is self.__sloppyButton: self.__sloppyButton = None self.__sloppyRegion = QRegion() tab.button.deleteLater() if self.currentIndex() == index: if self.count(): self.setCurrentIndex(max(index - 1, 0)) else: self.setCurrentIndex(-1) def setTabIcon(self, index, icon): """ Set the `icon` for tab at `index`. """ self.__tabs[index] = self.__tabs[index]._replace(icon=icon) self.__updateTab(index) def setTabToolTip(self, index, toolTip): """ Set `toolTip` for tab at `index`. """ self.__tabs[index] = self.__tabs[index]._replace(toolTip=toolTip) self.__updateTab(index) def setTabText(self, index, text): """ Set tab `text` for tab at `index` """ self.__tabs[index] = self.__tabs[index]._replace(text=text) self.__updateTab(index) def setTabPalette(self, index, palette): """ Set the tab button palette. """ self.__tabs[index] = self.__tabs[index]._replace(palette=palette) self.__updateTab(index) def setCurrentIndex(self, index): """ Set the current tab index. """ if self.__currentIndex != index: self.__currentIndex = index self.__sloppyRegion = QRegion() self.__sloppyButton = None if index != -1: self.__tabs[index].button.setChecked(True) self.currentChanged.emit(index) def currentIndex(self): """ Return the current index. """ return self.__currentIndex def button(self, index): """ Return the `TabButton` instance for index. """ return self.__tabs[index].button def setIconSize(self, size): if self.__iconSize != size: self.__iconSize = size for tab in self.__tabs: tab.button.setIconSize(self.__iconSize) def __updateTab(self, index): """ Update the tab button. """ tab = self.__tabs[index] b = tab.button if tab.text: b.setText(tab.text) if tab.icon is not None and not tab.icon.isNull(): b.setIcon(tab.icon) if tab.palette: b.setPalette(tab.palette) def __onButtonPressed(self, button): for i, tab in enumerate(self.__tabs): if tab.button is button: self.setCurrentIndex(i) break def __calcSloppyRegion(self, current): """ Given a current mouse cursor position return a region of the widget where hover/move events should change the current tab only on a timeout. """ p1 = current + QPoint(0, 2) p2 = current + QPoint(0, -2) p3 = self.pos() + QPoint(self.width()+10, 0) p4 = self.pos() + QPoint(self.width()+10, self.height()) return QRegion(QPolygon([p1, p2, p3, p4])) def __setSloppyButton(self, button): """ Set the current sloppy button (a tab button inside sloppy region) and reset the sloppy timeout. """ if not button.isChecked(): self.__sloppyButton = button delay = self.style().styleHint(QStyle.SH_Menu_SubMenuPopupDelay, None) # The delay timeout is the same as used by Qt in the QMenu. self.__sloppyTimer.start(delay) else: self.__sloppyTimer.stop() def __onSloppyTimeout(self): if self.__sloppyButton is not None: button = self.__sloppyButton self.__sloppyButton = None if not button.isChecked(): index = [tab.button for tab in self.__tabs].index(button) self.setCurrentIndex(index) def eventFilter(self, receiver, event): if event.type() == QEvent.MouseMove and \ isinstance(receiver, TabButton): pos = receiver.mapTo(self, event.pos()) if self.__sloppyRegion.contains(pos): self.__setSloppyButton(receiver) else: if not receiver.isChecked(): index = [tab.button for tab in self.__tabs].index(receiver) self.setCurrentIndex(index) #also update sloppy region if mouse is moved on the same icon self.__sloppyRegion = self.__calcSloppyRegion(pos) return QWidget.eventFilter(self, receiver, event) def leaveEvent(self, event): self.__sloppyButton = None self.__sloppyRegion = QRegion() return QWidget.leaveEvent(self, event)
class OWPCA(widget.OWWidget): name = "PCA" description = "Principal component analysis with a scree-diagram." icon = "icons/PCA.svg" priority = 3050 keywords = ["principal component analysis", "linear transformation"] class Inputs: data = Input("Data", Table) class Outputs: transformed_data = Output("Transformed data", Table) components = Output("Components", Table) pca = Output("PCA", PCA, dynamic=False) preprocessor = Output("Preprocessor", Preprocess) settingsHandler = settings.DomainContextHandler() ncomponents = settings.Setting(2) variance_covered = settings.Setting(100) batch_size = settings.Setting(100) address = settings.Setting('') auto_update = settings.Setting(True) auto_commit = settings.Setting(True) normalize = settings.ContextSetting(True) decomposition_idx = settings.ContextSetting(0) maxp = settings.Setting(20) axis_labels = settings.Setting(10) graph_name = "plot.plotItem" class Warning(widget.OWWidget.Warning): trivial_components = widget.Msg( "All components of the PCA are trivial (explain 0 variance). " "Input data is constant (or near constant).") class Error(widget.OWWidget.Error): no_features = widget.Msg("At least 1 feature is required") no_instances = widget.Msg("At least 1 data instance is required") sparse_data = widget.Msg("Sparse data is not supported") def __init__(self): super().__init__() self.data = None self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = False self._init_projector() # Components Selection box = gui.vBox(self.controlArea, "Components Selection") form = QFormLayout() box.layout().addLayout(form) self.components_spin = gui.spin( box, self, "ncomponents", 1, MAX_COMPONENTS, callback=self._update_selection_component_spin, keyboardTracking=False ) self.components_spin.setSpecialValueText("All") self.variance_spin = gui.spin( box, self, "variance_covered", 1, 100, callback=self._update_selection_variance_spin, keyboardTracking=False ) self.variance_spin.setSuffix("%") form.addRow("Components:", self.components_spin) form.addRow("Variance covered:", self.variance_spin) # Incremental learning self.sampling_box = gui.vBox(self.controlArea, "Incremental learning") self.addresstext = QLineEdit(box) self.addresstext.setPlaceholderText('Remote server') if self.address: self.addresstext.setText(self.address) self.sampling_box.layout().addWidget(self.addresstext) form = QFormLayout() self.sampling_box.layout().addLayout(form) self.batch_spin = gui.spin( self.sampling_box, self, "batch_size", 50, 100000, step=50, keyboardTracking=False) form.addRow("Batch size ~ ", self.batch_spin) self.start_button = gui.button( self.sampling_box, self, "Start remote computation", callback=self.start, autoDefault=False, tooltip="Start/abort computation on the server") self.start_button.setEnabled(False) gui.checkBox(self.sampling_box, self, "auto_update", "Periodically fetch model", callback=self.update_model) self.__timer = QTimer(self, interval=2000) self.__timer.timeout.connect(self.get_model) self.sampling_box.setVisible(remotely) # Decomposition self.decomposition_box = gui.radioButtons( self.controlArea, self, "decomposition_idx", [d.name for d in DECOMPOSITIONS], box="Decomposition", callback=self._update_decomposition ) # Options self.options_box = gui.vBox(self.controlArea, "Options") self.normalize_box = gui.checkBox( self.options_box, self, "normalize", "Normalize data", callback=self._update_normalize ) self.maxp_spin = gui.spin( self.options_box, self, "maxp", 1, MAX_COMPONENTS, label="Show only first", callback=self._setup_plot, keyboardTracking=False ) self.controlArea.layout().addStretch() gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") self.plot = pg.PlotWidget(background="w") axis = self.plot.getAxis("bottom") axis.setLabel("Principal Components") axis = self.plot.getAxis("left") axis.setLabel("Proportion of variance") self.plot_horlabels = [] self.plot_horlines = [] self.plot.getViewBox().setMenuEnabled(False) self.plot.getViewBox().setMouseEnabled(False, False) self.plot.showGrid(True, True, alpha=0.5) self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) self.mainArea.layout().addWidget(self.plot) self._update_normalize() def update_model(self): self.get_model() if self.auto_update and self.rpca and not self.rpca.ready(): self.__timer.start(2000) else: self.__timer.stop() def update_buttons(self, sparse_data=False): if sparse_data: self.normalize = False buttons = self.decomposition_box.buttons for cls, button in zip(DECOMPOSITIONS, buttons): button.setDisabled(sparse_data and not cls.supports_sparse) if not buttons[self.decomposition_idx].isEnabled(): # Set decomposition index to first sparse-enabled decomposition for i, cls in enumerate(DECOMPOSITIONS): if cls.supports_sparse: self.decomposition_idx = i break self._init_projector() def start(self): if 'Abort' in self.start_button.text(): self.rpca.abort() self.__timer.stop() self.start_button.setText("Start remote computation") else: self.address = self.addresstext.text() with remote.server(self.address): from Orange.projection.pca import RemotePCA maxiter = (1e5 + self.data.approx_len()) / self.batch_size * 3 self.rpca = RemotePCA(self.data, self.batch_size, int(maxiter)) self.update_model() self.start_button.setText("Abort remote computation") @Inputs.data def set_data(self, data): self.closeContext() self.clear_messages() self.clear() self.start_button.setEnabled(False) self.information() self.data = None if isinstance(data, SqlTable): if data.approx_len() < AUTO_DL_LIMIT: data = Table(data) elif not remotely: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) else: # data was big and remote available self.sampling_box.setVisible(True) self.start_button.setText("Start remote computation") self.start_button.setEnabled(True) if not isinstance(data, SqlTable): self.sampling_box.setVisible(False) if isinstance(data, Table): if len(data.domain.attributes) == 0: self.Error.no_features() self.clear_outputs() return if len(data) == 0: self.Error.no_instances() self.clear_outputs() return self.openContext(data) sparse_data = data is not None and data.is_sparse() self.normalize_box.setDisabled(sparse_data) self.update_buttons(sparse_data=sparse_data) self.data = data self.fit() def fit(self): self.clear() self.Warning.trivial_components.clear() if self.data is None: return data = self.data self._pca_projector.preprocessors = \ self._pca_preprocessors + ([Normalize()] if self.normalize else []) if not isinstance(data, SqlTable): pca = self._pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = numpy.cumsum(variance_ratio) if numpy.isfinite(cumulative[-1]): self.components_spin.setRange(0, len(cumulative)) self._pca = pca self._variance_ratio = variance_ratio self._cumulative = cumulative self._setup_plot() else: self.Warning.trivial_components() self.unconditional_commit() def clear(self): self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = None self.plot_horlabels = [] self.plot_horlines = [] self.plot.clear() def clear_outputs(self): self.Outputs.transformed_data.send(None) self.Outputs.components.send(None) self.Outputs.pca.send(self._pca_projector) self.Outputs.preprocessor.send(None) def get_model(self): if self.rpca is None: return if self.rpca.ready(): self.__timer.stop() self.start_button.setText("Restart (finished)") self._pca = self.rpca.get_state() if self._pca is None: return self._variance_ratio = self._pca.explained_variance_ratio_ self._cumulative = numpy.cumsum(self._variance_ratio) self._setup_plot() self._transformed = None self.commit() def _setup_plot(self): self.plot.clear() if self._pca is None: return explained_ratio = self._variance_ratio explained = self._cumulative p = min(len(self._variance_ratio), self.maxp) self.plot.plot(numpy.arange(p), explained_ratio[:p], pen=pg.mkPen(QColor(Qt.red), width=2), antialias=True, name="Variance") self.plot.plot(numpy.arange(p), explained[:p], pen=pg.mkPen(QColor(Qt.darkYellow), width=2), antialias=True, name="Cumulative Variance") cutpos = self._nselected_components() - 1 self._line = pg.InfiniteLine( angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) self._line.setCursor(Qt.SizeHorCursor) self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) self._line.sigPositionChanged.connect(self._on_cut_changed) self.plot.addItem(self._line) self.plot_horlines = ( pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) self.plot_horlabels = ( pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) for item in self.plot_horlabels + self.plot_horlines: self.plot.addItem(item) self._set_horline_pos() self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) self._update_axis() def _set_horline_pos(self): cutidx = self.ncomponents - 1 for line, label, curve in zip(self.plot_horlines, self.plot_horlabels, (self._variance_ratio, self._cumulative)): y = curve[cutidx] line.setData([-1, cutidx], 2 * [y]) label.setPos(cutidx, y) label.setPlainText("{:.3f}".format(y)) def _on_cut_changed(self, line): # cut changed by means of a cut line over the scree plot. value = int(round(line.value())) self._line.setValue(value) current = self._nselected_components() components = value + 1 if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components self._set_horline_pos() if self._pca is not None: var = self._cumulative[components - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if current != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): # cut changed by "ncomponents" spin. if self._pca is None: self._invalidate_selection() return if self.ncomponents == 0: # Special "All" value cut = len(self._variance_ratio) else: cut = self.ncomponents var = self._cumulative[cut - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_selection_variance_spin(self): # cut changed by "max variance" spin. if self._pca is None: return cut = numpy.searchsorted(self._cumulative, self.variance_covered / 100.0) + 1 cut = min(cut, len(self._cumulative)) self.ncomponents = cut if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_normalize(self): self.fit() if self.data is None: self._invalidate_selection() def _init_projector(self): cls = DECOMPOSITIONS[self.decomposition_idx] self._pca_projector = cls(n_components=MAX_COMPONENTS) self._pca_projector.component = self.ncomponents self._pca_preprocessors = cls.preprocessors def _update_decomposition(self): self._init_projector() self._update_normalize() def _nselected_components(self): """Return the number of selected components.""" if self._pca is None: return 0 if self.ncomponents == 0: # Special "All" value max_comp = len(self._variance_ratio) else: max_comp = self.ncomponents var_max = self._cumulative[max_comp - 1] if var_max != numpy.floor(self.variance_covered / 100.0): cut = max_comp assert numpy.isfinite(var_max) self.variance_covered = int(var_max * 100) else: self.ncomponents = cut = numpy.searchsorted( self._cumulative, self.variance_covered / 100.0) + 1 return cut def _invalidate_selection(self): self.commit() def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p-1)//(self.axis_labels-1), 1) axis.setTicks([[(i, str(i+1)) for i in range(0, p, d)]]) def commit(self): transformed = components = pp = None if self._pca is not None: if self._transformed is None: # Compute the full transform (MAX_COMPONENTS components) only once. self._transformed = self._pca(self.data) transformed = self._transformed domain = Domain( transformed.domain.attributes[:self.ncomponents], self.data.domain.class_vars, self.data.domain.metas ) transformed = transformed.from_table(domain, transformed) # prevent caching new features by defining compute_value dom = Domain([ContinuousVariable(a.name, compute_value=lambda _: None) for a in self._pca.orig_domain.attributes], metas=[StringVariable(name='component')]) metas = numpy.array([['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T components = Table(dom, self._pca.components_[:self.ncomponents], metas=metas) components.name = 'components' pp = ApplyDomain(domain, "PCA") self._pca_projector.component = self.ncomponents self.Outputs.transformed_data.send(transformed) self.Outputs.components.send(components) self.Outputs.pca.send(self._pca_projector) self.Outputs.preprocessor.send(pp) def send_report(self): if self.data is None: return self.report_items(( ("Decomposition", DECOMPOSITIONS[self.decomposition_idx].name), ("Normalize data", str(self.normalize)), ("Selected components", self.ncomponents), ("Explained variance", "{:.3f} %".format(self.variance_covered)) )) self.report_plot() @classmethod def migrate_settings(cls, settings, version): if "variance_covered" in settings: # Due to the error in gh-1896 the variance_covered was persisted # as a NaN value, causing a TypeError in the widgets `__init__`. vc = settings["variance_covered"] if isinstance(vc, numbers.Real): if numpy.isfinite(vc): vc = int(vc) else: vc = 100 settings["variance_covered"] = vc if settings.get("ncomponents", 0) > MAX_COMPONENTS: settings["ncomponents"] = MAX_COMPONENTS
class CanvasView(QGraphicsView): """Canvas View handles the zooming. """ def __init__(self, *args): QGraphicsView.__init__(self, *args) self.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.__backgroundIcon = QIcon() self.__autoScroll = False self.__autoScrollMargin = 16 self.__autoScrollTimer = QTimer(self) self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance) self.__scale = 10 def setScene(self, scene): QGraphicsView.setScene(self, scene) self._ensureSceneRect(scene) def _ensureSceneRect(self, scene): r = scene.addRect(QRectF(0, 0, 400, 400)) scene.sceneRect() scene.removeItem(r) def setAutoScrollMargin(self, margin): self.__autoScrollMargin = margin def autoScrollMargin(self): return self.__autoScrollMargin def setAutoScroll(self, enable): self.__autoScroll = enable def autoScroll(self): return self.__autoScroll def mousePressEvent(self, event): QGraphicsView.mousePressEvent(self, event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: if not self.__autoScrollTimer.isActive() and \ self.__shouldAutoScroll(event.pos()): self.__startAutoScroll() QGraphicsView.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if event.button() & Qt.LeftButton: self.__stopAutoScroll() return QGraphicsView.mouseReleaseEvent(self, event) def reset_zoom(self): self.__set_zoom(10) def change_zoom(self, delta): self.__set_zoom(self.__scale + delta) def __set_zoom(self, scale): self.__scale = min(15, max(scale, 3)) transform = QTransform() transform.scale(self.__scale / 10, self.__scale / 10) self.setTransform(transform) def wheelEvent(self, event: QWheelEvent): # use mouse position as anchor while zooming self.setTransformationAnchor(2) if event.modifiers() & Qt.ControlModifier \ and event.buttons() == Qt.NoButton: delta = event.angleDelta().y() if QT_VERSION >= 0x050500 \ and event.source() != Qt.MouseEventNotSynthesized \ and abs(delta) < 50: self.change_zoom(delta / 10) else: self.change_zoom(copysign(1, delta)) else: super().wheelEvent(event) def __shouldAutoScroll(self, pos): if self.__autoScroll: margin = self.__autoScrollMargin viewrect = self.contentsRect() rect = viewrect.adjusted(margin, margin, -margin, -margin) # only do auto scroll when on the viewport's margins return not rect.contains(pos) and viewrect.contains(pos) else: return False def __startAutoScroll(self): self.__autoScrollTimer.start(10) log.debug("Auto scroll timer started") def __stopAutoScroll(self): if self.__autoScrollTimer.isActive(): self.__autoScrollTimer.stop() log.debug("Auto scroll timer stopped") def __autoScrollAdvance(self): """Advance the auto scroll """ pos = QCursor.pos() pos = self.mapFromGlobal(pos) margin = self.__autoScrollMargin vvalue = self.verticalScrollBar().value() hvalue = self.horizontalScrollBar().value() vrect = QRect(0, 0, self.width(), self.height()) # What should be the speed advance = 10 # We only do auto scroll if the mouse is inside the view. if vrect.contains(pos): if pos.x() < vrect.left() + margin: self.horizontalScrollBar().setValue(hvalue - advance) if pos.y() < vrect.top() + margin: self.verticalScrollBar().setValue(vvalue - advance) if pos.x() > vrect.right() - margin: self.horizontalScrollBar().setValue(hvalue + advance) if pos.y() > vrect.bottom() - margin: self.verticalScrollBar().setValue(vvalue + advance) if self.verticalScrollBar().value() == vvalue and \ self.horizontalScrollBar().value() == hvalue: self.__stopAutoScroll() else: self.__stopAutoScroll() log.debug("Auto scroll advance") def setBackgroundIcon(self, icon): if not isinstance(icon, QIcon): raise TypeError("A QIcon expected.") if self.__backgroundIcon != icon: self.__backgroundIcon = icon self.viewport().update() def backgroundIcon(self): return QIcon(self.__backgroundIcon) def drawBackground(self, painter, rect): QGraphicsView.drawBackground(self, painter, rect) if not self.__backgroundIcon.isNull(): painter.setClipRect(rect) vrect = QRect(QPoint(0, 0), self.viewport().size()) vrect = self.mapToScene(vrect).boundingRect() pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo( QSize(200, 200))) pmrect = QRect(QPoint(0, 0), pm.size()) pmrect.moveCenter(vrect.center().toPoint()) if rect.toRect().intersects(pmrect): painter.drawPixmap(pmrect, pm)
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) features = Input("Features", AttributeList) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) features = Output("Features", Table, dynamic=False) settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting(None) attr_y = ContextSetting(None) selection = Setting(None, schema_only=True) graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QPen(self.palette().color(QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) self.cb_attr_x = gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) gui.valueSlider( box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox( gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter numeric values', callback=self.reset_graph_data) self.sampling = gui.auto_commit( self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) g = self.graph.gui g.point_properties_box(self.controlArea) self.models = [self.xy_model] + g.points_models box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox( box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') self.cb_class_density = gui.checkBox( box, self, value='graph.class_density', label='Show class density', callback=self.update_density) self.cb_reg_line = gui.checkBox( box, self, value='graph.show_reg_line', label='Show regression line', callback=self.update_regression_line) gui.checkBox( box, self, 'graph.label_only_selected', 'Label only selected points', callback=self.graph.update_labels) self.zoom_select_toolbar = g.zoom_select_toolbar( gui.vBox(self.controlArea, "Zoom/Select"), nomargin=True, buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset] ) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect(self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QAction( "Zoom in", self, triggered=lambda: zoom(1.25) ) zoom_in.setShortcuts([QKeySequence(QKeySequence.ZoomIn), QKeySequence(self.tr("Ctrl+="))]) zoom_out = QAction( "Zoom out", self, shortcut=QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25) ) zoom_fit = QAction( "Fit in view", self, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view ) self.addActions([zoom_in, zoom_out, zoom_fit]) def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() @Inputs.data def set_data(self, data): self.clear_messages() self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank.initialize() self.vizrank.attrs = self.data.domain.attributes if self.data is not None else [] self.vizrank_button.setEnabled( self.data is not None and not self.data.is_sparse() and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) if self.data is not None and self.data.domain.class_var is None \ and len(self.data.domain.attributes) > 1 and len(self.data) > 1: self.vizrank_button.setToolTip( "Data with a class variable is required.") else: self.vizrank_button.setToolTip("") self.openContext(self.data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Orange.data.Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.graph.attr_label, str): self.graph.attr_label = findvar( self.graph.attr_label, self.graph.gui.label_model) if isinstance(self.graph.attr_color, str): self.graph.attr_color = findvar( self.graph.attr_color, self.graph.gui.color_model) if isinstance(self.graph.attr_shape, str): self.graph.attr_shape = findvar( self.graph.attr_shape, self.graph.gui.shape_model) if isinstance(self.graph.attr_size, str): self.graph.attr_size = findvar( self.graph.attr_size, self.graph.gui.size_model) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [a for a in data.domain.attributes + data.domain.metas if a.is_primitive()] new_metas = [m for m in data.domain.metas if not m.is_primitive()] new_domain = Domain(new_attrs, data.domain.class_vars, new_metas) data = data.transform(new_domain) return data @Inputs.data_subset def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) self.controls.graph.alpha_value.setEnabled(subset_data is None) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.sparse_to_dense(self.data_metas_X), self.sparse_to_dense(self.subset_data)) if self.attribute_selection_list and self.graph.domain and \ all(attr in self.graph.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.apply_selection() self.unconditional_commit() def prepare_data(self): """ Only when dealing with sparse matrices. GH-2152 """ self.graph.new_data(self.sparse_to_dense(self.data_metas_X), self.sparse_to_dense(self.subset_data), new=False) def sparse_to_dense(self, input_data=None): if input_data is None or not input_data.is_sparse(): return input_data keys = [] attrs = {self.attr_x, self.attr_y, self.graph.attr_color, self.graph.attr_shape, self.graph.attr_size, self.graph.attr_label} for i, attr in enumerate(input_data.domain): if attr in attrs: keys.append(i) new_domain = input_data.domain.select_columns(keys) dmx = input_data.transform(new_domain) dmx.X = dmx.X.toarray() # TODO: remove once we make sure Y is always dense. if sp.issparse(dmx.Y): dmx.Y = dmx.Y.toarray() return dmx def apply_selection(self): """Apply selection saved in workflow.""" if self.data is not None and self.selection is not None: self.graph.selection = np.zeros(len(self.data), dtype=np.uint8) self.selection = [x for x in self.selection if x < len(self.data)] self.graph.selection[self.selection] = 1 self.graph.update_colors(keep_colors=True) @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x self.graph.attr_color = domain and self.data.domain.class_var or None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): self.prepare_data() self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.send_features() def update_colors(self): self.prepare_data() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() @staticmethod def create_groups_table(data, selection): if data is None: return None names = [var.name for var in data.domain.variables + data.domain.metas] name = get_next_name(names, "Selection group") metas = data.domain.metas + ( DiscreteVariable( name, ["Unselected"] + ["G{}".format(i + 1) for i in range(np.max(selection))]), ) domain = Domain(data.domain.attributes, data.domain.class_vars, metas) table = data.transform(domain) table.metas[:, len(data.domain.metas):] = \ selection.reshape(len(data), 1) return table def send_data(self): selected = None selection = None # TODO: Implement selection for sql data graph = self.graph if isinstance(self.data, SqlTable): selected = self.data elif self.data is not None: selection = graph.get_selection() if len(selection) > 0: selected = self.data[selection] if graph.selection is not None and np.max(graph.selection) > 1: annotated = self.create_groups_table(self.data, graph.selection) else: annotated = create_annotated_table(self.data, selection) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) # Store current selection in a setting that is stored in workflow if self.selection is not None and len(selection): self.selection = list(selection) def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"),)) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.Outputs.features.send(features) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert(( ("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear()
class CanvasView(QGraphicsView): """Canvas View handles the zooming. """ def __init__(self, *args): QGraphicsView.__init__(self, *args) self.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.__backgroundIcon = QIcon() self.__autoScroll = False self.__autoScrollMargin = 16 self.__autoScrollTimer = QTimer(self) self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance) def setScene(self, scene): QGraphicsView.setScene(self, scene) self._ensureSceneRect(scene) def _ensureSceneRect(self, scene): r = scene.addRect(QRectF(0, 0, 400, 400)) scene.sceneRect() scene.removeItem(r) def setAutoScrollMargin(self, margin): self.__autoScrollMargin = margin def autoScrollMargin(self): return self.__autoScrollMargin def setAutoScroll(self, enable): self.__autoScroll = enable def autoScroll(self): return self.__autoScroll def mousePressEvent(self, event): QGraphicsView.mousePressEvent(self, event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: if not self.__autoScrollTimer.isActive() and \ self.__shouldAutoScroll(event.pos()): self.__startAutoScroll() QGraphicsView.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if event.button() & Qt.LeftButton: self.__stopAutoScroll() return QGraphicsView.mouseReleaseEvent(self, event) def __shouldAutoScroll(self, pos): if self.__autoScroll: margin = self.__autoScrollMargin viewrect = self.contentsRect() rect = viewrect.adjusted(margin, margin, -margin, -margin) # only do auto scroll when on the viewport's margins return not rect.contains(pos) and viewrect.contains(pos) else: return False def __startAutoScroll(self): self.__autoScrollTimer.start(10) log.debug("Auto scroll timer started") def __stopAutoScroll(self): if self.__autoScrollTimer.isActive(): self.__autoScrollTimer.stop() log.debug("Auto scroll timer stopped") def __autoScrollAdvance(self): """Advance the auto scroll """ pos = QCursor.pos() pos = self.mapFromGlobal(pos) margin = self.__autoScrollMargin vvalue = self.verticalScrollBar().value() hvalue = self.horizontalScrollBar().value() vrect = QRect(0, 0, self.width(), self.height()) # What should be the speed advance = 10 # We only do auto scroll if the mouse is inside the view. if vrect.contains(pos): if pos.x() < vrect.left() + margin: self.horizontalScrollBar().setValue(hvalue - advance) if pos.y() < vrect.top() + margin: self.verticalScrollBar().setValue(vvalue - advance) if pos.x() > vrect.right() - margin: self.horizontalScrollBar().setValue(hvalue + advance) if pos.y() > vrect.bottom() - margin: self.verticalScrollBar().setValue(vvalue + advance) if self.verticalScrollBar().value() == vvalue and \ self.horizontalScrollBar().value() == hvalue: self.__stopAutoScroll() else: self.__stopAutoScroll() log.debug("Auto scroll advance") def setBackgroundIcon(self, icon): if not isinstance(icon, QIcon): raise TypeError("A QIcon expected.") if self.__backgroundIcon != icon: self.__backgroundIcon = icon self.viewport().update() def backgroundIcon(self): return QIcon(self.__backgroundIcon) def drawBackground(self, painter, rect): QGraphicsView.drawBackground(self, painter, rect) if not self.__backgroundIcon.isNull(): painter.setClipRect(rect) vrect = QRect(QPoint(0, 0), self.viewport().size()) vrect = self.mapToScene(vrect).boundingRect() pm = self.__backgroundIcon.pixmap( vrect.size().toSize().boundedTo(QSize(200, 200)) ) pmrect = QRect(QPoint(0, 0), pm.size()) pmrect.moveCenter(vrect.center().toPoint()) if rect.toRect().intersects(pmrect): painter.drawPixmap(pmrect, pm)
class OWMDS(OWWidget): name = "MDS" description = "Two-dimensional data projection by multidimensional " \ "scaling constructed from a distance matrix." icon = "icons/MDS.svg" class Inputs: data = Input("Data", Orange.data.Table, default=True) distances = Input("Distances", Orange.misc.DistMatrix) data_subset = Input("Data Subset", Orange.data.Table) class Outputs: selected_data = Output("Selected Data", Orange.data.Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table) settings_version = 2 #: Initialization type PCA, Random = 0, 1 #: Refresh rate RefreshRate = [ ("Every iteration", 1), ("Every 5 steps", 5), ("Every 10 steps", 10), ("Every 25 steps", 25), ("Every 50 steps", 50), ("None", -1) ] #: Runtime state Running, Finished, Waiting = 1, 2, 3 settingsHandler = settings.DomainContextHandler() max_iter = settings.Setting(300) initialization = settings.Setting(PCA) refresh_rate = settings.Setting(3) # output embedding role. NoRole, AttrRole, AddAttrRole, MetaRole = 0, 1, 2, 3 auto_commit = settings.Setting(True) selection_indices = settings.Setting(None, schema_only=True) #: Percentage of all pairs displayed (ranges from 0 to 20) connected_pairs = settings.Setting(5) legend_anchor = settings.Setting(((1, 0), (1, 0))) graph = SettingProvider(OWMDSGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Error(OWWidget.Error): not_enough_rows = Msg("Input data needs at least 2 rows") matrix_too_small = Msg("Input matrix must be at least 2x2") no_attributes = Msg("Data has no attributes") mismatching_dimensions = \ Msg("Data and distances dimensions do not match.") out_of_memory = Msg("Out of memory") optimization_error = Msg("Error during optimization\n{}") def __init__(self): super().__init__() #: Input dissimilarity matrix self.matrix = None # type: Optional[Orange.misc.DistMatrix] #: Effective data used for plot styling/annotations. Can be from the #: input signal (`self.signal_data`) or the input matrix #: (`self.matrix.data`) self.data = None # type: Optional[Orange.data.Table] #: Input subset data table self.subset_data = None # type: Optional[Orange.data.Table] #: Data table from the `self.matrix.row_items` (if present) self.matrix_data = None # type: Optional[Orange.data.Table] #: Input data table self.signal_data = None self._similar_pairs = None self._subset_mask = None # type: Optional[np.ndarray] self._invalidated = False self.effective_matrix = None self._curve = None self.variable_x = ContinuousVariable("mds-x") self.variable_y = ContinuousVariable("mds-y") self.__update_loop = None # timer for scheduling updates self.__timer = QTimer(self, singleShot=True, interval=0) self.__timer.timeout.connect(self.__next_step) self.__state = OWMDS.Waiting self.__in_next_step = False self.__draw_similar_pairs = False box = gui.vBox(self.controlArea, "MDS Optimization") form = QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10 ) form.addRow( "Max iterations:", gui.spin(box, self, "max_iter", 10, 10 ** 4, step=1)) form.addRow( "Initialization:", gui.radioButtons(box, self, "initialization", btnLabels=("PCA (Torgerson)", "Random"), callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow( "Refresh:", gui.comboBox(box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) gui.separator(box, 10) self.runbutton = gui.button(box, self, "Run", callback=self._toggle_run) box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWMDSGraph(self, box, "MDSGraph", view_box=MDSInteractiveViewBox) box.layout().addWidget(self.graph.plot_widget) self.plot = self.graph.plot_widget g = self.graph.gui box = g.point_properties_box(self.controlArea) self.models = g.points_models self.size_model = self.models[2] self.label_model = self.models[3] self.size_model.order = \ self.size_model.order[:1] + ("Stress", ) + self.models[2].order[1:] gui.hSlider(box, self, "connected_pairs", label="Show similar pairs:", minValue=0, maxValue=20, createLabel=False, callback=self._on_connected_changed) g.add_widgets(ids=[g.JitterSizeSlider], widget=box) box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ToolTipShowsAll, g.ClassDensity, g.LabelOnlySelected], box) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict palette = self.graph.plot_widget.palette() self.graph.set_palette(palette) gui.rubber(self.controlArea) self.graph.box_zoom_select(self.controlArea) gui.auto_commit(box, self, "auto_commit", "Send Selected", checkbox_label="Send selected automatically", box=None) self.plot.getPlotItem().hideButtons() self.plot.setRenderHint(QPainter.Antialiasing) self.graph.jitter_continuous = True self._initialize() def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() self.connect_pairs() def update_colors(self): pass def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def init_attr_values(self): self.graph.set_domain(self.data) def prepare_data(self): pass def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.variable_x, self.variable_y, True) def selection_changed(self): self.commit() @Inputs.data @check_sql_input def set_data(self, data): """Set the input dataset. Parameters ---------- data : Optional[Orange.data.Table] """ if data is not None and len(data) < 2: self.Error.not_enough_rows() data = None else: self.Error.not_enough_rows.clear() self.signal_data = data if self.matrix is not None and data is not None and len(self.matrix) == len(data): self.closeContext() self.data = data self.init_attr_values() self.openContext(data) else: self._invalidated = True @Inputs.distances def set_disimilarity(self, matrix): """Set the dissimilarity (distance) matrix. Parameters ---------- matrix : Optional[Orange.misc.DistMatrix] """ if matrix is not None and len(matrix) < 2: self.Error.matrix_too_small() matrix = None else: self.Error.matrix_too_small.clear() self.matrix = matrix self.matrix_data = matrix.row_items if matrix is not None else None self._invalidated = True @Inputs.data_subset def set_subset_data(self, subset_data): """Set a subset of `data` input to highlight in the plot. Parameters ---------- subset_data: Optional[Orange.data.Table] """ self.subset_data = subset_data # invalidate the pen/brush when the subset is changed self._subset_mask = None # type: Optional[np.ndarray] self.controls.graph.alpha_value.setEnabled(subset_data is None) def _clear(self): self._similar_pairs = None self.__set_update_loop(None) self.__state = OWMDS.Waiting def _clear_plot(self): self.graph.plot_widget.clear() def _initialize(self): # clear everything self.closeContext() self._clear() self.Error.clear() self.data = None self.effective_matrix = None self.embedding = None self.init_attr_values() # if no data nor matrix is present reset plot if self.signal_data is None and self.matrix is None: return if self.signal_data is not None and self.matrix is not None and \ len(self.signal_data) != len(self.matrix): self.Error.mismatching_dimensions() self._update_plot() return if self.signal_data is not None: self.data = self.signal_data elif self.matrix_data is not None: self.data = self.matrix_data if self.matrix is not None: self.effective_matrix = self.matrix if self.matrix.axis == 0 and self.data is self.matrix_data: self.data = None elif self.data.domain.attributes: preprocessed_data = Orange.projection.MDS().preprocess(self.data) self.effective_matrix = Orange.distance.Euclidean(preprocessed_data) else: self.Error.no_attributes() return self.init_attr_values() self.openContext(self.data) def _toggle_run(self): if self.__state == OWMDS.Running: self.stop() self._invalidate_output() else: self.start() def start(self): if self.__state == OWMDS.Running: return elif self.__state == OWMDS.Finished: # Resume/continue from a previous run self.__start() elif self.__state == OWMDS.Waiting and \ self.effective_matrix is not None: self.__start() def stop(self): if self.__state == OWMDS.Running: self.__set_update_loop(None) def __start(self): self.__draw_similar_pairs = False X = self.effective_matrix init = self.embedding # number of iterations per single GUI update step _, step_size = OWMDS.RefreshRate[self.refresh_rate] if step_size == -1: step_size = self.max_iter def update_loop(X, max_iter, step, init): """ return an iterator over successive improved MDS point embeddings. """ # NOTE: this code MUST NOT call into QApplication.processEvents done = False iterations_done = 0 oldstress = np.finfo(np.float).max init_type = "PCA" if self.initialization == OWMDS.PCA else "random" while not done: step_iter = min(max_iter - iterations_done, step) mds = Orange.projection.MDS( dissimilarity="precomputed", n_components=2, n_init=1, max_iter=step_iter, init_type=init_type, init_data=init) mdsfit = mds(X) iterations_done += step_iter embedding, stress = mdsfit.embedding_, mdsfit.stress_ stress /= np.sqrt(np.sum(embedding ** 2, axis=1)).sum() if iterations_done >= max_iter: done = True elif (oldstress - stress) < mds.params["eps"]: done = True init = embedding oldstress = stress yield embedding, mdsfit.stress_, iterations_done / max_iter self.__set_update_loop(update_loop(X, self.max_iter, step_size, init)) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): """ Set the update `loop` coroutine. The `loop` is a generator yielding `(embedding, stress, progress)` tuples where `embedding` is a `(N, 2) ndarray` of current updated MDS points, `stress` is the current stress and `progress` a float ratio (0 <= progress <= 1) If an existing update coroutine loop is already in place it is interrupted (i.e. closed). .. note:: The `loop` must not explicitly yield control flow to the event loop (i.e. call `QApplication.processEvents`) """ if self.__update_loop is not None: self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.setBlocking(True) self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWMDS.Running self.__timer.start() else: self.setBlocking(False) self.setStatusMessage("") self.runbutton.setText("Start") self.__state = OWMDS.Finished self.__timer.stop() def __next_step(self): if self.__update_loop is None: return assert not self.__in_next_step self.__in_next_step = True loop = self.__update_loop self.Error.out_of_memory.clear() try: embedding, _, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__set_update_loop(None) self.unconditional_commit() self.__draw_similar_pairs = True self._update_plot() except MemoryError: self.Error.out_of_memory() self.__set_update_loop(None) self.__draw_similar_pairs = True except Exception as exc: self.Error.optimization_error(str(exc)) self.__set_update_loop(None) self.__draw_similar_pairs = True else: self.progressBarSet(100.0 * progress, processEvents=None) self.embedding = embedding self._update_plot() # schedule next update self.__timer.start() self.__in_next_step = False def __invalidate_embedding(self): # reset/invalidate the MDS embedding, to the default initialization # (Random or PCA), restarting the optimization if necessary. if self.embedding is None: return state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) X = self.effective_matrix if self.initialization == OWMDS.PCA: self.embedding = torgerson(X) else: self.embedding = np.random.rand(len(X), 2) self._update_plot() # restart the optimization if it was interrupted. if state == OWMDS.Running: self.__start() def __invalidate_refresh(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) # restart the optimization if it was interrupted. # TODO: decrease the max iteration count by the already # completed iterations count. if state == OWMDS.Running: self.__start() def handleNewSignals(self): if self._invalidated: self.__draw_similar_pairs = False self._invalidated = False self._initialize() self.start() if self._subset_mask is None and self.subset_data is not None and \ self.data is not None: self._subset_mask = np.in1d(self.data.ids, self.subset_data.ids) self._update_plot(new=True) self.unconditional_commit() def _invalidate_output(self): self.commit() def _on_connected_changed(self): self._similar_pairs = None self.connect_pairs() def _update_plot(self, new=False): self._clear_plot() if self.embedding is not None: self._setup_plot(new=new) else: self.graph.new_data(None) def connect_pairs(self): if self._curve: self.graph.plot_widget.removeItem(self._curve) if not (self.connected_pairs and self.__draw_similar_pairs): return emb_x, emb_y = self.graph.get_xy_data_positions( self.variable_x, self.variable_y, self.graph.valid_data) if self._similar_pairs is None: # This code requires storing lower triangle of X (n x n / 2 # doubles), n x n / 2 * 2 indices to X, n x n / 2 indices for # argsort result. If this becomes an issue, it can be reduced to # n x n argsort indices by argsorting the entire X. Then we # take the first n + 2 * p indices. We compute their coordinates # i, j in the original matrix. We keep those for which i < j. # n + 2 * p will suffice to exclude the diagonal (i = j). If the # number of those for which i < j is smaller than p, we instead # take i > j. Among those that remain, we take the first p. # Assuming that MDS can't show so many points that memory could # become an issue, I preferred using simpler code. m = self.effective_matrix n = len(m) p = min(n * (n - 1) // 2 * self.connected_pairs // 100, MAX_N_PAIRS * self.connected_pairs // 20) indcs = np.triu_indices(n, 1) sorted = np.argsort(m[indcs])[:p] self._similar_pairs = fpairs = np.empty(2 * p, dtype=int) fpairs[::2] = indcs[0][sorted] fpairs[1::2] = indcs[1][sorted] emb_x_pairs = emb_x[self._similar_pairs].reshape((-1, 2)) emb_y_pairs = emb_y[self._similar_pairs].reshape((-1, 2)) # Filter out zero distance lines (in embedding coords). # Null (zero length) line causes bad rendering artifacts # in Qt when using the raster graphics system (see gh-issue: 1668). (x1, x2), (y1, y2) = (emb_x_pairs.T, emb_y_pairs.T) pairs_mask = ~(np.isclose(x1, x2) & np.isclose(y1, y2)) emb_x_pairs = emb_x_pairs[pairs_mask, :] emb_y_pairs = emb_y_pairs[pairs_mask, :] self._curve = pg.PlotCurveItem( emb_x_pairs.ravel(), emb_y_pairs.ravel(), pen=pg.mkPen(0.8, width=2, cosmetic=True), connect="pairs", antialias=True) self.graph.plot_widget.addItem(self._curve) def _setup_plot(self, new=False): emb_x, emb_y = self.embedding[:, 0], self.embedding[:, 1] coords = np.vstack((emb_x, emb_y)).T data = self.data attributes = data.domain.attributes + (self.variable_x, self.variable_y) domain = Domain(attributes=attributes, class_vars=data.domain.class_vars, metas=data.domain.metas) data = Table.from_numpy(domain, X=hstack((data.X, coords)), Y=data.Y, metas=data.metas) subset_data = data[self._subset_mask] if self._subset_mask is not None else None self.graph.new_data(data, subset_data=subset_data, new=new) self.graph.update_data(self.variable_x, self.variable_y, True) self.connect_pairs() def commit(self): if self.embedding is not None: names = get_unique_names([v.name for v in self.data.domain.variables], ["mds-x", "mds-y"]) output = embedding = Orange.data.Table.from_numpy( Orange.data.Domain([ContinuousVariable(names[0]), ContinuousVariable(names[1])]), self.embedding ) else: output = embedding = None if self.embedding is not None and self.data is not None: domain = self.data.domain domain = Orange.data.Domain(domain.attributes, domain.class_vars, domain.metas + embedding.domain.attributes) output = self.data.transform(domain) output.metas[:, -2:] = embedding.X selection = self.graph.get_selection() if output is not None and len(selection) > 0: selected = output[selection] else: selected = None if self.graph.selection is not None and np.max(self.graph.selection) > 1: annotated = create_groups_table(output, self.graph.selection) else: annotated = create_annotated_table(output, selection) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) def onDeleteWidget(self): super().onDeleteWidget() self._clear_plot() self._clear() def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert(( ("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", self.graph.jitter_size != 0 and "{} %".format(self.graph.jitter_size)))) self.report_plot() if caption: self.report_caption(caption) @classmethod def migrate_settings(cls, settings_, version): if version < 2: settings_graph = {} for old, new in (("label_only_selected", "label_only_selected"), ("symbol_opacity", "alpha_value"), ("symbol_size", "point_width"), ("jitter", "jitter_size")): settings_graph[new] = settings_[old] settings_["graph"] = settings_graph settings_["auto_commit"] = settings_["autocommit"] @classmethod def migrate_context(cls, context, version): if version < 2: domain = context.ordered_domain n_domain = [t for t in context.ordered_domain if t[1] == 2] c_domain = [t for t in context.ordered_domain if t[1] == 1] context_values_graph = {} for _, old_val, new_val in ((domain, "color_value", "attr_color"), (c_domain, "shape_value", "attr_shape"), (n_domain, "size_value", "attr_size"), (domain, "label_value", "attr_label")): tmp = context.values[old_val] if tmp[1] >= 0: context_values_graph[new_val] = (tmp[0], tmp[1] + 100) elif tmp[0] != "Stress": context_values_graph[new_val] = None else: context_values_graph[new_val] = tmp context.values["graph"] = context_values_graph
class OWLouvainClustering(widget.OWWidget): name = 'Louvain Clustering' description = 'Detects communities in a network of nearest neighbors.' icon = 'icons/LouvainClustering.svg' priority = 2110 want_main_area = False settingsHandler = DomainContextHandler() class Inputs: data = Input('Data', Table, default=True) if Graph is not None: class Outputs: annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, default=True) graph = Output('Network', Graph) else: class Outputs: annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, default=True) apply_pca = ContextSetting(True) pca_components = ContextSetting(_DEFAULT_PCA_COMPONENTS) metric_idx = ContextSetting(0) k_neighbors = ContextSetting(_DEFAULT_K_NEIGHBORS) resolution = ContextSetting(1.) auto_commit = Setting(False) class Information(widget.OWWidget.Information): modified = Msg("Press commit to recompute clusters and send new data") class Error(widget.OWWidget.Error): empty_dataset = Msg('No features in data') general_error = Msg('Error occured during clustering\n{}') def __init__(self): super().__init__() self.data = None # type: Optional[Table] self.preprocessed_data = None # type: Optional[Table] self.pca_projection = None # type: Optional[Table] self.graph = None # type: Optional[nx.Graph] self.partition = None # type: Optional[np.array] # Use a executor with a single worker, to limit CPU overcommitment for # cancelled tasks. The method does not have a fine cancellation # granularity so we assure that there are not N - 1 jobs executing # for no reason only to be thrown away. It would be better to use the # global pool but implement a limit on jobs from this source. self.__executor = futures.ThreadPoolExecutor(max_workers=1) self.__task = None # type: Optional[TaskState] self.__invalidated = False # coalescing commit timer self.__commit_timer = QTimer(self, singleShot=True) self.__commit_timer.timeout.connect(self.commit) pca_box = gui.vBox(self.controlArea, 'PCA Preprocessing') self.apply_pca_cbx = gui.checkBox( pca_box, self, 'apply_pca', label='Apply PCA preprocessing', callback=self._invalidate_graph, ) # type: QCheckBox self.pca_components_slider = gui.hSlider( pca_box, self, 'pca_components', label='Components: ', minValue=2, maxValue=_MAX_PCA_COMPONENTS, callback=self._invalidate_pca_projection, tracking=False ) # type: QSlider graph_box = gui.vBox(self.controlArea, 'Graph parameters') self.metric_combo = gui.comboBox( graph_box, self, 'metric_idx', label='Distance metric', items=[m[0] for m in METRICS], callback=self._invalidate_graph, orientation=Qt.Horizontal, ) # type: gui.OrangeComboBox self.k_neighbors_spin = gui.spin( graph_box, self, 'k_neighbors', minv=1, maxv=_MAX_K_NEIGBOURS, label='k neighbors', controlWidth=80, alignment=Qt.AlignRight, callback=self._invalidate_graph, ) # type: gui.SpinBoxWFocusOut self.resolution_spin = gui.hSlider( graph_box, self, 'resolution', minValue=0, maxValue=5., step=1e-1, label='Resolution', intOnly=False, labelFormat='%.1f', callback=self._invalidate_partition, tracking=False, ) # type: QSlider self.resolution_spin.parent().setToolTip( 'The resolution parameter affects the number of clusters to find. ' 'Smaller values tend to produce more clusters and larger values ' 'retrieve less clusters.' ) self.apply_button = gui.auto_commit( self.controlArea, self, 'auto_commit', 'Apply', box=None, commit=lambda: self.commit(), callback=lambda: self._on_auto_commit_changed(), ) # type: QWidget def _invalidate_pca_projection(self): self.pca_projection = None if not self.apply_pca: return self._invalidate_graph() self._set_modified(True) def _invalidate_graph(self): self.graph = None self._invalidate_partition() self._set_modified(True) def _invalidate_partition(self): self.partition = None self._invalidate_output() self.Information.modified() self._set_modified(True) def _invalidate_output(self): self.__invalidated = True if self.__task is not None: self.__cancel_task(wait=False) if self.auto_commit: self.__commit_timer.start() else: self.__set_state_ready() def _set_modified(self, state): """ Mark the widget (GUI) as containing modified state. """ if self.data is None: # does not apply when we have no data state = False elif self.auto_commit: # does not apply when auto commit is on state = False self.Information.modified(shown=state) def _on_auto_commit_changed(self): if self.auto_commit and self.__invalidated: self.commit() def cancel(self): """Cancel any running jobs.""" self.__cancel_task(wait=False) self.__set_state_ready() def commit(self): self.__commit_timer.stop() self.__invalidated = False self._set_modified(False) self.Error.clear() # Cancel current running task self.__cancel_task(wait=False) if self.data is None: self.__set_state_ready() return # Make sure the dataset is ok if len(self.data.domain.attributes) < 1: self.Error.empty_dataset() self.__set_state_ready() return if self.partition is not None: self.__set_state_ready() self._send_data() return # Preprocess the dataset if self.preprocessed_data is None: louvain = Louvain(random_state=0) self.preprocessed_data = louvain.preprocess(self.data) state = TaskState(self) # Prepare/assemble the task(s) to run; reuse partial results if self.apply_pca: if self.pca_projection is not None: data = self.pca_projection pca_components = None else: data = self.preprocessed_data pca_components = self.pca_components else: data = self.preprocessed_data pca_components = None if self.graph is not None: # run on graph only; no need to do PCA and k-nn search ... graph = self.graph k_neighbors = metric = None else: k_neighbors, metric = self.k_neighbors, METRICS[self.metric_idx][1] graph = None if graph is None: task = partial( run_on_data, data, pca_components=pca_components, k_neighbors=k_neighbors, metric=metric, resolution=self.resolution, state=state ) else: task = partial( run_on_graph, graph, resolution=self.resolution, state=state ) self.__set_state_busy() self.__start_task(task, state) @Slot(object) def __set_partial_results(self, result): # type: (Tuple[str, Any]) -> None which, res = result if which == "pca_projection": assert isinstance(res, Table) and len(res) == len(self.data) self.pca_projection = res elif which == "graph": assert isinstance(res, nx.Graph) self.graph = res elif which == "partition": assert isinstance(res, np.ndarray) self.partition = res else: assert False, which @Slot(object) def __on_done(self, future): # type: (Future['Results']) -> None assert future.done() assert self.__task is not None assert self.__task.future is future assert self.__task.watcher.future() is future self.__task, task = None, self.__task task.deleteLater() self.__set_state_ready() try: result = future.result() except Exception as err: # pylint: disable=broad-except self.Error.general_error(str(err), exc_info=True) else: self.__set_results(result) @Slot(str) def setStatusMessage(self, text): super().setStatusMessage(text) @Slot(float) def progressBarSet(self, value, *a, **kw): super().progressBarSet(value, *a, **kw) def __set_state_ready(self): self.progressBarFinished() self.setBlocking(False) self.setStatusMessage("") def __set_state_busy(self): self.progressBarInit() self.setBlocking(True) def __start_task(self, task, state): # type: (Callable[[], Any], TaskState) -> None assert self.__task is None state.status_changed.connect(self.setStatusMessage) state.progress_changed.connect(self.progressBarSet) state.partial_result_ready.connect(self.__set_partial_results) state.watcher.done.connect(self.__on_done) state.start(self.__executor, task) state.setParent(self) self.__task = state def __cancel_task(self, wait=True): # Cancel and dispose of the current task if self.__task is not None: state, self.__task = self.__task, None state.cancel() state.partial_result_ready.disconnect(self.__set_partial_results) state.status_changed.disconnect(self.setStatusMessage) state.progress_changed.disconnect(self.progressBarSet) state.watcher.done.disconnect(self.__on_done) if wait: futures.wait([state.future]) state.deleteLater() else: w = FutureWatcher(state.future, parent=state) w.done.connect(state.deleteLater) def __set_results(self, results): # type: ('Results') -> None # NOTE: All of these have already been set by __set_partial_results, # we double check that they are aliases if results.pca_projection is not None: assert self.pca_components == results.pca_components assert self.pca_projection is results.pca_projection self.pca_projection = results.pca_projection if results.graph is not None: assert results.metric == METRICS[self.metric_idx][1] assert results.k_neighbors == self.k_neighbors assert self.graph is results.graph self.graph = results.graph if results.partition is not None: assert results.resolution == self.resolution assert self.partition is results.partition self.partition = results.partition self._send_data() def _send_data(self): if self.partition is None or self.data is None: return domain = self.data.domain # Compute the frequency of each cluster index counts = np.bincount(self.partition) indices = np.argsort(counts)[::-1] index_map = {n: o for n, o in zip(indices, range(len(indices)))} new_partition = list(map(index_map.get, self.partition)) cluster_var = DiscreteVariable( get_unique_names(domain, 'Cluster'), values=['C%d' % (i + 1) for i, _ in enumerate(np.unique(new_partition))] ) new_domain = add_columns(domain, metas=[cluster_var]) new_table = self.data.transform(new_domain) new_table.get_column_view(cluster_var)[0][:] = new_partition self.Outputs.annotated_data.send(new_table) if Graph is not None: graph = Graph(self.graph) graph.set_items(new_table) self.Outputs.graph.send(graph) @Inputs.data def set_data(self, data): self.closeContext() self.Error.clear() prev_data, self.data = self.data, data self.openContext(self.data) # If X hasn't changed, there's no reason to recompute clusters if prev_data and self.data and np.array_equal(self.data.X, prev_data.X): if self.auto_commit: self._send_data() return # Clear the outputs self.Outputs.annotated_data.send(None) if Graph is not None: self.Outputs.graph.send(None) # Clear internal state self.clear() self._invalidate_pca_projection() if self.data is None: return # Can't have more PCA components than the number of attributes n_attrs = len(data.domain.attributes) self.pca_components_slider.setMaximum(min(_MAX_PCA_COMPONENTS, n_attrs)) self.pca_components_slider.setValue(min(_DEFAULT_PCA_COMPONENTS, n_attrs)) # Can't have more k neighbors than there are data points self.k_neighbors_spin.setMaximum(min(_MAX_K_NEIGBOURS, len(data) - 1)) self.k_neighbors_spin.setValue(min(_DEFAULT_K_NEIGHBORS, len(data) - 1)) self.commit() def clear(self): self.__cancel_task(wait=False) self.preprocessed_data = None self.pca_projection = None self.graph = None self.partition = None self.Error.clear() self.Information.modified.clear() def onDeleteWidget(self): self.__cancel_task(wait=True) self.__executor.shutdown(True) self.clear() self.data = None super().onDeleteWidget() def send_report(self): pca = report.bool_str(self.apply_pca) if self.apply_pca: pca += report.plural(', {number} component{s}', self.pca_components) self.report_items(( ('PCA preprocessing', pca), ('Metric', METRICS[self.metric_idx][0]), ('k neighbors', self.k_neighbors), ('Resolution', self.resolution), ))
class WidgetManager(QObject): """ OWWidget instance manager class. This class handles the lifetime of OWWidget instances in a :class:`WidgetsScheme`. """ #: A new OWWidget was created and added by the manager. widget_for_node_added = Signal(SchemeNode, QWidget) #: An OWWidget was removed, hidden and will be deleted when appropriate. widget_for_node_removed = Signal(SchemeNode, QWidget) class ProcessingState(enum.IntEnum): """Widget processing state flags""" #: Signal manager is updating/setting the widget's inputs InputUpdate = 1 #: Widget has entered a blocking state (OWWidget.isBlocking) BlockingUpdate = 2 #: Widget has entered processing state ProcessingUpdate = 4 #: Widget is still in the process of initialization Initializing = 8 InputUpdate, BlockingUpdate, ProcessingUpdate, Initializing = ProcessingState #: Widget initialization states Delayed = namedtuple( "Delayed", ["node"]) PartiallyInitialized = namedtuple( "Materializing", ["node", "partially_initialized_widget"]) Materialized = namedtuple( "Materialized", ["node", "widget"]) class CreationPolicy(enum.Enum): """Widget Creation Policy""" #: Widgets are scheduled to be created from the event loop, or when #: first accessed with `widget_for_node` Normal = "Normal" #: Widgets are created immediately when added to the workflow model Immediate = "Immediate" #: Widgets are created only when first accessed with `widget_for_node` OnDemand = "OnDemand" Normal, Immediate, OnDemand = CreationPolicy def __init__(self, parent=None): QObject.__init__(self, parent) self.__scheme = None self.__signal_manager = None self.__widgets = [] self.__initstate_for_node = {} self.__creation_policy = WidgetManager.Normal #: a queue of all nodes whose widgets are scheduled for #: creation/initialization self.__init_queue = deque() # type: Deque[SchemeNode] #: Timer for scheduling widget initialization self.__init_timer = QTimer(self, interval=0, singleShot=True) self.__init_timer.timeout.connect(self.__create_delayed) #: A mapping of SchemeNode -> OWWidget (note: a mapping is only added #: after the widget is actually created) self.__widget_for_node = {} #: a mapping of OWWidget -> SchemeNode self.__node_for_widget = {} # Widgets that were 'removed' from the scheme but were at # the time in an input update loop and could not be deleted # immediately self.__delay_delete = set() #: Deleted/removed during creation/initialization. self.__delete_after_create = [] #: processing state flags for all widgets (including the ones #: in __delay_delete). #: Note: widgets which have not yet been created do not have an entry self.__widget_processing_state = {} # Tracks the widget in the update loop by the SignalManager self.__updating_widget = None def set_scheme(self, scheme): """ Set the :class:`WidgetsScheme` instance to manage. """ self.__scheme = scheme self.__signal_manager = scheme.findChild(SignalManager) self.__signal_manager.processingStarted[SchemeNode].connect( self.__on_processing_started ) self.__signal_manager.processingFinished[SchemeNode].connect( self.__on_processing_finished ) scheme.node_added.connect(self.add_widget_for_node) scheme.node_removed.connect(self.remove_widget_for_node) scheme.runtime_env_changed.connect(self.__on_env_changed) scheme.installEventFilter(self) def scheme(self): """ Return the scheme instance on which this manager is installed. """ return self.__scheme def signal_manager(self): """ Return the signal manager in use on the :func:`scheme`. """ return self.__signal_manager def widget_for_node(self, node): """ Return the OWWidget instance for the scheme node. """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Delayed): # Create the widget now if it is still pending state = self.__materialize(state) return state.widget elif isinstance(state, WidgetManager.PartiallyInitialized): widget = state.partially_initialized_widget log.warning("WidgetManager.widget_for_node: " "Accessing a partially created widget instance. " "This is most likely a result of explicit " "QApplication.processEvents call from the '%s.%s' " "widgets __init__.", type(widget).__module__, type(widget).__name__) return widget elif isinstance(state, WidgetManager.Materialized): return state.widget else: assert False def node_for_widget(self, widget): """ Return the SchemeNode instance for the OWWidget. Raise a KeyError if the widget does not map to a node in the scheme. """ return self.__node_for_widget[widget] def widget_properties(self, node): """ Return the current widget properties/settings. Parameters ---------- node : SchemeNode Returns ------- settings : dict """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Materialized): return state.widget.settingsHandler.pack_data(state.widget) else: return node.properties def set_creation_policy(self, policy): """ Set the widget creation policy Parameters ---------- policy : WidgetManager.CreationPolicy """ if self.__creation_policy != policy: self.__creation_policy = policy if self.__creation_policy == WidgetManager.Immediate: self.__init_timer.stop() while self.__init_queue: state = self.__init_queue.popleft() self.__materialize(state) elif self.__creation_policy == WidgetManager.Normal: if not self.__init_timer.isActive() and self.__init_queue: self.__init_timer.start() elif self.__creation_policy == WidgetManager.OnDemand: self.__init_timer.stop() else: assert False def creation_policy(self): """ Return the current widget creation policy Returns ------- policy: WidgetManager.CreationPolicy """ return self.__creation_policy def add_widget_for_node(self, node): """ Create a new OWWidget instance for the corresponding scheme node. """ state = WidgetManager.Delayed(node) self.__initstate_for_node[node] = state if self.__creation_policy == WidgetManager.Immediate: self.__initstate_for_node[node] = self.__materialize(state) elif self.__creation_policy == WidgetManager.Normal: self.__init_queue.append(state) if not self.__init_timer.isActive(): self.__init_timer.start() elif self.__creation_policy == WidgetManager.OnDemand: self.__init_queue.append(state) def __materialize(self, state): # Create and initialize an OWWidget for a Delayed # widget initialization assert isinstance(state, WidgetManager.Delayed) if state in self.__init_queue: self.__init_queue.remove(state) node = state.node widget = self.create_widget_instance(node) self.__widgets.append(widget) self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__initialize_widget_state(node, widget) state = WidgetManager.Materialized(node, widget) self.__initstate_for_node[node] = state self.widget_for_node_added.emit(node, widget) return state def remove_widget_for_node(self, node): """ Remove the OWWidget instance for node. """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Delayed): del self.__initstate_for_node[node] self.__init_queue.remove(state) elif isinstance(state, WidgetManager.Materialized): # Update the node's stored settings/properties dict before # removing the widget. # TODO: Update/sync whenever the widget settings change. node.properties = self._widget_settings(state.widget) self.__widgets.remove(state.widget) del self.__initstate_for_node[node] del self.__widget_for_node[node] del self.__node_for_widget[state.widget] node.title_changed.disconnect(state.widget.setCaption) state.widget.progressBarValueChanged.disconnect(node.set_progress) self.widget_for_node_removed.emit(node, state.widget) self._delete_widget(state.widget) elif isinstance(state, WidgetManager.PartiallyInitialized): widget = state.partially_initialized_widget raise RuntimeError( "A widget/node {} was removed while being initialized. " "This is most likely a result of an explicit " "QApplication.processEvents call from the '{}.{}' " "widgets __init__.\n" .format(state.node.title, type(widget).__module__, type(widget).__init__)) def _widget_settings(self, widget): return widget.settingsHandler.pack_data(widget) def _delete_widget(self, widget): """ Delete the OWBaseWidget instance. """ widget.close() # Save settings to user global settings. widget.saveSettings() # Notify the widget it will be deleted. widget.onDeleteWidget() if self.__widget_processing_state[widget] != 0: # If the widget is in an update loop and/or blocking we # delay the scheduled deletion until the widget is done. self.__delay_delete.add(widget) else: widget.deleteLater() del self.__widget_processing_state[widget] def create_widget_instance(self, node): """ Create a OWWidget instance for the node. """ desc = node.description klass = widget = None initialized = False error = None # First try to actually retrieve the class. try: klass = name_lookup(desc.qualified_name) except (ImportError, AttributeError): sys.excepthook(*sys.exc_info()) error = "Could not import {0!r}\n\n{1}".format( node.description.qualified_name, traceback.format_exc() ) except Exception: sys.excepthook(*sys.exc_info()) error = "An unexpected error during import of {0!r}\n\n{1}".format( node.description.qualified_name, traceback.format_exc() ) if klass is None: widget = mock_error_owwidget(node, error) initialized = True if widget is None: log.info("WidgetManager: Creating '%s.%s' instance '%s'.", klass.__module__, klass.__name__, node.title) widget = klass.__new__( klass, None, captionTitle=node.title, signal_manager=self.signal_manager(), stored_settings=node.properties, # NOTE: env is a view of the real env and reflects # changes to the environment. env=self.scheme().runtime_env() ) initialized = False # Init the node/widget mapping and state before calling __init__ # Some OWWidgets might already send data in the constructor # (should this be forbidden? Raise a warning?) triggering the signal # manager which would request the widget => node mapping or state # Furthermore they can (though they REALLY REALLY REALLY should not) # explicitly call qApp.processEvents. assert node not in self.__widget_for_node self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__widget_processing_state[widget] = WidgetManager.Initializing self.__initstate_for_node[node] = \ WidgetManager.PartiallyInitialized(node, widget) if not initialized: try: widget.__init__() except Exception: sys.excepthook(*sys.exc_info()) msg = traceback.format_exc() msg = "Could not create {0!r}\n\n{1}".format( node.description.name, msg ) # remove state tracking for widget ... del self.__widget_for_node[node] del self.__node_for_widget[widget] del self.__widget_processing_state[widget] # ... and substitute it with a mock error widget. widget = mock_error_owwidget(node, msg) self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__widget_processing_state[widget] = 0 self.__initstate_for_node[node] = \ WidgetManager.Materialized(node, widget) self.__initstate_for_node[node] = \ WidgetManager.Materialized(node, widget) # Clear Initializing flag self.__widget_processing_state[widget] &= ~WidgetManager.Initializing node.title_changed.connect(widget.setCaption) # Widget's info/warning/error messages. widget.messageActivated.connect(self.__on_widget_state_changed) widget.messageDeactivated.connect(self.__on_widget_state_changed) # Widget's statusTip node.set_status_message(widget.statusMessage()) widget.statusMessageChanged.connect(node.set_status_message) # Widget's progress bar value state. widget.progressBarValueChanged.connect(node.set_progress) # Widget processing state (progressBarInit/Finished) # and the blocking state. widget.processingStateChanged.connect( self.__on_processing_state_changed ) widget.blockingStateChanged.connect(self.__on_blocking_state_changed) if widget.isBlocking(): # A widget can already enter blocking state in __init__ self.__widget_processing_state[widget] |= self.BlockingUpdate if widget.processingState != 0: # It can also start processing (initialization of resources, ...) self.__widget_processing_state[widget] |= self.ProcessingUpdate node.set_processing_state(1) node.set_progress(widget.progressBarValue) # Install a help shortcut on the widget help_shortcut = QShortcut(QKeySequence("F1"), widget) help_shortcut.activated.connect(self.__on_help_request) # Up shortcut (activate/open parent) up_shortcut = QShortcut( QKeySequence(Qt.ControlModifier + Qt.Key_Up), widget) up_shortcut.activated.connect(self.__on_activate_parent) # Call setters only after initialization. widget.setWindowIcon( icon_loader.from_description(desc).get(desc.icon) ) widget.setCaption(node.title) # Schedule an update with the signal manager, due to the cleared # implicit Initializing flag self.signal_manager()._update() return widget def node_processing_state(self, node): """ Return the processing state flags for the node. Same as `manager.widget_processing_state(manger.widget_for_node(node))` """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Materialized): return self.__widget_processing_state[state.widget] elif isinstance(state, WidgetManager.PartiallyInitialized): return self.__widget_processing_state[state.partially_initialized_widget] else: return WidgetManager.Initializing def widget_processing_state(self, widget): """ Return the processing state flags for the widget. The state is an bitwise or of `InputUpdate` and `BlockingUpdate`. """ return self.__widget_processing_state[widget] def __create_delayed(self): if self.__init_queue: state = self.__init_queue.popleft() node = state.node self.__initstate_for_node[node] = self.__materialize(state) if self.__creation_policy == WidgetManager.Normal and \ self.__init_queue: # restart the timer if pending widgets still in the queue self.__init_timer.start() def eventFilter(self, receiver, event): if event.type() == QEvent.Close and receiver is self.__scheme: self.signal_manager().stop() # Notify the widget instances. for widget in list(self.__widget_for_node.values()): widget.close() widget.saveSettings() widget.onDeleteWidget() event.accept() return True return QObject.eventFilter(self, receiver, event) def __on_help_request(self): """ Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to the scheme and hope someone responds to it. """ # Sender is the QShortcut, and parent the OWBaseWidget widget = self.sender().parent() try: node = self.node_for_widget(widget) except KeyError: pass else: qualified_name = node.description.qualified_name help_url = "help://search?" + urlencode({"id": qualified_name}) event = QWhatsThisClickedEvent(help_url) QCoreApplication.sendEvent(self.scheme(), event) def __on_activate_parent(self): """ Activate parent shortcut was pressed. """ event = ActivateParentEvent() QCoreApplication.sendEvent(self.scheme(), event) def __initialize_widget_state(self, node, widget): """ Initialize the tracked info/warning/error message state. """ for message_group in widget.message_groups: message = user_message_from_state(message_group) if message: node.set_state_message(message) def __on_widget_state_changed(self, msg): """ The OWBaseWidget info/warning/error state has changed. """ widget = msg.group.widget try: node = self.node_for_widget(widget) except KeyError: pass else: self.__initialize_widget_state(node, widget) def __on_processing_state_changed(self, state): """ A widget processing state has changed (progressBarInit/Finished) """ widget = self.sender() try: node = self.node_for_widget(widget) except KeyError: return if state: self.__widget_processing_state[widget] |= self.ProcessingUpdate else: self.__widget_processing_state[widget] &= ~self.ProcessingUpdate self.__update_node_processing_state(node) def __on_processing_started(self, node): """ Signal manager entered the input update loop for the node. """ widget = self.widget_for_node(node) # Remember the widget instance. The node and the node->widget mapping # can be removed between this and __on_processing_finished. self.__updating_widget = widget self.__widget_processing_state[widget] |= self.InputUpdate self.__update_node_processing_state(node) def __on_processing_finished(self, node): """ Signal manager exited the input update loop for the node. """ widget = self.__updating_widget self.__widget_processing_state[widget] &= ~self.InputUpdate if widget in self.__node_for_widget: self.__update_node_processing_state(node) elif widget in self.__delay_delete: self.__try_delete(widget) else: raise ValueError("%r is not managed" % widget) self.__updating_widget = None def __on_blocking_state_changed(self, state): """ OWWidget blocking state has changed. """ if not state: # schedule an update pass. self.signal_manager()._update() widget = self.sender() if state: self.__widget_processing_state[widget] |= self.BlockingUpdate else: self.__widget_processing_state[widget] &= ~self.BlockingUpdate if widget in self.__node_for_widget: node = self.node_for_widget(widget) self.__update_node_processing_state(node) elif widget in self.__delay_delete: self.__try_delete(widget) def __update_node_processing_state(self, node): """ Update the `node.processing_state` to reflect the widget state. """ state = self.node_processing_state(node) node.set_processing_state(1 if state else 0) def __try_delete(self, widget): if self.__widget_processing_state[widget] == 0: self.__delay_delete.remove(widget) widget.deleteLater() del self.__widget_processing_state[widget] def __on_env_changed(self, key, newvalue, oldvalue): # Notify widgets of a runtime environment change for widget in self.__widget_for_node.values(): widget.workflowEnvChanged(key, newvalue, oldvalue)
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 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) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False) 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 CanvasView(QGraphicsView): """Canvas View handles the zooming. """ def __init__(self, *args): super().__init__(*args) self.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.__backgroundIcon = QIcon() self.__autoScroll = False self.__autoScrollMargin = 16 self.__autoScrollTimer = QTimer(self) self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance) # scale factor accumulating partial increments from wheel events self.__zoomLevel = 100 # effective scale level(rounded to whole integers) self.__effectiveZoomLevel = 100 self.__zoomInAction = QAction( self.tr("Zoom in"), self, objectName="action-zoom-in", shortcut=QKeySequence.ZoomIn, triggered=self.zoomIn, ) self.__zoomOutAction = QAction( self.tr("Zoom out"), self, objectName="action-zoom-out", shortcut=QKeySequence.ZoomOut, triggered=self.zoomOut ) self.__zoomResetAction = QAction( self.tr("Reset Zoom"), self, objectName="action-zoom-reset", triggered=self.zoomReset, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0) ) def setScene(self, scene): super().setScene(scene) self._ensureSceneRect(scene) def _ensureSceneRect(self, scene): r = scene.addRect(QRectF(0, 0, 400, 400)) scene.sceneRect() scene.removeItem(r) def setAutoScrollMargin(self, margin): self.__autoScrollMargin = margin def autoScrollMargin(self): return self.__autoScrollMargin def setAutoScroll(self, enable): self.__autoScroll = enable def autoScroll(self): return self.__autoScroll def mousePressEvent(self, event): super().mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: if not self.__autoScrollTimer.isActive() and \ self.__shouldAutoScroll(event.pos()): self.__startAutoScroll() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() & Qt.LeftButton: self.__stopAutoScroll() return super().mouseReleaseEvent(event) def wheelEvent(self, event: QWheelEvent): if event.modifiers() & Qt.ControlModifier \ and event.buttons() == Qt.NoButton: delta = event.angleDelta().y() # use mouse position as anchor while zooming anchor = self.transformationAnchor() self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120) self.setTransformationAnchor(anchor) event.accept() else: super().wheelEvent(event) def zoomIn(self): self.__setZoomLevel(self.__zoomLevel + 10) def zoomOut(self): self.__setZoomLevel(self.__zoomLevel - 10) def zoomReset(self): """ Reset the zoom level. """ self.__setZoomLevel(100) def zoomLevel(self): # type: () -> float """ Return the current zoom level. Level is expressed in percentages; 100 is unscaled, 50 is half size, ... """ return self.__effectiveZoomLevel def setZoomLevel(self, level): self.__setZoomLevel(level) def __setZoomLevel(self, scale): self.__zoomLevel = max(30, min(scale, 300)) scale = round(self.__zoomLevel) self.__zoomOutAction.setEnabled(scale != 30) self.__zoomInAction.setEnabled(scale != 300) if self.__effectiveZoomLevel != scale: self.__effectiveZoomLevel = scale transform = QTransform() transform.scale(scale / 100, scale / 100) self.setTransform(transform) self.zoomLevelChanged.emit(scale) zoomLevelChanged = Signal(float) zoomLevel_ = Property( float, zoomLevel, setZoomLevel, notify=zoomLevelChanged ) def __shouldAutoScroll(self, pos): if self.__autoScroll: margin = self.__autoScrollMargin viewrect = self.contentsRect() rect = viewrect.adjusted(margin, margin, -margin, -margin) # only do auto scroll when on the viewport's margins return not rect.contains(pos) and viewrect.contains(pos) else: return False def __startAutoScroll(self): self.__autoScrollTimer.start(10) log.debug("Auto scroll timer started") def __stopAutoScroll(self): if self.__autoScrollTimer.isActive(): self.__autoScrollTimer.stop() log.debug("Auto scroll timer stopped") def __autoScrollAdvance(self): """Advance the auto scroll """ pos = QCursor.pos() pos = self.mapFromGlobal(pos) margin = self.__autoScrollMargin vvalue = self.verticalScrollBar().value() hvalue = self.horizontalScrollBar().value() vrect = QRect(0, 0, self.width(), self.height()) # What should be the speed advance = 10 # We only do auto scroll if the mouse is inside the view. if vrect.contains(pos): if pos.x() < vrect.left() + margin: self.horizontalScrollBar().setValue(hvalue - advance) if pos.y() < vrect.top() + margin: self.verticalScrollBar().setValue(vvalue - advance) if pos.x() > vrect.right() - margin: self.horizontalScrollBar().setValue(hvalue + advance) if pos.y() > vrect.bottom() - margin: self.verticalScrollBar().setValue(vvalue + advance) if self.verticalScrollBar().value() == vvalue and \ self.horizontalScrollBar().value() == hvalue: self.__stopAutoScroll() else: self.__stopAutoScroll() log.debug("Auto scroll advance") def setBackgroundIcon(self, icon): if not isinstance(icon, QIcon): raise TypeError("A QIcon expected.") if self.__backgroundIcon != icon: self.__backgroundIcon = icon self.viewport().update() def backgroundIcon(self): return QIcon(self.__backgroundIcon) def drawBackground(self, painter, rect): super().drawBackground(painter, rect) if not self.__backgroundIcon.isNull(): painter.setClipRect(rect) vrect = QRect(QPoint(0, 0), self.viewport().size()) vrect = self.mapToScene(vrect).boundingRect() pm = self.__backgroundIcon.pixmap( vrect.size().toSize().boundedTo(QSize(200, 200)) ) pmrect = QRect(QPoint(0, 0), pm.size()) pmrect.moveCenter(vrect.center().toPoint()) if rect.toRect().intersects(pmrect): painter.drawPixmap(pmrect, pm)
class SignalManager(QObject): """ SignalManager handles the runtime signal propagation for a :class:`.Scheme` instance. Note ---- If a scheme instance is passed as a parent to the constructor it is also set as the workflow model. """ class State(enum.IntEnum): """ SignalManager state flags. .. seealso:: :func:`SignalManager.state()` """ #: The manager is running, i.e. it propagates signals Running = 0 #: The manager is stopped. It does not track node output changes, #: and does not deliver signals to dependent nodes Stopped = 1 #: The manager is paused. It still tracks node output changes, but #: does not deliver new signals to dependent nodes. The pending signals #: will be delivered once it enters Running state again Paused = 2 #: The manager is running, i.e. it propagates signals Running = State.Running #: The manager is stopped. It does not track node ouput changes, #: and does not deliver signals to dependent nodes Stopped = State.Stopped #: The manager is paused. It still tracks node output changes, but #: does not deliver new signals to dependent nodes. The pending signals #: will be delivered once it enters Running state again Paused = State.Paused # unused; back-compatibility Error = 3 class RuntimeState(enum.IntEnum): """ SignalManager runtime state. See Also -------- SignalManager.runtime_state """ #: Waiting, idle state. The signal queue is empty Waiting = 0 #: ... Processing = 1 Waiting = RuntimeState.Waiting Processing = RuntimeState.Processing #: Emitted when the state of the signal manager changes. stateChanged = pyqtSignal(int) #: Emitted when signals are added to the queue. updatesPending = pyqtSignal() #: Emitted right before a `SchemeNode` instance has its inputs updated. processingStarted = pyqtSignal([], [SchemeNode]) #: Emitted right after a `SchemeNode` instance has had its inputs updated. processingFinished = pyqtSignal([], [SchemeNode]) #: Emitted when `SignalManager`'s runtime state changes. runtimeStateChanged = pyqtSignal(int) def __init__(self, parent=None, *, max_running=None, **kwargs): # type: (Optional[QObject], Optional[int], Any) -> None super().__init__(parent, **kwargs) self.__workflow = None # type: Optional[Scheme] self.__input_queue = [] # type: List[Signal] # mapping a node to its current outputs self.__node_outputs = { } # type: Dict[SchemeNode, DefaultDict[OutputSignal, _OutputState]] self.__state = SignalManager.Running self.__runtime_state = SignalManager.Waiting self.__update_timer = QTimer(self, interval=100, singleShot=True) self.__update_timer.timeout.connect(self.__process_next) self.__max_running = max_running if isinstance(parent, Scheme): self.set_workflow(parent) def _can_process(self): # type: () -> bool """ Return a bool indicating if the manger can enter the main processing loop. """ return self.__state not in [SignalManager.Error, SignalManager.Stopped] def workflow(self): # type: () -> Optional[Scheme] """ Return the :class:`Scheme` instance. """ return self.__workflow #: Alias scheme = workflow def set_workflow(self, workflow): # type: (Scheme) -> None """ Set the workflow model. Parameters ---------- workflow : Scheme """ if workflow is self.__workflow: return if self.__workflow is not None: for node in self.__workflow.nodes: node.state_changed.disconnect(self._update) for link in self.__workflow.links: link.enabled_changed.disconnect(self.__on_link_enabled_changed) self.__workflow.node_added.disconnect(self.__on_node_added) self.__workflow.node_removed.disconnect(self.__on_node_removed) self.__workflow.link_added.disconnect(self.__on_link_added) self.__workflow.link_removed.disconnect(self.__on_link_removed) self.__workflow.removeEventFilter(self) self.__node_outputs = {} self.__input_queue = [] self.__workflow = workflow if workflow is not None: workflow.node_added.connect(self.__on_node_added) workflow.node_removed.connect(self.__on_node_removed) workflow.link_added.connect(self.__on_link_added) workflow.link_removed.connect(self.__on_link_removed) for node in workflow.nodes: self.__node_outputs[node] = defaultdict(_OutputState) node.state_changed.connect(self._update) for link in workflow.links: link.enabled_changed.connect(self.__on_link_enabled_changed) workflow.installEventFilter(self) def has_pending(self): # type: () -> bool """ Does the manager have any signals to deliver? """ return bool(self.__input_queue) def start(self): # type: () -> None """ Start the update loop. Note ---- The updates will not happen until the control reaches the Qt event loop. """ if self.__state != SignalManager.Running: self.__state = SignalManager.Running self.stateChanged.emit(SignalManager.Running) self._update() def stop(self): # type: () -> None """ Stop the update loop. Note ---- If the `SignalManager` is currently in `process_queues` it will still update all current pending signals, but will not re-enter until `start()` is called again. """ if self.__state != SignalManager.Stopped: self.__state = SignalManager.Stopped self.stateChanged.emit(SignalManager.Stopped) self.__update_timer.stop() def pause(self): # type: () -> None """ Pause the delivery of signals. """ if self.__state != SignalManager.Paused: self.__state = SignalManager.Paused self.stateChanged.emit(SignalManager.Paused) self.__update_timer.stop() def resume(self): # type: () -> None """ Resume the delivery of signals. """ if self.__state == SignalManager.Paused: self.__state = SignalManager.Running self.stateChanged.emit(self.__state) self._update() def step(self): # type: () -> None """ Deliver signals to a single node (only applicable while the `state()` is `Paused`). """ if self.__state == SignalManager.Paused: self.process_queued() def state(self): # type: () -> State """ Return the current state. Return ------ state : SignalManager.State """ return self.__state def _set_runtime_state(self, state): # type: (Union[RuntimeState, int]) -> None """ Set the runtime state. Should only be called by `SignalManager` implementations. """ if self.__runtime_state != state: self.__runtime_state = state self.runtimeStateChanged.emit(self.__runtime_state) def runtime_state(self): # type: () -> RuntimeState """ Return the runtime state. This can be `SignalManager.Waiting` or `SignalManager.Processing`. """ return self.__runtime_state def __on_node_removed(self, node): # type: (SchemeNode) -> None # remove all pending input signals for node so we don't get # stale references in process_node. # NOTE: This does not remove output signals for this node. In # particular the final 'None' will be delivered to the sink # nodes even after the source node is no longer in the scheme. log.info("Removing pending signals for '%s'.", node.title) self.remove_pending_signals(node) del self.__node_outputs[node] node.state_changed.disconnect(self._update) def __on_node_added(self, node): # type: (SchemeNode) -> None self.__node_outputs[node] = defaultdict(_OutputState) # schedule update pass on state change node.state_changed.connect(self._update) def __on_link_added(self, link): # type: (SchemeLink) -> None # push all current source values to the sink link.set_runtime_state(SchemeLink.Empty) state = self.__node_outputs[link.source_node][link.source_channel] link.set_runtime_state_flag( SchemeLink.Invalidated, bool(state.flags & _OutputState.Invalidated)) if link.enabled: log.info("Scheduling signal data update for '%s'.", link) self._schedule(self.signals_on_link(link)) self._update() link.enabled_changed.connect(self.__on_link_enabled_changed) def __on_link_removed(self, link): # type: (SchemeLink) -> None # purge all values in sink's queue log.info("Scheduling signal data purge (%s).", link) self.purge_link(link) link.enabled_changed.disconnect(self.__on_link_enabled_changed) def __on_link_enabled_changed(self, enabled): if enabled: link = self.sender() log.info("Link %s enabled. Scheduling signal data update.", link) self._schedule(self.signals_on_link(link)) def signals_on_link(self, link): # type: (SchemeLink) -> List[Signal] """ Return :class:`Signal` instances representing the current values present on the `link`. """ items = self.link_contents(link) signals = [] for key, value in items.items(): signals.append(Signal(link, value, key)) return signals def link_contents(self, link): # type: (SchemeLink) -> Dict[Any, Any] """ Return the contents on the `link`. """ node, channel = link.source_node, link.source_channel if node in self.__node_outputs: return self.__node_outputs[node][channel].outputs else: # if the the node was already removed its tracked outputs in # __node_outputs are cleared, however the final 'None' signal # deliveries for the link are left in the _input_queue. pending = [sig for sig in self.__input_queue if sig.link is link] return {sig.id: sig.value for sig in pending} def send(self, node, channel, value, id): # type: (SchemeNode, OutputSignal, Any, Any) -> None """ Send the `value` with `id` on an output `channel` from node. Schedule the signal delivery to all dependent nodes Parameters ---------- node : SchemeNode The originating node. channel : OutputSignal The nodes output on which the value is sent. value : Any The value to send, id : Any Signal id. """ if self.__workflow is None: raise RuntimeError("'send' called with no workflow!.") log.debug("%r sending %r (id: %r) on channel %r", node.title, type(value), id, channel.name) scheme = self.__workflow state = self.__node_outputs[node][channel] state.outputs[id] = value # clear invalidated flag if state.flags & _OutputState.Invalidated: log.debug("%r clear invalidated flag on channel %r", node.title, channel.name) state.flags &= ~_OutputState.Invalidated links = filter( is_enabled, scheme.find_links(source_node=node, source_channel=channel)) signals = [] for link in links: signals.append(Signal(link, value, id)) link.set_runtime_state_flag(SchemeLink.Invalidated, False) self._schedule(signals) def invalidate(self, node, channel): # type: (SchemeNode, OutputSignal) -> None """ Invalidate the `channel` on `node`. The channel is effectively considered changed but unavailable until a new value is sent via `send`. While this state is set the dependent nodes will not be updated. All links originating with this node/channel will be marked with `SchemeLink.Invalidated` flag until a new value is sent with `send`. Parameters ---------- node: SchemeNode The originating node. channel: OutputSignal The channel to invalidate. .. versionadded:: 0.1.8 """ log.debug("%r invalidating channel %r", node.title, channel.name) self.__node_outputs[node][channel].flags |= _OutputState.Invalidated if self.__workflow is None: return links = self.__workflow.find_links(source_node=node, source_channel=channel) for link in links: link.set_runtime_state(link.runtime_state() | link.Invalidated) def purge_link(self, link): # type: (SchemeLink) -> None """ Purge the link (send None for all ids currently present) """ contents = self.link_contents(link) ids = contents.keys() signals = [Signal(link, None, id) for id in ids] self._schedule(signals) def _schedule(self, signals): # type: (List[Signal]) -> None """ Schedule a list of :class:`Signal` for delivery. """ self.__input_queue.extend(signals) for link in {sig.link for sig in signals}: # update the SchemeLink's runtime state flags contents = self.link_contents(link) if any(value is not None for value in contents.values()): state = SchemeLink.Active else: state = SchemeLink.Empty link.set_runtime_state(state | SchemeLink.Pending) for node in {sig.link.sink_node for sig in signals}: # type: SchemeNode # update the SchemeNodes's runtime state flags node.set_state_flags(SchemeNode.Pending, True) if signals: self.updatesPending.emit() self._update() def _update_link(self, link): # type: (SchemeLink) -> None """ Schedule update of a single link. """ signals = self.signals_on_link(link) self._schedule(signals) def process_queued(self, max_nodes=None): # type: (Any) -> None """ Process queued signals. Take the first eligible node from the pending input queue and deliver all scheduled signals. """ if not (max_nodes is None or max_nodes == 1): warnings.warn( "`max_nodes` is deprecated and will be removed in the future", FutureWarning, stacklevel=2) if self.__runtime_state == SignalManager.Processing: raise RuntimeError("Cannot re-enter 'process_queued'") if not self._can_process(): raise RuntimeError("Can't process in state %i" % self.__state) self.process_next() def process_next(self): # type: () -> bool """ Process queued signals. Take the first eligible node from the pending input queue and deliver all scheduled signals for it and return `True`. If no node is eligible for update do nothing and return `False`. """ return self.__process_next_helper(use_max_active=False) def process_node(self, node): # type: (SchemeNode) -> None """ Process pending input signals for `node`. """ assert self.__runtime_state != SignalManager.Processing signals_in = self.pending_input_signals(node) self.remove_pending_signals(node) signals_in = self.compress_signals(signals_in) log.debug("Processing %r, sending %i signals.", node.title, len(signals_in)) # Clear the link's pending flag. for link in {sig.link for sig in signals_in}: link.set_runtime_state(link.runtime_state() & ~SchemeLink.Pending) def process_dynamic(signals): # type: (List[Signal]) -> List[Signal] """ Process dynamic signals; Update the link's dynamic_enabled flag if the value is valid; replace values that do not type check with `None` """ res = [] for sig in signals: # Check and update the dynamic link state link = sig.link if sig.link.is_dynamic(): enabled = can_enable_dynamic(link, sig.value) link.set_dynamic_enabled(enabled) if not enabled: # Send None instead (clear the link) sig = Signal(link, None, sig.id) res.append(sig) return res signals_in = process_dynamic(signals_in) assert ({sig.link for sig in self.__input_queue }.intersection({sig.link for sig in signals_in}) == set([])) self._set_runtime_state(SignalManager.Processing) self.processingStarted.emit() self.processingStarted[SchemeNode].emit(node) try: self.send_to_node(node, signals_in) finally: node.set_state_flags(SchemeNode.Pending, False) self.processingFinished.emit() self.processingFinished[SchemeNode].emit(node) self._set_runtime_state(SignalManager.Waiting) def compress_signals(self, signals): # type: (List[Signal]) -> List[Signal] """ Compress a list of :class:`Signal` instances to be delivered. Before the signal values are delivered to the sink node they can be optionally `compressed`, i.e. values can be merged or dropped depending on the execution semantics. The input list is in the order that the signals were enqueued. The base implementation returns the list unmodified. Parameters ---------- signals : List[Signal] Return ------ signals : List[Signal] """ return signals def send_to_node(self, node, signals): # type: (SchemeNode, List[Signal]) -> None """ Abstract. Reimplement in subclass. Send/notify the `node` instance (or whatever object/instance it is a representation of) that it has new inputs as represented by the `signals` list). Parameters ---------- node : SchemeNode signals : List[Signal] """ raise NotImplementedError def is_pending(self, node): # type: (SchemeNode) -> bool """ Is `node` (class:`SchemeNode`) scheduled for processing (i.e. it has incoming pending signals). Parameters ---------- node : SchemeNode Returns ------- pending : bool """ return node in [signal.link.sink_node for signal in self.__input_queue] def pending_nodes(self): # type: () -> List[SchemeNode] """ Return a list of pending nodes. The nodes are returned in the order they were enqueued for signal delivery. Returns ------- nodes : List[SchemeNode] """ return list(unique(sig.link.sink_node for sig in self.__input_queue)) def pending_input_signals(self, node): # type: (SchemeNode) -> List[Signal] """ Return a list of pending input signals for node. """ return [ signal for signal in self.__input_queue if node is signal.link.sink_node ] def remove_pending_signals(self, node): # type: (SchemeNode) -> None """ Remove pending signals for `node`. """ for signal in self.pending_input_signals(node): try: self.__input_queue.remove(signal) except ValueError: pass def __nodes(self): # type: () -> Sequence[SchemeNode] return self.__workflow.nodes if self.__workflow else [] def blocking_nodes(self): # type: () -> List[SchemeNode] """ Return a list of nodes in a blocking state. """ return [node for node in self.__nodes() if self.is_blocking(node)] def invalidated_nodes(self): # type: () -> List[SchemeNode] """ Return a list of invalidated nodes. .. versionadded:: 0.1.8 """ return [ node for node in self.__nodes() if self.has_invalidated_outputs(node) or self.is_invalidated(node) ] def active_nodes(self): # type: () -> List[SchemeNode] """ Return a list of active nodes. .. versionadded:: 0.1.8 """ return [node for node in self.__nodes() if self.is_active(node)] def is_blocking(self, node): # type: (SchemeNode) -> bool """ Is the node in `blocking` state. Is it currently in a state where will produce new outputs and therefore no signals should be delivered to dependent nodes until it does so. Also no signals will be delivered to the node until it exits this state. The default implementation returns False. .. deprecated:: 0.1.8 Use a combination of `is_invalidated` and `is_ready`. """ return False def is_ready(self, node: SchemeNode) -> bool: """ Is the node in a state where it can receive inputs. Re-implement this method in as subclass to prevent specific nodes from being considered for input update (e.g. they are still initializing runtime resources, executing a non-interruptable task, ...) Note that whenever the implicit state changes the `post_update_request` should be called. The default implementation returns the state of the node's `SchemeNode.NotReady` flag. Parameters ---------- node: SchemeNode """ return not node.test_state_flags(SchemeNode.NotReady) def is_invalidated(self, node: SchemeNode) -> bool: """ Is the node marked as invalidated. Parameters ---------- node : SchemeNode Returns ------- state: bool """ return node.test_state_flags(SchemeNode.Invalidated) def has_invalidated_outputs(self, node): # type: (SchemeNode) -> bool """ Does node have any explicitly invalidated outputs. Parameters ---------- node: SchemeNode Returns ------- state: bool See also -------- invalidate .. versionadded:: 0.1.8 """ out = self.__node_outputs.get(node) if out is not None: return any(state.flags & _OutputState.Invalidated for state in out.values()) else: return False def has_invalidated_inputs(self, node): # type: (SchemeNode) -> bool """ Does the node have any immediate ancestor with invalidated outputs. Parameters ---------- node : SchemeNode Returns ------- state: bool Note ---- The node's ancestors are only computed over enabled links. .. versionadded:: 0.1.8 """ if self.__workflow is None: return False workflow = self.__workflow return any( self.has_invalidated_outputs(link.source_node) for link in workflow.find_links(sink_node=node) if link.is_enabled()) def is_active(self, node): # type: (SchemeNode) -> bool """ Is the node considered active (executing a task). Parameters ---------- node: SchemeNode Returns ------- active: bool """ return bool(node.state() & SchemeNode.Running) def node_update_front(self): # type: () -> Sequence[SchemeNode] """ Return a list of nodes on the update front, i.e. nodes scheduled for an update that have no ancestor which is either itself scheduled for update or is in a blocking state). Note ---- The node's ancestors are only computed over enabled links. """ if self.__workflow is None: return [] workflow = self.__workflow expand = partial(expand_node, workflow) components = strongly_connected_components(workflow.nodes, expand) node_scc = {node: scc for scc in components for node in scc} def isincycle(node): # type: (SchemeNode) -> bool return len(node_scc[node]) > 1 def dependents(node): # type: (SchemeNode) -> List[SchemeNode] return dependent_nodes(workflow, node) # A list of all nodes currently active/executing a non-interruptable # task. blocking_nodes = set(self.blocking_nodes()) # nodes marked as having invalidated outputs (not yet available) invalidated_nodes = set(self.invalidated_nodes()) #: transitive invalidated nodes (including the legacy self.is_blocked #: behaviour - blocked nodes are both invalidated and cannot receive #: new inputs) invalidated_ = reduce( set.union, map(dependents, invalidated_nodes | blocking_nodes), set([]), ) # type: Set[SchemeNode] pending = self.pending_nodes() pending_ = set() for n in pending: depend = set(dependents(n)) if isincycle(n): # a pending node in a cycle would would have a circular # dependency on itself, preventing any progress being made # by the workflow execution. cc = node_scc[n] depend -= set(cc) pending_.update(depend) def has_invalidated_ancestor(node): # type: (SchemeNode) -> bool return node in invalidated_ def has_pending_ancestor(node): # type: (SchemeNode) -> bool return node in pending_ #: nodes that are eligible for update. ready = list( filter( lambda node: not has_pending_ancestor(node) and not has_invalidated_ancestor(node) and not self.is_blocking( node), pending)) return ready @Slot() def __process_next(self): if not self.__state == SignalManager.Running: log.debug("Received 'UpdateRequest' while not in 'Running' state") return if self.__runtime_state == SignalManager.Processing: # This happens if QCoreApplication.processEvents is called from # the input handlers. A `__process_next` must be rescheduled when # exiting process_queued. log.warning("Received 'UpdateRequest' while in 'process_queued'. " "An update will be re-scheduled when exiting the " "current update.") return if not self.__input_queue: return if self.__process_next_helper(use_max_active=True): # Schedule another update (will be a noop if nothing to do). self._update() def __process_next_helper(self, use_max_active=True) -> bool: eligible = [n for n in self.node_update_front() if self.is_ready(n)] if not eligible: return False max_active = self.max_active() nactive = len(set(self.active_nodes()) | set(self.blocking_nodes())) log.debug( "Process next, queued signals: %i, nactive: %i " "(max_active: %i)", len(self.__input_queue), nactive, max_active) _ = lambda nodes: list(map(attrgetter('title'), nodes)) log.debug("Pending nodes: %s", _(self.pending_nodes())) log.debug("Blocking nodes: %s", _(self.blocking_nodes())) log.debug("Invalidated nodes: %s", _(self.invalidated_nodes())) log.debug("Nodes ready for update: %s", _(eligible)) # Select an node that is already running (effectively cancelling # already executing tasks that are immediately updatable) selected_node = None # type: Optional[SchemeNode] for node in eligible: if self.is_active(node): selected_node = node break # Return if over committed, except in the case that the selected_node # is already active. if use_max_active and nactive >= max_active and selected_node is None: return False if selected_node is None: selected_node = eligible[0] self.process_node(selected_node) return True def _update(self): # type: () -> None """ Schedule processing at a later time. """ if self.__state == SignalManager.Running and \ not self.__update_timer.isActive(): self.__update_timer.start() def post_update_request(self): """ Schedule an update pass. Call this method whenever: * a node's outputs change (note that this is already done by `send`) * any change in the node that influences its eligibility to be picked for an input update (is_ready, is_blocking ...). Multiple update requests are merged into one. """ self._update() def set_max_active(self, val: int) -> None: if self.__max_running != val: self.__max_running = val self._update() def max_active(self) -> int: value = self.__max_running # type: Optional[int] if value is None: value = mapping_get(os.environ, "MAX_ACTIVE_NODES", int, None) if value is None: s = QSettings() s.beginGroup(__name__) value = s.value("max-active-nodes", defaultValue=1, type=int) if value < 0: ccount = os.cpu_count() if ccount is None: return 1 else: return max(1, ccount + value) else: return max(1, value)
class OWFilter(widget.OWWidget): name = "Filter" icon = 'icons/Filter.svg' description = "Filter cells/genes" class Inputs: data = widget.Input("Data", Orange.data.Table) class Outputs: data = widget.Output("Data", Orange.data.Table) class Warning(widget.OWWidget.Warning): invalid_range = widget.Msg( "Negative values in input data.\n" "This filter only makes sense for non-negative measurements" "where 0 indicates a lack (of) and/or a neutral reading." ) sampling_in_effect = widget.Msg( "Too many data points to display.\n" "Sampling {} of {} data points." ) #: Filter mode. #: Filter out rows/columns or 'zap' data values in range. Cells, Genes, Data = Cells, Genes, Data settings_version = 2 #: The selected filter mode selected_filter_type = settings.Setting(Cells) # type: int #: Augment the violin plot with a dot plot (strip plot) of the (non-zero) #: measurement counts in Cells/Genes mode or data matrix values in Data #: mode. display_dotplot = settings.Setting(True) # type: bool #: Is min/max range selection enable limit_lower_enabled = settings.Setting(True) # type: bool limit_upper_enabled = settings.Setting(True) # type: bool #: The lower and upper selection limit for each filter type thresholds = settings.Setting({ Cells: (0, 2 ** 31 - 1), Genes: (0, 2 ** 31 - 1), Data: (0.0, 2.0 ** 31 - 1) }) # type: Dict[int, Tuple[float, float]] auto_commit = settings.Setting(True) # type: bool def __init__(self): super().__init__() self.data = None # type: Optional[Orange.data.Table] self._counts = None # type: Optional[np.ndarray] box = gui.widgetBox(self.controlArea, "Info") self._info = QLabel(box, wordWrap=True) self._info.setText("No data in input\n") box.layout().addWidget(self._info) box = gui.widgetBox(self.controlArea, "Filter Type") rbg = QButtonGroup(box, exclusive=True) for id_ in [Cells, Genes, Data]: name, _, tip = FilterInfo[id_] b = QRadioButton( name, toolTip=tip, checked=id_ == self.selected_filter_type ) box.layout().addWidget(b) rbg.addButton(b, id_) rbg.buttonClicked[int].connect(self.set_filter_type) box = gui.widgetBox(self.controlArea, "View") self._showpoints = gui.checkBox( box, self, "display_dotplot", "Show data points", callback=self._update_dotplot ) form = QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow ) self._filter_box = box = gui.widgetBox( self.controlArea, "Filter", orientation=form ) # type: QGroupBox self.threshold_stacks = ( QStackedWidget(enabled=self.limit_lower_enabled), QStackedWidget(enabled=self.limit_upper_enabled), ) finfo = np.finfo(np.float64) for filter_ in [Cells, Genes, Data]: if filter_ in {Cells, Genes}: minimum = 0.0 ndecimals = 1 else: minimum = finfo.min ndecimals = 3 spinlower = QDoubleSpinBox( self, minimum=minimum, maximum=finfo.max, decimals=ndecimals, keyboardTracking=False, ) spinupper = QDoubleSpinBox( self, minimum=minimum, maximum=finfo.max, decimals=ndecimals, keyboardTracking=False, ) lower, upper = self.thresholds[filter_] spinlower.setValue(lower) spinupper.setValue(upper) self.threshold_stacks[0].addWidget(spinlower) self.threshold_stacks[1].addWidget(spinupper) spinlower.valueChanged.connect(self._limitchanged) spinupper.valueChanged.connect(self._limitchanged) self.threshold_stacks[0].setCurrentIndex(self.selected_filter_type) self.threshold_stacks[1].setCurrentIndex(self.selected_filter_type) self.limit_lower_enabled_cb = cb = QCheckBox( "Min", checked=self.limit_lower_enabled ) cb.toggled.connect(self.set_lower_limit_enabled) cb.setAttribute(Qt.WA_LayoutUsesWidgetRect, True) form.addRow(cb, self.threshold_stacks[0]) self.limit_upper_enabled_cb = cb = QCheckBox( "Max", checked=self.limit_upper_enabled ) cb.toggled.connect(self.set_upper_limit_enabled) cb.setAttribute(Qt.WA_LayoutUsesWidgetRect, True) form.addRow(cb, self.threshold_stacks[1]) self.controlArea.layout().addStretch(10) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") self._view = pg.GraphicsView() self._view.enableMouse(False) self._view.setAntialiasing(True) self._plot = plot = ViolinPlot() self._plot.setDataPointsVisible(self.display_dotplot) self._plot.setSelectionMode( (ViolinPlot.Low if self.limit_lower_enabled else 0) | (ViolinPlot.High if self.limit_upper_enabled else 0) ) self._plot.selectionEdited.connect(self._limitchanged_plot) self._view.setCentralWidget(self._plot) self._plot.setTitle("Detected genes") left = self._plot.getAxis("left") # type: pg.AxisItem left.setLabel("Detected genes") bottom = self._plot.getAxis("bottom") # type: pg.AxisItem bottom.hide() plot.setMouseEnabled(False, False) plot.hideButtons() self.mainArea.layout().addWidget(self._view) # Coalescing commit timer self._committimer = QTimer(self, singleShot=True) self._committimer.timeout.connect(self.commit) self.addAction( QAction("Select All", self, shortcut=QKeySequence.SelectAll, triggered=self._select_all) ) def sizeHint(self): sh = super().sizeHint() # type: QSize return sh.expandedTo(QSize(800, 600)) def set_filter_type(self, type_): if self.selected_filter_type != type_: assert type_ in (Cells, Genes, Data), str(type_) self.selected_filter_type = type_ self.threshold_stacks[0].setCurrentIndex(type_) self.threshold_stacks[1].setCurrentIndex(type_) if self.data is not None: self._setup(self.data, type_) self._schedule_commit() def filter_type(self): return self.selected_filter_type def set_upper_limit_enabled(self, enabled): if enabled != self.limit_upper_enabled: self.limit_upper_enabled = enabled self.threshold_stacks[1].setEnabled(enabled) self.limit_upper_enabled_cb.setChecked(enabled) self._update_filter() self._schedule_commit() def set_lower_limit_enabled(self, enabled): if enabled != self.limit_lower_enabled: self.limit_lower_enabled = enabled self.threshold_stacks[0].setEnabled(enabled) self.limit_lower_enabled_cb.setChecked(enabled) self._update_filter() self._schedule_commit() def _update_filter(self): mode = 0 if self.limit_lower_enabled: mode |= ViolinPlot.Low if self.limit_upper_enabled: mode |= ViolinPlot.High self._plot.setSelectionMode(mode) self._update_info() self._schedule_commit() def _is_filter_enabled(self): return self.limit_lower_enabled or self.limit_upper_enabled @Inputs.data def set_data(self, data): # type: (Optional[Orange.data.Table]) -> None self.clear() self.data = data if data is not None: if np.any(data.X < 0): self.Warning.invalid_range() self._setup(data, self.filter_type()) self.unconditional_commit() def clear(self): self._plot.clear() self.data = None self._counts = None self._update_info() self.Warning.clear() def _update_info(self): text = [] if self.data is None: text += ["No data on input.\n"] else: N, M = len(self.data), len(self.data.domain.attributes) text = [] text += [ "Data with {N} cell{Np} and {M} gene{Mp}" .format(N=N, Np="s" if N != 1 else "", M=M, Mp="s" if N != 1 else "") ] if self._is_filter_enabled() and \ self.filter_type() in [Cells, Genes]: mask = np.ones(self._counts.shape, dtype=bool) if self.limit_lower_enabled: mask &= self.limit_lower <= self._counts if self.limit_upper_enabled: mask &= self._counts <= self.limit_upper n = np.count_nonzero(mask) subject = "cell" if self.filter_type() == Cells else "gene" if n == 0: text += ["All {}s filtered out".format(subject)] else: text += [ "{} {subject}{s} in selection" .format(n, subject=subject, s="s" if n != 1 else "") ] else: text += [""] self._info.setText("\n".join(text)) def _select_all(self): self.limit_lower = 0 self.limit_upper = 2 ** 31 - 1 self._limitchanged() def _setup(self, data, filter_type): self._plot.clear() self._counts = None title = None sample_range = None if filter_type in [Cells, Genes]: if filter_type == Cells: axis = 1 title = "Cell Filter" axis_label = "Detected Genes" else: axis = 0 title = "Gene Filter" axis_label = "Detected Cells" mask = (data.X != 0) & (np.isfinite(data.X)) counts = np.count_nonzero(mask, axis=axis) x = counts self._counts = counts self.Warning.sampling_in_effect.clear() elif filter_type == Data: x = data.X.ravel() x = x[np.isfinite(x)] self._counts = x MAX_DISPLAY_SIZE = 20000 if x.size > MAX_DISPLAY_SIZE: self.Warning.sampling_in_effect(MAX_DISPLAY_SIZE, x.size) # tails to preserve exactly tails = 1 assert x.flags.owndata x.sort() x1, x2, x3 = x[:tails], x[tails:x.size - tails], x[x.size-tails:] assert x1.size + x2.size + x3.size == x.size x2 = np.random.RandomState(0x667).choice( x2, size=MAX_DISPLAY_SIZE - 2 * tails, replace=False, ) x = np.r_[x1, x2, x3] span = x[-1] - x[0] else: span = np.ptp(x) self.Warning.sampling_in_effect.clear() if span > 0: ndecimals = max(4 - int(np.floor(np.log10(span))), 1) else: ndecimals = 1 spinlow = self.threshold_stacks[0].widget(Data) spinhigh = self.threshold_stacks[1].widget(Data) spinlow.setDecimals(ndecimals) spinhigh.setDecimals(ndecimals) title = "Data Filter" axis_label = "Gene Expression" else: assert False if x.size: xmin, xmax = np.min(x), np.max(x) self.limit_lower = np.clip(self.limit_lower, xmin, xmax) self.limit_upper = np.clip(self.limit_upper, xmin, xmax) if x.size > 0: # TODO: Need correction for lower bounded distribution (counts) # Use reflection around 0, but gaussian_kde does not provide # sufficient flexibility w.r.t bandwidth selection. self._plot.setData(x, 1000) self._plot.setBoundary(self.limit_lower, self.limit_upper) ax = self._plot.getAxis("left") # type: pg.AxisItem ax.setLabel(axis_label) self._plot.setTitle(title) self._update_info() def _update_dotplot(self): self._plot.setDataPointsVisible(self.display_dotplot) @property def limit_lower(self): return self.thresholds[self.selected_filter_type][0] @limit_lower.setter def limit_lower(self, value): _, upper = self.thresholds[self.selected_filter_type] self.thresholds[self.selected_filter_type] = (value, upper) stacklower, _ = self.threshold_stacks sb = stacklower.widget(self.selected_filter_type) # prevent changes due to spin box rounding sb.setValue(value) @property def limit_upper(self): return self.thresholds[self.selected_filter_type][1] @limit_upper.setter def limit_upper(self, value): lower, _ = self.thresholds[self.selected_filter_type] self.thresholds[self.selected_filter_type] = (lower, value) _, stackupper = self.threshold_stacks sb = stackupper.widget(self.selected_filter_type) sb.setValue(value) @Slot() def _limitchanged(self): # Low/high limit changed via the spin boxes stacklow, stackhigh = self.threshold_stacks filter_ = self.selected_filter_type lower = stacklow.widget(filter_).value() upper = stackhigh.widget(filter_).value() self.thresholds[filter_] = (lower, upper) if self._counts is not None and self._counts.size: xmin = np.min(self._counts) xmax = np.max(self._counts) self._plot.setBoundary( np.clip(lower, xmin, xmax), np.clip(upper, xmin, xmax) ) # TODO: Only when the actual selection/filter mask changes self._schedule_commit() self._update_info() def _limitchanged_plot(self): # Low/high limit changed via the plot if self._counts is not None: newlower, newupper = self._plot.boundary() filter_ = self.selected_filter_type lower, upper = self.thresholds[filter_] stacklow, stackhigh = self.threshold_stacks spin_lower = stacklow.widget(filter_) spin_upper = stackhigh.widget(filter_) # do rounding to match the spin box's precision if self.limit_lower_enabled: newlower = round(newlower, spin_lower.decimals()) else: newlower = lower if self.limit_upper_enabled: newupper = round(newupper, spin_upper.decimals()) else: newupper = upper if self.limit_lower_enabled and newlower != lower: self.limit_lower = newlower if self.limit_upper_enabled and newupper != upper: self.limit_upper = newupper self._plot.setBoundary(newlower, newupper) # TODO: Only when the actual selection/filter mask changes self._schedule_commit() self._update_info() def _schedule_commit(self): self._committimer.start() def commit(self): self._committimer.stop() data = self.data if data is not None and self._is_filter_enabled(): if self.filter_type() in [Cells, Genes]: counts = self._counts cmax = self.limit_upper cmin = self.limit_lower mask = np.ones(counts.shape, dtype=bool) if self.limit_lower_enabled: mask &= cmin <= counts if self.limit_upper_enabled: mask &= counts <= cmax if self.filter_type() == Cells: assert counts.size == len(data) data = data[mask] else: assert counts.size == len(data.domain.attributes) atts = [v for v, m in zip(data.domain.attributes, mask) if m] data = data.from_table( Orange.data.Domain( atts, data.domain.class_vars, data.domain.metas ), data ) if len(data) == 0 or \ len(data.domain) + len(data.domain.metas) == 0: data = None elif self.filter_type() == Data: dmin, dmax = self.limit_lower, self.limit_upper data = data.copy() assert data.X.base is None mask = None if self.limit_lower_enabled: mask = data.X < dmin if self.limit_upper_enabled: if mask is not None: mask |= data.X > dmax else: mask = data.X < dmax data.X[mask] = 0.0 else: assert False self.Outputs.data.send(data) def onDeleteWidget(self): self.clear() self._plot.close() super().onDeleteWidget() @classmethod def migrate_settings(cls, settings, version): if (version is None or version < 2) and \ ("limit_lower" in settings and "limit_upper" in settings): # v2 changed limit_lower, limit_upper to per filter limits stored # in a single dict lower = settings.pop("limit_lower") upper = settings.pop("limit_upper") settings["thresholds"] = { Cells: (lower, upper), Genes: (lower, upper), Data: (lower, upper), }
class OWSelectAttributes(widget.OWWidget): # pylint: disable=too-many-instance-attributes name = "列选择" description = "Select columns from the data table and assign them to " \ "data features, classes or meta variables." icon = "icons/SelectColumns.svg" priority = 100 keywords = ["filter"] class Inputs: data = Input("Data", Table) class Outputs: data = Output("Data", Table) features = Output("Features", widget.AttributeList, dynamic=False) want_main_area = False want_control_area = True settingsHandler = SelectAttributesDomainContextHandler() domain_role_hints = ContextSetting({}) auto_commit = Setting(True) def __init__(self): super().__init__() # Schedule interface updates (enabled buttons) using a coalescing # single shot timer (complex interactions on selection and filtering # updates in the 'available_attrs_view') self.__interface_update_timer = QTimer(self, interval=0, singleShot=True) self.__interface_update_timer.timeout.connect( self.__update_interface_state) # The last view that has the selection for move operation's source self.__last_active_view = None # type: Optional[QListView] def update_on_change(view): # Schedule interface state update on selection change in `view` self.__last_active_view = view self.__interface_update_timer.start() self.controlArea = QWidget(self.controlArea) self.layout().addWidget(self.controlArea) layout = QGridLayout() self.controlArea.setLayout(layout) layout.setContentsMargins(4, 4, 4, 4) box = gui.vBox(self.controlArea, "Available Variables", addToLayout=False) self.available_attrs = VariablesListItemModel() filter_edit, self.available_attrs_view = variables_filter( parent=self, model=self.available_attrs) box.layout().addWidget(filter_edit) def dropcompleted(action): if action == Qt.MoveAction: self.commit() self.available_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.available_attrs_view)) self.available_attrs_view.dragDropActionDidComplete.connect( dropcompleted) box.layout().addWidget(self.available_attrs_view) layout.addWidget(box, 0, 0, 3, 1) box = gui.vBox(self.controlArea, "Features", addToLayout=False) self.used_attrs = VariablesListItemModel() self.used_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.used_attrs_view.setModel(self.used_attrs) self.used_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.used_attrs_view)) self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.used_attrs_view) layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False) self.class_attrs = VariablesListItemModel() self.class_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) self.class_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.class_attrs_view)) self.class_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.class_attrs_view.setMaximumHeight(72) box.layout().addWidget(self.class_attrs_view) layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False) self.meta_attrs = VariablesListItemModel() self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) self.meta_attrs_view.selectionModel().selectionChanged.connect( partial(update_on_change, self.meta_attrs_view)) self.meta_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.meta_attrs_view) layout.addWidget(box, 2, 2, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 0, 1, 1, 1) self.up_attr_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.used_attrs_view)) self.move_attr_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.used_attrs_view)) self.down_attr_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.used_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 1, 1, 1, 1) self.up_class_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.class_attrs_view)) self.move_class_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.class_attrs_view, exclusive=False)) self.down_class_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.class_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 2, 1, 1, 1) self.up_meta_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.meta_attrs_view)) self.move_meta_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.meta_attrs_view)) self.down_meta_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.meta_attrs_view)) autobox = gui.auto_commit(None, self, "auto_commit", "Send") layout.addWidget(autobox, 3, 0, 1, 3) reset = gui.button(None, self, "Reset", callback=self.reset, width=120) autobox.layout().insertWidget(0, reset) autobox.layout().insertStretch(1, 20) layout.setRowStretch(0, 4) layout.setRowStretch(1, 0) layout.setRowStretch(2, 2) layout.setHorizontalSpacing(0) self.controlArea.setLayout(layout) self.data = None self.output_data = None self.original_completer_items = [] self.resize(500, 600) @Inputs.data def set_data(self, data=None): self.update_domain_role_hints() self.closeContext() self.data = data if data is not None: self.openContext(data) all_vars = data.domain.variables + data.domain.metas var_sig = lambda attr: (attr.name, vartype(attr)) domain_hints = { var_sig(attr): ("attribute", i) for i, attr in enumerate(data.domain.attributes) } domain_hints.update({ var_sig(attr): ("meta", i) for i, attr in enumerate(data.domain.metas) }) if data.domain.class_vars: domain_hints.update({ var_sig(attr): ("class", i) for i, attr in enumerate(data.domain.class_vars) }) # update the hints from context settings domain_hints.update(self.domain_role_hints) attrs_for_role = lambda role: [ (domain_hints[var_sig(attr)][1], attr) for attr in all_vars if domain_hints[var_sig(attr)][0] == role ] attributes = [ attr for place, attr in sorted(attrs_for_role("attribute"), key=lambda a: a[0]) ] classes = [ attr for place, attr in sorted(attrs_for_role("class"), key=lambda a: a[0]) ] metas = [ attr for place, attr in sorted(attrs_for_role("meta"), key=lambda a: a[0]) ] available = [ attr for place, attr in sorted(attrs_for_role("available"), key=lambda a: a[0]) ] self.used_attrs[:] = attributes self.class_attrs[:] = classes self.meta_attrs[:] = metas self.available_attrs[:] = available else: self.used_attrs[:] = [] self.class_attrs[:] = [] self.meta_attrs[:] = [] self.available_attrs[:] = [] self.unconditional_commit() def update_domain_role_hints(self): """ Update the domain hints to be stored in the widgets settings. """ hints_from_model = lambda role, model: [( (attr.name, vartype(attr)), (role, i)) for i, attr in enumerate(model)] hints = dict(hints_from_model("available", self.available_attrs)) hints.update(hints_from_model("attribute", self.used_attrs)) hints.update(hints_from_model("class", self.class_attrs)) hints.update(hints_from_model("meta", self.meta_attrs)) self.domain_role_hints = hints def selected_rows(self, view): """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() model = view.model() if isinstance(model, QSortFilterProxyModel): rows = [model.mapToSource(r) for r in rows] return [r.row() for r in rows] def move_rows(self, view, rows, offset): model = view.model() newrows = [min(max(0, row + offset), len(model) - 1) for row in rows] for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): model[row], model[newrow] = model[newrow], model[row] selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect) self.commit() def move_up(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, -1) def move_down(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, 1) def move_selected(self, view, exclusive=False): if self.selected_rows(view): self.move_selected_from_to(view, self.available_attrs_view) elif self.selected_rows(self.available_attrs_view): self.move_selected_from_to(self.available_attrs_view, view, exclusive) def move_selected_from_to(self, src, dst, exclusive=False): self.move_from_to(src, dst, self.selected_rows(src), exclusive) def move_from_to(self, src, dst, rows, exclusive=False): src_model = source_model(src) attrs = [src_model[r] for r in rows] for s1, s2 in reversed(list(slices(rows))): del src_model[s1:s2] dst_model = source_model(dst) dst_model.extend(attrs) self.commit() def __update_interface_state(self): last_view = self.__last_active_view if last_view is not None: self.update_interface_state(last_view) def update_interface_state(self, focus=None, selected=None, deselected=None): for view in [ self.available_attrs_view, self.used_attrs_view, self.class_attrs_view, self.meta_attrs_view ]: if view is not focus and not view.hasFocus() \ and view.selectionModel().hasSelection(): view.selectionModel().clear() def selected_vars(view): model = source_model(view) return [model[i] for i in self.selected_rows(view)] available_selected = selected_vars(self.available_attrs_view) attrs_selected = selected_vars(self.used_attrs_view) class_selected = selected_vars(self.class_attrs_view) meta_selected = selected_vars(self.meta_attrs_view) available_types = set(map(type, available_selected)) all_primitive = all(var.is_primitive() for var in available_types) move_attr_enabled = (available_selected and all_primitive) or \ attrs_selected self.move_attr_button.setEnabled(bool(move_attr_enabled)) if move_attr_enabled: self.move_attr_button.setText(">" if available_selected else "<") move_class_enabled = (all_primitive and available_selected) or class_selected self.move_class_button.setEnabled(bool(move_class_enabled)) if move_class_enabled: self.move_class_button.setText(">" if available_selected else "<") move_meta_enabled = available_selected or meta_selected self.move_meta_button.setEnabled(bool(move_meta_enabled)) if move_meta_enabled: self.move_meta_button.setText(">" if available_selected else "<") self.__last_active_view = None self.__interface_update_timer.stop() def commit(self): self.update_domain_role_hints() if self.data is not None: attributes = list(self.used_attrs) class_var = list(self.class_attrs) metas = list(self.meta_attrs) domain = Orange.data.Domain(attributes, class_var, metas) newdata = self.data.transform(domain) self.output_data = newdata self.Outputs.data.send(newdata) self.Outputs.features.send(widget.AttributeList(attributes)) else: self.output_data = None self.Outputs.data.send(None) self.Outputs.features.send(None) def reset(self): if self.data is not None: self.available_attrs[:] = [] self.used_attrs[:] = self.data.domain.attributes self.class_attrs[:] = self.data.domain.class_vars self.meta_attrs[:] = self.data.domain.metas self.update_domain_role_hints() self.commit() def send_report(self): if not self.data or not self.output_data: return in_domain, out_domain = self.data.domain, self.output_data.domain self.report_domain("Input data", self.data.domain) if (in_domain.attributes, in_domain.class_vars, in_domain.metas) == (out_domain.attributes, out_domain.class_vars, out_domain.metas): self.report_paragraph("Output data", "No changes.") else: self.report_domain("Output data", self.output_data.domain) diff = list( set(in_domain.variables + in_domain.metas) - set(out_domain.variables + out_domain.metas)) if diff: text = "%i (%s)" % (len(diff), ", ".join(x.name for x in diff)) self.report_items((("Removed", text), ))
class TabBarWidget(QWidget): """ A vertical tab bar widget using tool buttons as for tabs. """ currentChanged = Signal(int) def __init__(self, parent=None, **kwargs): QWidget.__init__(self, parent, **kwargs) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.__tabs = [] self.__currentIndex = -1 self.__changeOnHover = False self.__iconSize = QSize(26, 26) self.__group = QButtonGroup(self, exclusive=True) self.__group.buttonPressed[QAbstractButton].connect( self.__onButtonPressed) self.setMouseTracking(True) self.__sloppyButton = None self.__sloppyRegion = QRegion() self.__sloppyTimer = QTimer(self, singleShot=True) self.__sloppyTimer.timeout.connect(self.__onSloppyTimeout) def setChangeOnHover(self, changeOnHover): """ If set to ``True`` the tab widget will change the current index when the mouse hovers over a tab button. """ if self.__changeOnHover != changeOnHover: self.__changeOnHover = changeOnHover def changeOnHover(self): """ Does the current tab index follow the mouse cursor. """ return self.__changeOnHover def count(self): """ Return the number of tabs in the widget. """ return len(self.__tabs) def addTab(self, text, icon=None, toolTip=None): """ Add a new tab and return it's index. """ return self.insertTab(self.count(), text, icon, toolTip) def insertTab(self, index, text, icon=None, toolTip=None): """ Insert a tab at `index` """ button = TabButton(self, objectName="tab-button") button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) button.setIconSize(self.__iconSize) button.setMouseTracking(True) self.__group.addButton(button) button.installEventFilter(self) tab = _Tab(text, icon, toolTip, button, None, None) self.layout().insertWidget(index, button) self.__tabs.insert(index, tab) self.__updateTab(index) if self.currentIndex() == -1: self.setCurrentIndex(0) return index def removeTab(self, index): """ Remove a tab at `index`. """ if index >= 0 and index < self.count(): tab = self.__tabs.pop(index) layout_index = self.layout().indexOf(tab.button) if layout_index != -1: self.layout().takeAt(layout_index) self.__group.removeButton(tab.button) tab.button.removeEventFilter(self) if tab.button is self.__sloppyButton: self.__sloppyButton = None self.__sloppyRegion = QRegion() tab.button.deleteLater() tab.button.setParent(None) if self.currentIndex() == index: if self.count(): self.setCurrentIndex(max(index - 1, 0)) else: self.setCurrentIndex(-1) def setTabIcon(self, index, icon): """ Set the `icon` for tab at `index`. """ self.__tabs[index] = self.__tabs[index]._replace(icon=icon) self.__updateTab(index) def setTabToolTip(self, index, toolTip): """ Set `toolTip` for tab at `index`. """ self.__tabs[index] = self.__tabs[index]._replace(toolTip=toolTip) self.__updateTab(index) def setTabText(self, index, text): """ Set tab `text` for tab at `index` """ self.__tabs[index] = self.__tabs[index]._replace(text=text) self.__updateTab(index) def setTabPalette(self, index, palette): """ Set the tab button palette. """ self.__tabs[index] = self.__tabs[index]._replace(palette=palette) self.__updateTab(index) def setCurrentIndex(self, index): """ Set the current tab index. """ if self.__currentIndex != index: self.__currentIndex = index self.__sloppyRegion = QRegion() self.__sloppyButton = None if index != -1: self.__tabs[index].button.setChecked(True) self.currentChanged.emit(index) def currentIndex(self): """ Return the current index. """ return self.__currentIndex def button(self, index): """ Return the `TabButton` instance for index. """ return self.__tabs[index].button def setIconSize(self, size): if self.__iconSize != size: self.__iconSize = size for tab in self.__tabs: tab.button.setIconSize(self.__iconSize) def __updateTab(self, index): """ Update the tab button. """ tab = self.__tabs[index] b = tab.button if tab.text: b.setText(tab.text) if tab.icon is not None and not tab.icon.isNull(): b.setIcon(tab.icon) if tab.palette: b.setPalette(tab.palette) def __onButtonPressed(self, button): for i, tab in enumerate(self.__tabs): if tab.button is button: self.setCurrentIndex(i) break def __calcSloppyRegion(self, current): """ Given a current mouse cursor position return a region of the widget where hover/move events should change the current tab only on a timeout. """ p1 = current + QPoint(0, 2) p2 = current + QPoint(0, -2) p3 = self.pos() + QPoint(self.width() + 10, 0) p4 = self.pos() + QPoint(self.width() + 10, self.height()) return QRegion(QPolygon([p1, p2, p3, p4])) def __setSloppyButton(self, button): """ Set the current sloppy button (a tab button inside sloppy region) and reset the sloppy timeout. """ if not button.isChecked(): self.__sloppyButton = button delay = self.style().styleHint(QStyle.SH_Menu_SubMenuPopupDelay, None) # The delay timeout is the same as used by Qt in the QMenu. self.__sloppyTimer.start(delay) else: self.__sloppyTimer.stop() def __onSloppyTimeout(self): if self.__sloppyButton is not None: button = self.__sloppyButton self.__sloppyButton = None if not button.isChecked(): index = [tab.button for tab in self.__tabs].index(button) self.setCurrentIndex(index) def eventFilter(self, receiver, event): if event.type() == QEvent.MouseMove and \ isinstance(receiver, TabButton): pos = receiver.mapTo(self, event.pos()) if self.__sloppyRegion.contains(pos): self.__setSloppyButton(receiver) else: if not receiver.isChecked(): index = [tab.button for tab in self.__tabs].index(receiver) self.setCurrentIndex(index) #also update sloppy region if mouse is moved on the same icon self.__sloppyRegion = self.__calcSloppyRegion(pos) return QWidget.eventFilter(self, receiver, event) def leaveEvent(self, event): self.__sloppyButton = None self.__sloppyRegion = QRegion() return QWidget.leaveEvent(self, event)
class WidgetManager(QObject): """ OWWidget instance manager class. This class handles the lifetime of OWWidget instances in a :class:`WidgetsScheme`. """ #: A new OWWidget was created and added by the manager. widget_for_node_added = Signal(SchemeNode, QWidget) #: An OWWidget was removed, hidden and will be deleted when appropriate. widget_for_node_removed = Signal(SchemeNode, QWidget) class ProcessingState(enum.IntEnum): """Widget processing state flags""" #: Signal manager is updating/setting the widget's inputs InputUpdate = 1 #: Widget has entered a blocking state (OWWidget.isBlocking) BlockingUpdate = 2 #: Widget has entered processing state ProcessingUpdate = 4 #: Widget is still in the process of initialization Initializing = 8 InputUpdate, BlockingUpdate, ProcessingUpdate, Initializing = ProcessingState #: State mask for widgets that cannot be deleted immediately #: (see __try_delete) _DelayDeleteMask = InputUpdate | BlockingUpdate #: Widget initialization states Delayed = namedtuple("Delayed", ["node"]) PartiallyInitialized = namedtuple("Materializing", ["node", "partially_initialized_widget"]) Materialized = namedtuple("Materialized", ["node", "widget"]) class CreationPolicy(enum.Enum): """Widget Creation Policy""" #: Widgets are scheduled to be created from the event loop, or when #: first accessed with `widget_for_node` Normal = "Normal" #: Widgets are created immediately when added to the workflow model Immediate = "Immediate" #: Widgets are created only when first accessed with `widget_for_node` OnDemand = "OnDemand" Normal, Immediate, OnDemand = CreationPolicy def __init__(self, parent=None): QObject.__init__(self, parent) self.__scheme = None self.__signal_manager = None self.__widgets = [] self.__initstate_for_node = {} self.__creation_policy = WidgetManager.Normal #: a queue of all nodes whose widgets are scheduled for #: creation/initialization self.__init_queue = deque() # type: Deque[SchemeNode] #: Timer for scheduling widget initialization self.__init_timer = QTimer(self, interval=0, singleShot=True) self.__init_timer.timeout.connect(self.__create_delayed) #: A mapping of SchemeNode -> OWWidget (note: a mapping is only added #: after the widget is actually created) self.__widget_for_node = {} #: a mapping of OWWidget -> SchemeNode self.__node_for_widget = {} # Widgets that were 'removed' from the scheme but were at # the time in an input update loop and could not be deleted # immediately self.__delay_delete = set() #: processing state flags for all widgets (including the ones #: in __delay_delete). #: Note: widgets which have not yet been created do not have an entry self.__widget_processing_state = {} # Tracks the widget in the update loop by the SignalManager self.__updating_widget = None def set_scheme(self, scheme): """ Set the :class:`WidgetsScheme` instance to manage. """ self.__scheme = scheme self.__signal_manager = scheme.findChild(SignalManager) self.__signal_manager.processingStarted[SchemeNode].connect( self.__on_processing_started) self.__signal_manager.processingFinished[SchemeNode].connect( self.__on_processing_finished) scheme.node_added.connect(self.add_widget_for_node) scheme.node_removed.connect(self.remove_widget_for_node) scheme.runtime_env_changed.connect(self.__on_env_changed) scheme.installEventFilter(self) def scheme(self): """ Return the scheme instance on which this manager is installed. """ return self.__scheme def signal_manager(self): """ Return the signal manager in use on the :func:`scheme`. """ return self.__signal_manager def widget_for_node(self, node): """ Return the OWWidget instance for the scheme node. """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Delayed): # Create the widget now if it is still pending state = self.__materialize(state) return state.widget elif isinstance(state, WidgetManager.PartiallyInitialized): widget = state.partially_initialized_widget log.warning( "WidgetManager.widget_for_node: " "Accessing a partially created widget instance. " "This is most likely a result of explicit " "QApplication.processEvents call from the '%s.%s' " "widgets __init__.", type(widget).__module__, type(widget).__name__, ) return widget elif isinstance(state, WidgetManager.Materialized): return state.widget else: assert False def node_for_widget(self, widget): """ Return the SchemeNode instance for the OWWidget. Raise a KeyError if the widget does not map to a node in the scheme. """ return self.__node_for_widget[widget] def widget_properties(self, node): """ Return the current widget properties/settings. Parameters ---------- node : SchemeNode Returns ------- settings : dict """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Materialized): return state.widget.settingsHandler.pack_data(state.widget) else: return node.properties def set_creation_policy(self, policy): """ Set the widget creation policy Parameters ---------- policy : WidgetManager.CreationPolicy """ if self.__creation_policy != policy: self.__creation_policy = policy if self.__creation_policy == WidgetManager.Immediate: self.__init_timer.stop() while self.__init_queue: state = self.__init_queue.popleft() self.__materialize(state) elif self.__creation_policy == WidgetManager.Normal: if not self.__init_timer.isActive() and self.__init_queue: self.__init_timer.start() elif self.__creation_policy == WidgetManager.OnDemand: self.__init_timer.stop() else: assert False def creation_policy(self): """ Return the current widget creation policy Returns ------- policy: WidgetManager.CreationPolicy """ return self.__creation_policy def add_widget_for_node(self, node): """ Create a new OWWidget instance for the corresponding scheme node. """ state = WidgetManager.Delayed(node) self.__initstate_for_node[node] = state if self.__creation_policy == WidgetManager.Immediate: self.__initstate_for_node[node] = self.__materialize(state) elif self.__creation_policy == WidgetManager.Normal: self.__init_queue.append(state) if not self.__init_timer.isActive(): self.__init_timer.start() elif self.__creation_policy == WidgetManager.OnDemand: self.__init_queue.append(state) def __materialize(self, state): # Create and initialize an OWWidget for a Delayed # widget initialization assert isinstance(state, WidgetManager.Delayed) if state in self.__init_queue: self.__init_queue.remove(state) node = state.node widget = self.create_widget_instance(node) self.__widgets.append(widget) self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__initialize_widget_state(node, widget) state = WidgetManager.Materialized(node, widget) self.__initstate_for_node[node] = state self.widget_for_node_added.emit(node, widget) return state def remove_widget_for_node(self, node): """ Remove the OWWidget instance for node. """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Delayed): del self.__initstate_for_node[node] self.__init_queue.remove(state) elif isinstance(state, WidgetManager.Materialized): # Update the node's stored settings/properties dict before # removing the widget. # TODO: Update/sync whenever the widget settings change. node.properties = self._widget_settings(state.widget) self.__widgets.remove(state.widget) del self.__initstate_for_node[node] del self.__widget_for_node[node] del self.__node_for_widget[state.widget] node.title_changed.disconnect(state.widget.setCaption) state.widget.progressBarValueChanged.disconnect(node.set_progress) self.widget_for_node_removed.emit(node, state.widget) self._delete_widget(state.widget) elif isinstance(state, WidgetManager.PartiallyInitialized): widget = state.partially_initialized_widget raise RuntimeError( "A widget/node {} was removed while being initialized. " "This is most likely a result of an explicit " "QApplication.processEvents call from the '{}.{}' " "widgets __init__.\n".format(state.node.title, type(widget).__module__, type(widget).__init__)) def _widget_settings(self, widget): return widget.settingsHandler.pack_data(widget) def _delete_widget(self, widget): """ Delete the OWBaseWidget instance. """ widget.close() # Save settings to user global settings. widget.saveSettings() # Notify the widget it will be deleted. widget.onDeleteWidget() state = self.__widget_processing_state[widget] if state & WidgetManager._DelayDeleteMask: # If the widget is in an update loop and/or blocking we # delay the scheduled deletion until the widget is done. log.debug( "Widget %s removed but still in state :%s. " "Deferring deletion.", widget, state, ) self.__delay_delete.add(widget) else: widget.deleteLater() del self.__widget_processing_state[widget] def create_widget_instance(self, node): """ Create a OWWidget instance for the node. """ desc = node.description klass = widget = None initialized = False error = None # First try to actually retrieve the class. try: klass = name_lookup(desc.qualified_name) except (ImportError, AttributeError): sys.excepthook(*sys.exc_info()) error = "Could not import {0!r}\n\n{1}".format( node.description.qualified_name, traceback.format_exc()) except Exception: sys.excepthook(*sys.exc_info()) error = "An unexpected error during import of {0!r}\n\n{1}".format( node.description.qualified_name, traceback.format_exc()) if klass is None: widget = mock_error_owwidget(node, error) initialized = True if widget is None: log.info( "WidgetManager: Creating '%s.%s' instance '%s'.", klass.__module__, klass.__name__, node.title, ) widget = klass.__new__( klass, None, captionTitle=node.title, signal_manager=self.signal_manager(), stored_settings=node.properties, # NOTE: env is a view of the real env and reflects # changes to the environment. env=self.scheme().runtime_env(), ) initialized = False # Init the node/widget mapping and state before calling __init__ # Some OWWidgets might already send data in the constructor # (should this be forbidden? Raise a warning?) triggering the signal # manager which would request the widget => node mapping or state # Furthermore they can (though they REALLY REALLY REALLY should not) # explicitly call qApp.processEvents. assert node not in self.__widget_for_node self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__widget_processing_state[widget] = WidgetManager.Initializing self.__initstate_for_node[node] = WidgetManager.PartiallyInitialized( node, widget) if not initialized: try: widget.__init__() except Exception: sys.excepthook(*sys.exc_info()) msg = traceback.format_exc() msg = "Could not create {0!r}\n\n{1}".format( node.description.name, msg) # remove state tracking for widget ... del self.__widget_for_node[node] del self.__node_for_widget[widget] del self.__widget_processing_state[widget] # ... and substitute it with a mock error widget. widget = mock_error_owwidget(node, msg) self.__widget_for_node[node] = widget self.__node_for_widget[widget] = node self.__widget_processing_state[widget] = 0 self.__initstate_for_node[node] = WidgetManager.Materialized( node, widget) self.__initstate_for_node[node] = WidgetManager.Materialized( node, widget) # Clear Initializing flag self.__widget_processing_state[widget] &= ~WidgetManager.Initializing node.title_changed.connect(widget.setCaption) # Widget's info/warning/error messages. widget.messageActivated.connect(self.__on_widget_state_changed) widget.messageDeactivated.connect(self.__on_widget_state_changed) # Widget's statusTip node.set_status_message(widget.statusMessage()) widget.statusMessageChanged.connect(node.set_status_message) # Widget's progress bar value state. widget.progressBarValueChanged.connect(node.set_progress) # Widget processing state (progressBarInit/Finished) # and the blocking state. widget.processingStateChanged.connect( self.__on_processing_state_changed) widget.blockingStateChanged.connect(self.__on_blocking_state_changed) if widget.isBlocking(): # A widget can already enter blocking state in __init__ self.__widget_processing_state[widget] |= self.BlockingUpdate if widget.processingState != 0: # It can also start processing (initialization of resources, ...) self.__widget_processing_state[widget] |= self.ProcessingUpdate node.set_processing_state(1) node.set_progress(widget.progressBarValue) # Install a help shortcut on the widget help_action = widget.findChild(QAction, "action-help") if help_action is not None: help_action.setEnabled(True) help_action.setVisible(True) help_action.triggered.connect(self.__on_help_request) # Up shortcut (activate/open parent) up_shortcut = QShortcut(QKeySequence(Qt.ControlModifier + Qt.Key_Up), widget) up_shortcut.activated.connect(self.__on_activate_parent) # Call setters only after initialization. widget.setWindowIcon(icon_loader.from_description(desc).get(desc.icon)) widget.setCaption(node.title) # Schedule an update with the signal manager, due to the cleared # implicit Initializing flag self.signal_manager()._update() return widget def node_processing_state(self, node): """ Return the processing state flags for the node. Same as `manager.widget_processing_state(manger.widget_for_node(node))` """ state = self.__initstate_for_node[node] if isinstance(state, WidgetManager.Materialized): return self.__widget_processing_state[state.widget] elif isinstance(state, WidgetManager.PartiallyInitialized): return self.__widget_processing_state[ state.partially_initialized_widget] else: return WidgetManager.Initializing def widget_processing_state(self, widget): """ Return the processing state flags for the widget. The state is an bitwise or of `InputUpdate` and `BlockingUpdate`. """ return self.__widget_processing_state[widget] def __create_delayed(self): if self.__init_queue: state = self.__init_queue.popleft() node = state.node self.__initstate_for_node[node] = self.__materialize(state) if self.__creation_policy == WidgetManager.Normal and self.__init_queue: # restart the timer if pending widgets still in the queue self.__init_timer.start() def eventFilter(self, receiver, event): if event.type() == QEvent.Close and receiver is self.__scheme: self.signal_manager().stop() # Notify the widget instances. for widget in list(self.__widget_for_node.values()): widget.close() widget.saveSettings() widget.onDeleteWidget() widget.deleteLater() event.accept() return True return QObject.eventFilter(self, receiver, event) def __on_help_request(self): """ Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to the scheme and hope someone responds to it. """ # Sender is the QShortcut, and parent the OWBaseWidget widget = self.sender().parent() try: node = self.node_for_widget(widget) except KeyError: pass else: qualified_name = node.description.qualified_name help_url = "help://search?" + urlencode({"id": qualified_name}) event = QWhatsThisClickedEvent(help_url) QCoreApplication.sendEvent(self.scheme(), event) def __on_activate_parent(self): """ Activate parent shortcut was pressed. """ event = ActivateParentEvent() QCoreApplication.sendEvent(self.scheme(), event) def __initialize_widget_state(self, node, widget): """ Initialize the tracked info/warning/error message state. """ for message_group in widget.message_groups: message = user_message_from_state(message_group) if message: node.set_state_message(message) def __on_widget_state_changed(self, msg): """ The OWBaseWidget info/warning/error state has changed. """ widget = msg.group.widget try: node = self.node_for_widget(widget) except KeyError: pass else: self.__initialize_widget_state(node, widget) def __on_processing_state_changed(self, state): """ A widget processing state has changed (progressBarInit/Finished) """ widget = self.sender() if state: self.__widget_processing_state[widget] |= self.ProcessingUpdate else: self.__widget_processing_state[widget] &= ~self.ProcessingUpdate # propagate the change to the workflow model. try: # we can still track widget state after it was removed from the # workflow model (`__delay_delete`) node = self.node_for_widget(widget) except KeyError: pass else: self.__update_node_processing_state(node) def __on_processing_started(self, node): """ Signal manager entered the input update loop for the node. """ widget = self.widget_for_node(node) # Remember the widget instance. The node and the node->widget mapping # can be removed between this and __on_processing_finished. self.__updating_widget = widget self.__widget_processing_state[widget] |= self.InputUpdate self.__update_node_processing_state(node) def __on_processing_finished(self, node): """ Signal manager exited the input update loop for the node. """ widget = self.__updating_widget self.__widget_processing_state[widget] &= ~self.InputUpdate if widget in self.__node_for_widget: self.__update_node_processing_state(node) elif widget in self.__delay_delete: self.__try_delete(widget) else: raise ValueError("%r is not managed" % widget) self.__updating_widget = None def __on_blocking_state_changed(self, state): """ OWWidget blocking state has changed. """ if not state: # schedule an update pass. self.signal_manager()._update() widget = self.sender() if state: self.__widget_processing_state[widget] |= self.BlockingUpdate else: self.__widget_processing_state[widget] &= ~self.BlockingUpdate if widget in self.__node_for_widget: node = self.node_for_widget(widget) self.__update_node_processing_state(node) elif widget in self.__delay_delete: self.__try_delete(widget) def __update_node_processing_state(self, node): """ Update the `node.processing_state` to reflect the widget state. """ state = self.node_processing_state(node) node.set_processing_state(1 if state else 0) def __try_delete(self, widget): if not (self.__widget_processing_state[widget] & WidgetManager._DelayDeleteMask): log.debug("Delayed delete for widget %s", widget) self.__delay_delete.remove(widget) del self.__widget_processing_state[widget] widget.blockingStateChanged.disconnect( self.__on_blocking_state_changed) widget.processingStateChanged.disconnect( self.__on_processing_state_changed) widget.deleteLater() def __on_env_changed(self, key, newvalue, oldvalue): # Notify widgets of a runtime environment change for widget in self.__widget_for_node.values(): widget.workflowEnvChanged(key, newvalue, oldvalue)
def test_anchoritem(self): anchoritem = NodeAnchorItem(None) anchoritem.setAnimationEnabled(False) self.scene.addItem(anchoritem) path = QPainterPath() path.addEllipse(0, 0, 100, 100) anchoritem.setAnchorPath(path) anchor = AnchorPoint() anchoritem.addAnchor(anchor) ellipse1 = QGraphicsEllipseItem(-3, -3, 6, 6) ellipse2 = QGraphicsEllipseItem(-3, -3, 6, 6) self.scene.addItem(ellipse1) self.scene.addItem(ellipse2) anchor.scenePositionChanged.connect(ellipse1.setPos) with self.assertRaises(ValueError): anchoritem.addAnchor(anchor) anchor1 = AnchorPoint() anchoritem.addAnchor(anchor1) anchor1.scenePositionChanged.connect(ellipse2.setPos) self.assertSequenceEqual(anchoritem.anchorPoints(), [anchor, anchor1]) self.assertSequenceEqual(anchoritem.anchorPositions(), [2 / 3, 1 / 3]) anchoritem.setAnchorPositions([0.5, 0.0]) self.assertSequenceEqual(anchoritem.anchorPositions(), [0.5, 0.0]) def advance(): t = anchoritem.anchorPositions() t = [(t + 0.05) % 1.0 for t in t] anchoritem.setAnchorPositions(t) timer = QTimer(anchoritem, interval=10) timer.start() timer.timeout.connect(advance) self.qWait() timer.stop() anchoritem.setAnchorOpen(True) anchoritem.setHovered(True) self.assertEqual(*[p.scenePos() for p in anchoritem.anchorPoints()]) anchoritem.setAnchorOpen(False) self.assertNotEqual(*[p.scenePos() for p in anchoritem.anchorPoints()]) anchoritem.setAnchorOpen(False) anchoritem.setHovered(True) self.assertNotEqual(*[p.scenePos() for p in anchoritem.anchorPoints()]) anchoritem = NodeAnchorItem(None) anchoritem.setSignals([ InputSignal("first", "object", "set_first"), InputSignal("second", "object", "set_second") ]) self.assertListEqual( anchoritem._NodeAnchorItem__pathStroker.dashPattern(), list(anchoritem._NodeAnchorItem__unanchoredDash)) anchoritem.setAnchorOpen(True) anchoritem.setHovered(True) self.assertListEqual( anchoritem._NodeAnchorItem__pathStroker.dashPattern(), list(anchoritem._NodeAnchorItem__channelDash))
class OWScatterPlotBase(gui.OWComponent, QObject): """ Provide a graph component for widgets that show any kind of point plot The component plots a set of points with given coordinates, shapes, sizes and colors. Its function is similar to that of a *view*, whereas the widget represents a *model* and a *controler*. The model (widget) needs to provide methods: - `get_coordinates_data`, `get_size_data`, `get_color_data`, `get_shape_data`, `get_label_data`, which return a 1d array (or two arrays, for `get_coordinates_data`) of `dtype` `float64`, except for `get_label_data`, which returns formatted labels; - `get_color_labels`, `get_shape_labels`, which are return lists of strings used for the color and shape legend; - `get_tooltip`, which gives a tooltip for a single data point - (optional) `impute_sizes`, `impute_shapes` get final coordinates and shapes, and replace nans; - `get_subset_mask` returns a bool array indicating whether a data point is in the subset or not (e.g. in the 'Data Subset' signal in the Scatter plot and similar widgets); - `get_palette` returns a palette appropriate for visualizing the current color data; - `is_continuous_color` decides the type of the color legend; The widget (in a role of controller) must also provide methods - `selection_changed` If `get_coordinates_data` returns `(None, None)`, the plot is cleared. If `get_size_data`, `get_color_data` or `get_shape_data` return `None`, all points will have the same size, color or shape, respectively. If `get_label_data` returns `None`, there are no labels. The view (this compomnent) provides methods `update_coordinates`, `update_sizes`, `update_colors`, `update_shapes` and `update_labels` that the widget (in a role of a controler) should call when any of these properties are changed. If the widget calls, for instance, the plot's `update_colors`, the plot will react by calling the widget's `get_color_data` as well as the widget's methods needed to construct the legend. The view also provides a method `reset_graph`, which should be called only when - the widget gets entirely new data - the number of points may have changed, for instance when selecting a different attribute for x or y in the scatter plot, where the points with missing x or y coordinates are hidden. Every `update_something` calls the plot's `get_something`, which calls the model's `get_something_data`, then it transforms this data into whatever is needed (colors, shapes, scaled sizes) and changes the plot. For the simplest example, here is `update_shapes`: ``` def update_shapes(self): if self.scatterplot_item: shape_data = self.get_shapes() self.scatterplot_item.setSymbol(shape_data) self.update_legends() def get_shapes(self): shape_data = self.master.get_shape_data() shape_data = self.master.impute_shapes( shape_data, len(self.CurveSymbols) - 1) return self.CurveSymbols[shape_data] ``` On the widget's side, `get_something_data` is essentially just: ``` def get_size_data(self): return self.get_column(self.attr_size) ``` where `get_column` retrieves a column while also filtering out the points with missing x and y and so forth. (Here we present the simplest two cases, "shapes" for the view and "sizes" for the model. The colors for the view are more complicated since they deal with discrete and continuous palettes, and the shapes for the view merge infrequent shapes.) The plot can also show just a random sample of the data. The sample size is set by `set_sample_size`, and the rest is taken care by the plot: the widget keeps providing the data for all points, selection indices refer to the entire set etc. Internally, sampling happens as early as possible (in methods `get_<something>`). """ too_many_labels = Signal(bool) label_only_selected = Setting(False) point_width = Setting(10) alpha_value = Setting(128) show_grid = Setting(False) show_legend = Setting(True) class_density = Setting(False) jitter_size = Setting(0) resolution = 256 CurveSymbols = np.array("o x t + d s t2 t3 p h star ?".split()) MinShapeSize = 6 DarkerValue = 120 UnknownColor = (168, 50, 168) COLOR_NOT_SUBSET = (128, 128, 128, 0) COLOR_SUBSET = (128, 128, 128, 255) COLOR_DEFAULT = (128, 128, 128, 0) MAX_VISIBLE_LABELS = 500 def __init__(self, scatter_widget, parent=None, view_box=ViewBox): QObject.__init__(self) gui.OWComponent.__init__(self, scatter_widget) self.subset_is_shown = False self.view_box = view_box(self) self.plot_widget = pg.PlotWidget(viewBox=self.view_box, parent=parent, background="w") self.plot_widget.hideAxis("left") self.plot_widget.hideAxis("bottom") self.plot_widget.getPlotItem().buttonsHidden = True self.plot_widget.setAntialiasing(True) self.plot_widget.sizeHint = lambda: QSize(500, 500) self.density_img = None self.scatterplot_item = None self.scatterplot_item_sel = None self.labels = [] self.master = scatter_widget self._create_drag_tooltip(self.plot_widget.scene()) self.selection = None # np.ndarray self.n_valid = 0 self.n_shown = 0 self.sample_size = None self.sample_indices = None self.palette = None self.shape_legend = self._create_legend(((1, 0), (1, 0))) self.color_legend = self._create_legend(((1, 1), (1, 1))) self.update_legend_visibility() self.scale = None # DiscretizedScale self._too_many_labels = False # self.setMouseTracking(True) # self.grabGesture(QPinchGesture) # self.grabGesture(QPanGesture) self.update_grid_visibility() self._tooltip_delegate = EventDelegate(self.help_event) self.plot_widget.scene().installEventFilter(self._tooltip_delegate) self.view_box.sigTransformChanged.connect(self.update_density) self.view_box.sigRangeChangedManually.connect(self.update_labels) self.timer = None def _create_legend(self, anchor): legend = LegendItem() legend.setParentItem(self.plot_widget.getViewBox()) legend.restoreAnchor(anchor) return legend def _create_drag_tooltip(self, scene): tip_parts = [(Qt.ShiftModifier, "Shift: Add group"), (Qt.ShiftModifier + Qt.ControlModifier, "Shift-{}: Append to group".format( "Cmd" if sys.platform == "darwin" else "Ctrl")), (Qt.AltModifier, "Alt: Remove")] all_parts = ", ".join(part for _, part in tip_parts) self.tiptexts = { int(modifier): all_parts.replace(part, "<b>{}</b>".format(part)) for modifier, part in tip_parts } self.tiptexts[0] = all_parts self.tip_textitem = text = QGraphicsTextItem() # Set to the longest text text.setHtml(self.tiptexts[Qt.ShiftModifier + Qt.ControlModifier]) text.setPos(4, 2) r = text.boundingRect() rect = QGraphicsRectItem(0, 0, r.width() + 8, r.height() + 4) rect.setBrush(QColor(224, 224, 224, 212)) rect.setPen(QPen(Qt.NoPen)) self.update_tooltip() scene.drag_tooltip = scene.createItemGroup([rect, text]) scene.drag_tooltip.hide() def update_tooltip(self, modifiers=Qt.NoModifier): modifiers &= Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier text = self.tiptexts.get(int(modifiers), self.tiptexts[0]) self.tip_textitem.setHtml(text + self._get_jittering_tooltip()) def _get_jittering_tooltip(self): warn_jittered = "" if self.jitter_size: warn_jittered = \ '<br/><br/>' \ '<span style="background-color: red; color: white; ' \ 'font-weight: 500;">' \ ' Warning: Selection is applied to unjittered data ' \ '</span>' return warn_jittered def update_jittering(self): self.update_tooltip() x, y = self.get_coordinates() if x is None or not len(x) or self.scatterplot_item is None: return self._update_plot_coordinates(self.scatterplot_item, x, y) self._update_plot_coordinates(self.scatterplot_item_sel, x, y) self.update_labels() # TODO: Rename to remove_plot_items def clear(self): """ Remove all graphical elements from the plot Calls the pyqtgraph's plot widget's clear, sets all handles to `None`, removes labels and selections. This method should generally not be called by the widget. If the data is gone (*e.g.* upon receiving `None` as an input data signal), this should be handler by calling `reset_graph`, which will in turn call `clear`. Derived classes should override this method if they add more graphical elements. For instance, the regression line in the scatterplot adds `self.reg_line_item = None` (the line in the plot is already removed in this method). """ self.plot_widget.clear() self.density_img = None if self.timer is not None and self.timer.isActive(): self.timer.stop() self.timer = None self.scatterplot_item = None self.scatterplot_item_sel = None self.labels = [] self._signal_too_many_labels(False) self.view_box.init_history() self.view_box.tag_history() # TODO: I hate `keep_something` and `reset_something` arguments # __keep_selection is used exclusively be set_sample size which would # otherwise just repeat the code from reset_graph except for resetting # the selection. I'm uncomfortable with this; we may prefer to have a # method _reset_graph which does everything except resetting the selection, # and reset_graph would call it. def reset_graph(self, __keep_selection=False): """ Reset the graph to new data (or no data) The method must be called when the plot receives new data, in particular when the number of points change. If only their properties - like coordinates or shapes - change, an update method (`update_coordinates`, `update_shapes`...) should be called instead. The method must also be called when the data is gone. The method calls `clear`, followed by calls of all update methods. NB. Argument `__keep_selection` is for internal use only """ self.clear() if not __keep_selection: self.selection = None self.sample_indices = None self.update_coordinates() self.update_point_props() def set_sample_size(self, sample_size): """ Set the sample size Args: sample_size (int or None): sample size or `None` to show all points """ if self.sample_size != sample_size: self.sample_size = sample_size self.reset_graph(True) def update_point_props(self): """ Update the sizes, colors, shapes and labels The method calls the appropriate update methods for individual properties. """ self.update_sizes() self.update_colors() self.update_selection_colors() self.update_shapes() self.update_labels() # Coordinates # TODO: It could be nice if this method was run on entire data, not just # a sample. For this, however, it would need to either be called from # `get_coordinates` before sampling (very ugly) or call # `self.master.get_coordinates_data` (beyond ugly) or the widget would # have to store the ranges of unsampled data (ugly). # Maybe we leave it as it is. def _reset_view(self, x_data, y_data): """ Set the range of the view box Args: x_data (np.ndarray): x coordinates y_data (np.ndarray) y coordinates """ min_x, max_x = np.min(x_data), np.max(x_data) min_y, max_y = np.min(y_data), np.max(y_data) self.view_box.setRange(QRectF(min_x, min_y, max_x - min_x or 1, max_y - min_y or 1), padding=0.025) def _filter_visible(self, data): """Return the sample from the data using the stored sample_indices""" if data is None or self.sample_indices is None: return data else: return np.asarray(data[self.sample_indices]) def get_coordinates(self): """ Prepare coordinates of the points in the plot The method is called by `update_coordinates`. It gets the coordinates from the widget, jitters them and return them. The methods also initializes the sample indices if neededd and stores the original and sampled number of points. Returns: (tuple): a pair of numpy arrays containing (sampled) coordinates, or `(None, None)`. """ x, y = self.master.get_coordinates_data() if x is None: self.n_valid = self.n_shown = 0 return None, None self.n_valid = len(x) self._create_sample() x = self._filter_visible(x) y = self._filter_visible(y) # Jittering after sampling is OK if widgets do not change the sample # semi-permanently, e.g. take a sample for the duration of some # animation. If the sample size changes dynamically (like by adding # a "sample size" slider), points would move around when the sample # size changes. To prevent this, jittering should be done before # sampling (i.e. two lines earlier). This would slow it down somewhat. x, y = self.jitter_coordinates(x, y) return x, y def _create_sample(self): """ Create a random sample if the data is larger than the set sample size """ self.n_shown = min(self.n_valid, self.sample_size or self.n_valid) if self.sample_size is not None \ and self.sample_indices is None \ and self.n_valid != self.n_shown: random = np.random.RandomState(seed=0) self.sample_indices = random.choice(self.n_valid, self.n_shown, replace=False) # TODO: Is this really needed? np.sort(self.sample_indices) def jitter_coordinates(self, x, y): """ Display coordinates to random positions within ellipses with radiuses of `self.jittter_size` percents of spans """ if self.jitter_size == 0: return x, y return self._jitter_data(x, y) def _jitter_data(self, x, y, span_x=None, span_y=None): if span_x is None: span_x = np.max(x) - np.min(x) if span_y is None: span_y = np.max(y) - np.min(y) random = np.random.RandomState(seed=0) rs = random.uniform(0, 1, len(x)) phis = random.uniform(0, 2 * np.pi, len(x)) magnitude = self.jitter_size / 100 return (x + magnitude * span_x * rs * np.cos(phis), y + magnitude * span_y * rs * np.sin(phis)) def _update_plot_coordinates(self, plot, x, y): """ Change the coordinates of points while keeping other properites Note. Pyqtgraph does not offer a method for this: setting coordinates invalidates other data. We therefore retrieve the data to set it together with the coordinates. Pyqtgraph also does not offer a (documented) method for retrieving the data, yet using `plot.data[prop]` looks reasonably safe. The alternative, calling update for every property would essentially reset the graph, which can be time consuming. """ data = dict(x=x, y=y) for prop in ('pen', 'brush', 'size', 'symbol', 'data', 'sourceRect', 'targetRect'): data[prop] = plot.data[prop] plot.setData(**data) def update_coordinates(self): """ Trigger the update of coordinates while keeping other features intact. The method gets the coordinates by calling `self.get_coordinates`, which in turn calls the widget's `get_coordinate_data`. The number of coordinate pairs returned by the latter must match the current number of points. If this is not the case, the widget should trigger the complete update by calling `reset_graph` instead of this method. """ x, y = self.get_coordinates() if x is None or not len(x): return if self.scatterplot_item is None: if self.sample_indices is None: indices = np.arange(self.n_valid) else: indices = self.sample_indices kwargs = dict(x=x, y=y, data=indices) self.scatterplot_item = ScatterPlotItem(**kwargs) self.scatterplot_item.sigClicked.connect(self.select_by_click) self.scatterplot_item_sel = ScatterPlotItem(**kwargs) self.plot_widget.addItem(self.scatterplot_item_sel) self.plot_widget.addItem(self.scatterplot_item) else: self._update_plot_coordinates(self.scatterplot_item, x, y) self._update_plot_coordinates(self.scatterplot_item_sel, x, y) self.update_labels() self.update_density() # Todo: doesn't work: try MDS with density on self._reset_view(x, y) # Sizes def get_sizes(self): """ Prepare data for sizes of points in the plot The method is called by `update_sizes`. It gets the sizes from the widget and performs the necessary scaling and sizing. Returns: (np.ndarray): sizes """ size_column = self.master.get_size_data() if size_column is None: return np.full((self.n_shown, ), self.MinShapeSize + (5 + self.point_width) * 0.5) size_column = self._filter_visible(size_column) size_column = size_column.copy() with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) size_column -= np.nanmin(size_column) mx = np.nanmax(size_column) if mx > 0: size_column /= mx else: size_column[:] = 0.5 return self.MinShapeSize + (5 + self.point_width) * size_column def update_sizes(self): """ Trigger an update of point sizes The method calls `self.get_sizes`, which in turn calls the widget's `get_size_data`. The result are properly scaled and then passed back to widget for imputing (`master.impute_sizes`). """ if self.scatterplot_item: size_data = self.get_sizes() size_imputer = getattr(self.master, "impute_sizes", self.default_impute_sizes) size_imputer(size_data) if self.timer is not None and self.timer.isActive(): self.timer.stop() self.timer = None current_size_data = self.scatterplot_item.data["size"].copy() diff = size_data - current_size_data widget = self class Timeout: # 0.5 - np.cos(np.arange(0.17, 1, 0.17) * np.pi) / 2 factors = [0.07, 0.26, 0.52, 0.77, 0.95, 1] def __init__(self): self._counter = 0 def __call__(self): factor = self.factors[self._counter] self._counter += 1 size = current_size_data + diff * factor if len(self.factors) == self._counter: widget.timer.stop() widget.timer = None size = size_data widget.scatterplot_item.setSize(size) widget.scatterplot_item_sel.setSize(size + SELECTION_WIDTH) if np.sum(current_size_data) / self.n_valid != -1 and np.sum(diff): # If encountered any strange behaviour when updating sizes, # implement it with threads self.timer = QTimer(self.scatterplot_item, interval=50) self.timer.timeout.connect(Timeout()) self.timer.start() else: self.scatterplot_item.setSize(size_data) self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH) update_point_size = update_sizes # backward compatibility (needed?!) update_size = update_sizes @classmethod def default_impute_sizes(cls, size_data): """ Fallback imputation for sizes. Set the size to two pixels smaller than the minimal size Returns: (bool): True if there was any missing data """ nans = np.isnan(size_data) if np.any(nans): size_data[nans] = cls.MinShapeSize - 2 return True else: return False # Colors def get_colors(self): """ Prepare data for colors of the points in the plot The method is called by `update_colors`. It gets the colors and the indices of the data subset from the widget (`get_color_data`, `get_subset_mask`), and constructs lists of pens and brushes for each data point. The method uses different palettes for discrete and continuous data, as determined by calling the widget's method `is_continuous_color`. If also marks the points that are in the subset as defined by, for instance the 'Data Subset' signal in the Scatter plot and similar widgets. (Do not confuse this with *selected points*, which are marked by circles around the points, which are colored by groups and thus independent of this method.) Returns: (tuple): a list of pens and list of brushes """ self.palette = self.master.get_palette() c_data = self.master.get_color_data() c_data = self._filter_visible(c_data) subset = self.master.get_subset_mask() subset = self._filter_visible(subset) self.subset_is_shown = subset is not None if c_data is None: # same color return self._get_same_colors(subset) elif self.master.is_continuous_color(): return self._get_continuous_colors(c_data, subset) else: return self._get_discrete_colors(c_data, subset) def _get_same_colors(self, subset): """ Return the same pen for all points while the brush color depends upon whether the point is in the subset or not Args: subset (np.ndarray): a bool array indicating whether a data point is in the subset or not (e.g. in the 'Data Subset' signal in the Scatter plot and similar widgets); Returns: (tuple): a list of pens and list of brushes """ color = self.plot_widget.palette().color(OWPalette.Data) pen = [_make_pen(color, 1.5) for _ in range(self.n_shown)] if subset is not None: brush = np.where( subset, *(QBrush(QColor(*col)) for col in (self.COLOR_SUBSET, self.COLOR_NOT_SUBSET))) else: color = QColor(*self.COLOR_DEFAULT) color.setAlpha(self.alpha_value) brush = [QBrush(color) for _ in range(self.n_shown)] return pen, brush def _get_continuous_colors(self, c_data, subset): """ Return the pens and colors whose color represent an index into a continuous palette. The same color is used for pen and brush, except the former is darker. If the data has a subset, the brush is transparent for points that are not in the subset. """ if np.isnan(c_data).all(): self.scale = None else: self.scale = DiscretizedScale(np.nanmin(c_data), np.nanmax(c_data)) c_data -= self.scale.offset c_data /= self.scale.width c_data = np.floor(c_data) + 0.5 c_data /= self.scale.bins c_data = np.clip(c_data, 0, 1) pen = self.palette.getRGB(c_data) brush = np.hstack( [pen, np.full((len(pen), 1), self.alpha_value, dtype=int)]) pen *= 100 pen //= self.DarkerValue pen = [_make_pen(QColor(*col), 1.5) for col in pen.tolist()] if subset is not None: brush[:, 3] = 0 brush[subset, 3] = 255 brush = np.array([QBrush(QColor(*col)) for col in brush.tolist()]) return pen, brush def _get_discrete_colors(self, c_data, subset): """ Return the pens and colors whose color represent an index into a discrete palette. The same color is used for pen and brush, except the former is darker. If the data has a subset, the brush is transparent for points that are not in the subset. """ n_colors = self.palette.number_of_colors c_data = c_data.copy() c_data[np.isnan(c_data)] = n_colors c_data = c_data.astype(int) colors = np.r_[self.palette.getRGB(np.arange(n_colors)), [[128, 128, 128]]] pens = np.array([ _make_pen(QColor(*col).darker(self.DarkerValue), 1.5) for col in colors ]) pen = pens[c_data] alpha = self.alpha_value if subset is None else 255 brushes = np.array([[ QBrush(QColor(0, 0, 0, 0)), QBrush(QColor(col[0], col[1], col[2], alpha)) ] for col in colors]) brush = brushes[c_data] if subset is not None: brush = np.where(subset, brush[:, 1], brush[:, 0]) else: brush = brush[:, 1] return pen, brush def update_colors(self): """ Trigger an update of point sizes The method calls `self.get_colors`, which in turn calls the widget's `get_color_data` to get the indices in the pallette. `get_colors` returns a list of pens and brushes to which this method uses to update the colors. Finally, the method triggers the update of the legend and the density plot. """ if self.scatterplot_item is not None: pen_data, brush_data = self.get_colors() self.scatterplot_item.setPen(pen_data, update=False, mask=None) self.scatterplot_item.setBrush(brush_data, mask=None) self.update_legends() self.update_density() update_alpha_value = update_colors def update_density(self): """ Remove the existing density plot (if there is one) and replace it with a new one (if enabled). The method gets the colors from the pens of the currently plotted points. """ if self.density_img: self.plot_widget.removeItem(self.density_img) self.density_img = None if self.class_density and self.scatterplot_item is not None: rgb_data = [ pen.color().getRgb()[:3] if pen is not None else (255, 255, 255) for pen in self.scatterplot_item.data['pen'] ] if len(set(rgb_data)) <= 1: return [min_x, max_x], [min_y, max_y] = self.view_box.viewRange() x_data, y_data = self.scatterplot_item.getData() self.density_img = classdensity.class_density_image( min_x, max_x, min_y, max_y, self.resolution, x_data, y_data, rgb_data) self.plot_widget.addItem(self.density_img) def update_selection_colors(self): """ Trigger an update of selection markers This update method is usually not called by the widget but by the plot, since it is the plot that handles the selections. Like other update methods, it calls the corresponding get method (`get_colors_sel`) which returns a list of pens and brushes. """ if self.scatterplot_item_sel is None: return pen, brush = self.get_colors_sel() self.scatterplot_item_sel.setPen(pen, update=False, mask=None) self.scatterplot_item_sel.setBrush(brush, mask=None) def get_colors_sel(self): """ Return pens and brushes for selection markers. A pen can is set to `Qt.NoPen` if a point is not selected. All brushes are completely transparent whites. Returns: (tuple): a list of pens and a list of brushes """ nopen = QPen(Qt.NoPen) if self.selection is None: pen = [nopen] * self.n_shown else: sels = np.max(self.selection) if sels == 1: pen = np.where( self._filter_visible(self.selection), _make_pen(QColor(255, 190, 0, 255), SELECTION_WIDTH + 1), nopen) else: palette = ColorPaletteGenerator(number_of_colors=sels + 1) pen = np.choose(self._filter_visible(self.selection), [nopen] + [ _make_pen(palette[i], SELECTION_WIDTH + 1) for i in range(sels) ]) return pen, [QBrush(QColor(255, 255, 255, 0))] * self.n_shown # Labels def get_labels(self): """ Prepare data for labels for points The method returns the results of the widget's `get_label_data` Returns: (labels): a sequence of labels """ return self._filter_visible(self.master.get_label_data()) def update_labels(self): """ Trigger an update of labels The method calls `get_labels` which in turn calls the widget's `get_label_data`. The obtained labels are shown if the corresponding points are selected or if `label_only_selected` is `false`. """ for label in self.labels: self.plot_widget.removeItem(label) self.labels = [] mask = None if self.scatterplot_item is not None: x, y = self.scatterplot_item.getData() mask = self._label_mask(x, y) if mask is not None: labels = self.get_labels() if labels is None: mask = None self._signal_too_many_labels(mask is not None and mask.sum() > self.MAX_VISIBLE_LABELS) if self._too_many_labels or mask is None or not np.any(mask): return black = pg.mkColor(0, 0, 0) labels = labels[mask] x = x[mask] y = y[mask] for label, xp, yp in zip(labels, x, y): ti = TextItem(label, black) ti.setPos(xp, yp) self.plot_widget.addItem(ti) self.labels.append(ti) def _signal_too_many_labels(self, too_many): if self._too_many_labels != too_many: self._too_many_labels = too_many self.too_many_labels.emit(too_many) def _label_mask(self, x, y): (x0, x1), (y0, y1) = self.view_box.viewRange() mask = np.logical_and(np.logical_and(x >= x0, x <= x1), np.logical_and(y >= y0, y <= y1)) if self.label_only_selected: sub_mask = self._filter_visible(self.master.get_subset_mask()) if self.selection is None: if sub_mask is None: return None else: sel_mask = sub_mask else: sel_mask = self._filter_visible(self.selection) != 0 if sub_mask is not None: sel_mask = np.logical_or(sel_mask, sub_mask) mask = np.logical_and(mask, sel_mask) return mask # Shapes def get_shapes(self): """ Prepare data for shapes of points in the plot The method is called by `update_shapes`. It gets the data from the widget's `get_shape_data`, and then calls its `impute_shapes` to impute the missing shape (usually with some default shape). Returns: (np.ndarray): an array of symbols (e.g. o, x, + ...) """ shape_data = self.master.get_shape_data() shape_data = self._filter_visible(shape_data) # Data has to be copied so the imputation can change it in-place # TODO: Try avoiding this when we move imputation to the widget if shape_data is not None: shape_data = np.copy(shape_data) shape_imputer = getattr(self.master, "impute_shapes", self.default_impute_shapes) shape_imputer(shape_data, len(self.CurveSymbols) - 1) if isinstance(shape_data, np.ndarray): shape_data = shape_data.astype(int) else: shape_data = np.zeros(self.n_shown, dtype=int) return self.CurveSymbols[shape_data] @staticmethod def default_impute_shapes(shape_data, default_symbol): """ Fallback imputation for shapes. Use the default symbol, usually the last symbol in the list. Returns: (bool): True if there was any missing data """ if shape_data is None: return False nans = np.isnan(shape_data) if np.any(nans): shape_data[nans] = default_symbol return True else: return False def update_shapes(self): """ Trigger an update of point symbols The method calls `get_shapes` to obtain an array with a symbol for each point and uses it to update the symbols. Finally, the method updates the legend. """ if self.scatterplot_item: shape_data = self.get_shapes() self.scatterplot_item.setSymbol(shape_data) self.update_legends() def update_grid_visibility(self): """Show or hide the grid""" self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid) def update_legend_visibility(self): """ Show or hide legends based on whether they are enabled and non-empty """ self.shape_legend.setVisible(self.show_legend and bool(self.shape_legend.items)) self.color_legend.setVisible(self.show_legend and bool(self.color_legend.items)) def update_legends(self): """Update content of legends and their visibility""" cont_color = self.master.is_continuous_color() shape_labels = self.master.get_shape_labels() color_labels = None if cont_color else self.master.get_color_labels() if shape_labels == color_labels and shape_labels is not None: self._update_combined_legend(shape_labels) else: self._update_shape_legend(shape_labels) if cont_color: self._update_continuous_color_legend() else: self._update_color_legend(color_labels) self.update_legend_visibility() def _update_shape_legend(self, labels): self.shape_legend.clear() if labels is None or self.scatterplot_item is None: return color = QColor(0, 0, 0) color.setAlpha(self.alpha_value) for label, symbol in zip(labels, self.CurveSymbols): self.shape_legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, symbol=symbol), escape(label)) def _update_continuous_color_legend(self): self.color_legend.clear() if self.scale is None or self.scatterplot_item is None: return label = PaletteItemSample(self.palette, self.scale) self.color_legend.addItem(label, "") self.color_legend.setGeometry(label.boundingRect()) def _update_color_legend(self, labels): self.color_legend.clear() if labels is None: return self._update_colored_legend(self.color_legend, labels, 'o') def _update_combined_legend(self, labels): # update_colored_legend will already clear the shape legend # so we remove colors here use_legend = \ self.shape_legend if self.shape_legend.items else self.color_legend self.color_legend.clear() self.shape_legend.clear() self._update_colored_legend(use_legend, labels, self.CurveSymbols) def _update_colored_legend(self, legend, labels, symbols): if self.scatterplot_item is None or not self.palette: return if isinstance(symbols, str): symbols = itertools.repeat(symbols, times=len(labels)) for i, (label, symbol) in enumerate(zip(labels, symbols)): color = QColor(*self.palette.getRGB(i)) pen = _make_pen(color.darker(self.DarkerValue), 1.5) color.setAlpha(255 if self.subset_is_shown else self.alpha_value) brush = QBrush(color) legend.addItem( ScatterPlotItem(pen=pen, brush=brush, size=10, symbol=symbol), escape(label)) def zoom_button_clicked(self): self.plot_widget.getViewBox().setMouseMode( self.plot_widget.getViewBox().RectMode) def pan_button_clicked(self): self.plot_widget.getViewBox().setMouseMode( self.plot_widget.getViewBox().PanMode) def select_button_clicked(self): self.plot_widget.getViewBox().setMouseMode( self.plot_widget.getViewBox().RectMode) def reset_button_clicked(self): self.plot_widget.getViewBox().autoRange() self.update_labels() def select_by_click(self, _, points): if self.scatterplot_item is not None: self.select(points) def select_by_rectangle(self, value_rect): if self.scatterplot_item is not None: x0, y0 = value_rect.topLeft().x(), value_rect.topLeft().y() x1, y1 = value_rect.bottomRight().x(), value_rect.bottomRight().y() x, y = self.master.get_coordinates_data() indices = np.flatnonzero((x0 <= x) & (x <= x1) & (y0 <= y) & (y <= y1)) self.select_by_indices(indices.astype(int)) def unselect_all(self): if self.selection is not None: self.selection = None self.update_selection_colors() if self.label_only_selected: self.update_labels() self.master.selection_changed() def select(self, points): # noinspection PyArgumentList if self.scatterplot_item is None: return indices = [p.data() for p in points] self.select_by_indices(indices) def select_by_indices(self, indices): if self.selection is None: self.selection = np.zeros(self.n_valid, dtype=np.uint8) keys = QApplication.keyboardModifiers() if keys & Qt.AltModifier: self.selection_remove(indices) elif keys & Qt.ShiftModifier and keys & Qt.ControlModifier: self.selection_append(indices) elif keys & Qt.ShiftModifier: self.selection_new_group(indices) else: self.selection_select(indices) def selection_select(self, indices): self.selection = np.zeros(self.n_valid, dtype=np.uint8) self.selection[indices] = 1 self._update_after_selection() def selection_append(self, indices): self.selection[indices] = np.max(self.selection) self._update_after_selection() def selection_new_group(self, indices): self.selection[indices] = np.max(self.selection) + 1 self._update_after_selection() def selection_remove(self, indices): self.selection[indices] = 0 self._update_after_selection() def _update_after_selection(self): self._compress_indices() self.update_selection_colors() if self.label_only_selected: self.update_labels() self.master.selection_changed() def _compress_indices(self): indices = sorted(set(self.selection) | {0}) if len(indices) == max(indices) + 1: return mapping = np.zeros((max(indices) + 1, ), dtype=int) for i, ind in enumerate(indices): mapping[ind] = i self.selection = mapping[self.selection] def get_selection(self): if self.selection is None: return np.array([], dtype=np.uint8) else: return np.flatnonzero(self.selection) def help_event(self, event): """ Create a `QToolTip` for the point hovered by the mouse """ if self.scatterplot_item is None: return False act_pos = self.scatterplot_item.mapFromScene(event.scenePos()) point_data = [ p.data() for p in self.scatterplot_item.pointsAt(act_pos) ] text = self.master.get_tooltip(point_data) if text: QToolTip.showText(event.screenPos(), text, widget=self.plot_widget) return True else: return False
class OWScatterPlotBase(gui.OWComponent, QObject): """ Provide a graph component for widgets that show any kind of point plot The component plots a set of points with given coordinates, shapes, sizes and colors. Its function is similar to that of a *view*, whereas the widget represents a *model* and a *controler*. The model (widget) needs to provide methods: - `get_coordinates_data`, `get_size_data`, `get_color_data`, `get_shape_data`, `get_label_data`, which return a 1d array (or two arrays, for `get_coordinates_data`) of `dtype` `float64`, except for `get_label_data`, which returns formatted labels; - `get_color_labels`, `get_shape_labels`, which are return lists of strings used for the color and shape legend; - `get_tooltip`, which gives a tooltip for a single data point - (optional) `impute_sizes`, `impute_shapes` get final coordinates and shapes, and replace nans; - `get_subset_mask` returns a bool array indicating whether a data point is in the subset or not (e.g. in the 'Data Subset' signal in the Scatter plot and similar widgets); - `get_palette` returns a palette appropriate for visualizing the current color data; - `is_continuous_color` decides the type of the color legend; The widget (in a role of controller) must also provide methods - `selection_changed` If `get_coordinates_data` returns `(None, None)`, the plot is cleared. If `get_size_data`, `get_color_data` or `get_shape_data` return `None`, all points will have the same size, color or shape, respectively. If `get_label_data` returns `None`, there are no labels. The view (this compomnent) provides methods `update_coordinates`, `update_sizes`, `update_colors`, `update_shapes` and `update_labels` that the widget (in a role of a controler) should call when any of these properties are changed. If the widget calls, for instance, the plot's `update_colors`, the plot will react by calling the widget's `get_color_data` as well as the widget's methods needed to construct the legend. The view also provides a method `reset_graph`, which should be called only when - the widget gets entirely new data - the number of points may have changed, for instance when selecting a different attribute for x or y in the scatter plot, where the points with missing x or y coordinates are hidden. Every `update_something` calls the plot's `get_something`, which calls the model's `get_something_data`, then it transforms this data into whatever is needed (colors, shapes, scaled sizes) and changes the plot. For the simplest example, here is `update_shapes`: ``` def update_shapes(self): if self.scatterplot_item: shape_data = self.get_shapes() self.scatterplot_item.setSymbol(shape_data) self.update_legends() def get_shapes(self): shape_data = self.master.get_shape_data() shape_data = self.master.impute_shapes( shape_data, len(self.CurveSymbols) - 1) return self.CurveSymbols[shape_data] ``` On the widget's side, `get_something_data` is essentially just: ``` def get_size_data(self): return self.get_column(self.attr_size) ``` where `get_column` retrieves a column while also filtering out the points with missing x and y and so forth. (Here we present the simplest two cases, "shapes" for the view and "sizes" for the model. The colors for the view are more complicated since they deal with discrete and continuous palettes, and the shapes for the view merge infrequent shapes.) The plot can also show just a random sample of the data. The sample size is set by `set_sample_size`, and the rest is taken care by the plot: the widget keeps providing the data for all points, selection indices refer to the entire set etc. Internally, sampling happens as early as possible (in methods `get_<something>`). """ too_many_labels = Signal(bool) label_only_selected = Setting(False) point_width = Setting(10) alpha_value = Setting(128) show_grid = Setting(False) show_legend = Setting(True) class_density = Setting(False) jitter_size = Setting(0) resolution = 256 CurveSymbols = np.array("o x t + d s t2 t3 p h star ?".split()) MinShapeSize = 6 DarkerValue = 120 UnknownColor = (168, 50, 168) COLOR_NOT_SUBSET = (128, 128, 128, 0) COLOR_SUBSET = (128, 128, 128, 255) COLOR_DEFAULT = (128, 128, 128, 0) MAX_VISIBLE_LABELS = 500 def __init__(self, scatter_widget, parent=None, view_box=ViewBox): QObject.__init__(self) gui.OWComponent.__init__(self, scatter_widget) self.subset_is_shown = False self.view_box = view_box(self) self.plot_widget = pg.PlotWidget(viewBox=self.view_box, parent=parent, background="w") self.plot_widget.hideAxis("left") self.plot_widget.hideAxis("bottom") self.plot_widget.getPlotItem().buttonsHidden = True self.plot_widget.setAntialiasing(True) self.plot_widget.sizeHint = lambda: QSize(500, 500) self.density_img = None self.scatterplot_item = None self.scatterplot_item_sel = None self.labels = [] self.master = scatter_widget self._create_drag_tooltip(self.plot_widget.scene()) self.selection = None # np.ndarray self.n_valid = 0 self.n_shown = 0 self.sample_size = None self.sample_indices = None self.palette = None self.shape_legend = self._create_legend(((1, 0), (1, 0))) self.color_legend = self._create_legend(((1, 1), (1, 1))) self.update_legend_visibility() self.scale = None # DiscretizedScale self._too_many_labels = False # self.setMouseTracking(True) # self.grabGesture(QPinchGesture) # self.grabGesture(QPanGesture) self.update_grid_visibility() self._tooltip_delegate = EventDelegate(self.help_event) self.plot_widget.scene().installEventFilter(self._tooltip_delegate) self.view_box.sigTransformChanged.connect(self.update_density) self.view_box.sigRangeChangedManually.connect(self.update_labels) self.timer = None def _create_legend(self, anchor): legend = LegendItem() legend.setParentItem(self.plot_widget.getViewBox()) legend.restoreAnchor(anchor) return legend def _create_drag_tooltip(self, scene): tip_parts = [ (Qt.ShiftModifier, "Shift: Add group"), (Qt.ShiftModifier + Qt.ControlModifier, "Shift-{}: Append to group". format("Cmd" if sys.platform == "darwin" else "Ctrl")), (Qt.AltModifier, "Alt: Remove") ] all_parts = ", ".join(part for _, part in tip_parts) self.tiptexts = { int(modifier): all_parts.replace(part, "<b>{}</b>".format(part)) for modifier, part in tip_parts } self.tiptexts[0] = all_parts self.tip_textitem = text = QGraphicsTextItem() # Set to the longest text text.setHtml(self.tiptexts[Qt.ShiftModifier + Qt.ControlModifier]) text.setPos(4, 2) r = text.boundingRect() rect = QGraphicsRectItem(0, 0, r.width() + 8, r.height() + 4) rect.setBrush(QColor(224, 224, 224, 212)) rect.setPen(QPen(Qt.NoPen)) self.update_tooltip() scene.drag_tooltip = scene.createItemGroup([rect, text]) scene.drag_tooltip.hide() def update_tooltip(self, modifiers=Qt.NoModifier): modifiers &= Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier text = self.tiptexts.get(int(modifiers), self.tiptexts[0]) self.tip_textitem.setHtml(text + self._get_jittering_tooltip()) def _get_jittering_tooltip(self): warn_jittered = "" if self.jitter_size: warn_jittered = \ '<br/><br/>' \ '<span style="background-color: red; color: white; ' \ 'font-weight: 500;">' \ ' Warning: Selection is applied to unjittered data ' \ '</span>' return warn_jittered def update_jittering(self): self.update_tooltip() x, y = self.get_coordinates() if x is None or not len(x) or self.scatterplot_item is None: return self._update_plot_coordinates(self.scatterplot_item, x, y) self._update_plot_coordinates(self.scatterplot_item_sel, x, y) self.update_labels() # TODO: Rename to remove_plot_items def clear(self): """ Remove all graphical elements from the plot Calls the pyqtgraph's plot widget's clear, sets all handles to `None`, removes labels and selections. This method should generally not be called by the widget. If the data is gone (*e.g.* upon receiving `None` as an input data signal), this should be handler by calling `reset_graph`, which will in turn call `clear`. Derived classes should override this method if they add more graphical elements. For instance, the regression line in the scatterplot adds `self.reg_line_item = None` (the line in the plot is already removed in this method). """ self.plot_widget.clear() self.density_img = None if self.timer is not None and self.timer.isActive(): self.timer.stop() self.timer = None self.scatterplot_item = None self.scatterplot_item_sel = None self.labels = [] self._signal_too_many_labels(False) self.view_box.init_history() self.view_box.tag_history() # TODO: I hate `keep_something` and `reset_something` arguments # __keep_selection is used exclusively be set_sample size which would # otherwise just repeat the code from reset_graph except for resetting # the selection. I'm uncomfortable with this; we may prefer to have a # method _reset_graph which does everything except resetting the selection, # and reset_graph would call it. def reset_graph(self, __keep_selection=False): """ Reset the graph to new data (or no data) The method must be called when the plot receives new data, in particular when the number of points change. If only their properties - like coordinates or shapes - change, an update method (`update_coordinates`, `update_shapes`...) should be called instead. The method must also be called when the data is gone. The method calls `clear`, followed by calls of all update methods. NB. Argument `__keep_selection` is for internal use only """ self.clear() if not __keep_selection: self.selection = None self.sample_indices = None self.update_coordinates() self.update_point_props() def set_sample_size(self, sample_size): """ Set the sample size Args: sample_size (int or None): sample size or `None` to show all points """ if self.sample_size != sample_size: self.sample_size = sample_size self.reset_graph(True) def update_point_props(self): """ Update the sizes, colors, shapes and labels The method calls the appropriate update methods for individual properties. """ self.update_sizes() self.update_colors() self.update_selection_colors() self.update_shapes() self.update_labels() # Coordinates # TODO: It could be nice if this method was run on entire data, not just # a sample. For this, however, it would need to either be called from # `get_coordinates` before sampling (very ugly) or call # `self.master.get_coordinates_data` (beyond ugly) or the widget would # have to store the ranges of unsampled data (ugly). # Maybe we leave it as it is. def _reset_view(self, x_data, y_data): """ Set the range of the view box Args: x_data (np.ndarray): x coordinates y_data (np.ndarray) y coordinates """ min_x, max_x = np.min(x_data), np.max(x_data) min_y, max_y = np.min(y_data), np.max(y_data) self.view_box.setRange( QRectF(min_x, min_y, max_x - min_x or 1, max_y - min_y or 1), padding=0.025) def _filter_visible(self, data): """Return the sample from the data using the stored sample_indices""" if data is None or self.sample_indices is None: return data else: return np.asarray(data[self.sample_indices]) def get_coordinates(self): """ Prepare coordinates of the points in the plot The method is called by `update_coordinates`. It gets the coordinates from the widget, jitters them and return them. The methods also initializes the sample indices if neededd and stores the original and sampled number of points. Returns: (tuple): a pair of numpy arrays containing (sampled) coordinates, or `(None, None)`. """ x, y = self.master.get_coordinates_data() if x is None: self.n_valid = self.n_shown = 0 return None, None self.n_valid = len(x) self._create_sample() x = self._filter_visible(x) y = self._filter_visible(y) # Jittering after sampling is OK if widgets do not change the sample # semi-permanently, e.g. take a sample for the duration of some # animation. If the sample size changes dynamically (like by adding # a "sample size" slider), points would move around when the sample # size changes. To prevent this, jittering should be done before # sampling (i.e. two lines earlier). This would slow it down somewhat. x, y = self.jitter_coordinates(x, y) return x, y def _create_sample(self): """ Create a random sample if the data is larger than the set sample size """ self.n_shown = min(self.n_valid, self.sample_size or self.n_valid) if self.sample_size is not None \ and self.sample_indices is None \ and self.n_valid != self.n_shown: random = np.random.RandomState(seed=0) self.sample_indices = random.choice( self.n_valid, self.n_shown, replace=False) # TODO: Is this really needed? np.sort(self.sample_indices) def jitter_coordinates(self, x, y): """ Display coordinates to random positions within ellipses with radiuses of `self.jittter_size` percents of spans """ if self.jitter_size == 0: return x, y return self._jitter_data(x, y) def _jitter_data(self, x, y, span_x=None, span_y=None): if span_x is None: span_x = np.max(x) - np.min(x) if span_y is None: span_y = np.max(y) - np.min(y) random = np.random.RandomState(seed=0) rs = random.uniform(0, 1, len(x)) phis = random.uniform(0, 2 * np.pi, len(x)) magnitude = self.jitter_size / 100 return (x + magnitude * span_x * rs * np.cos(phis), y + magnitude * span_y * rs * np.sin(phis)) def _update_plot_coordinates(self, plot, x, y): """ Change the coordinates of points while keeping other properites Note. Pyqtgraph does not offer a method for this: setting coordinates invalidates other data. We therefore retrieve the data to set it together with the coordinates. Pyqtgraph also does not offer a (documented) method for retrieving the data, yet using `plot.data[prop]` looks reasonably safe. The alternative, calling update for every property would essentially reset the graph, which can be time consuming. """ data = dict(x=x, y=y) for prop in ('pen', 'brush', 'size', 'symbol', 'data', 'sourceRect', 'targetRect'): data[prop] = plot.data[prop] plot.setData(**data) def update_coordinates(self): """ Trigger the update of coordinates while keeping other features intact. The method gets the coordinates by calling `self.get_coordinates`, which in turn calls the widget's `get_coordinate_data`. The number of coordinate pairs returned by the latter must match the current number of points. If this is not the case, the widget should trigger the complete update by calling `reset_graph` instead of this method. """ x, y = self.get_coordinates() if x is None or not len(x): return if self.scatterplot_item is None: if self.sample_indices is None: indices = np.arange(self.n_valid) else: indices = self.sample_indices kwargs = dict(x=x, y=y, data=indices) self.scatterplot_item = ScatterPlotItem(**kwargs) self.scatterplot_item.sigClicked.connect(self.select_by_click) self.scatterplot_item_sel = ScatterPlotItem(**kwargs) self.plot_widget.addItem(self.scatterplot_item_sel) self.plot_widget.addItem(self.scatterplot_item) else: self._update_plot_coordinates(self.scatterplot_item, x, y) self._update_plot_coordinates(self.scatterplot_item_sel, x, y) self.update_labels() self.update_density() # Todo: doesn't work: try MDS with density on self._reset_view(x, y) # Sizes def get_sizes(self): """ Prepare data for sizes of points in the plot The method is called by `update_sizes`. It gets the sizes from the widget and performs the necessary scaling and sizing. Returns: (np.ndarray): sizes """ size_column = self.master.get_size_data() if size_column is None: return np.full((self.n_shown,), self.MinShapeSize + (5 + self.point_width) * 0.5) size_column = self._filter_visible(size_column) size_column = size_column.copy() with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) size_column -= np.nanmin(size_column) mx = np.nanmax(size_column) if mx > 0: size_column /= mx else: size_column[:] = 0.5 return self.MinShapeSize + (5 + self.point_width) * size_column def update_sizes(self): """ Trigger an update of point sizes The method calls `self.get_sizes`, which in turn calls the widget's `get_size_data`. The result are properly scaled and then passed back to widget for imputing (`master.impute_sizes`). """ if self.scatterplot_item: size_data = self.get_sizes() size_imputer = getattr( self.master, "impute_sizes", self.default_impute_sizes) size_imputer(size_data) if self.timer is not None and self.timer.isActive(): self.timer.stop() self.timer = None current_size_data = self.scatterplot_item.data["size"].copy() diff = size_data - current_size_data widget = self class Timeout: # 0.5 - np.cos(np.arange(0.17, 1, 0.17) * np.pi) / 2 factors = [0.07, 0.26, 0.52, 0.77, 0.95, 1] def __init__(self): self._counter = 0 def __call__(self): factor = self.factors[self._counter] self._counter += 1 size = current_size_data + diff * factor if len(self.factors) == self._counter: widget.timer.stop() widget.timer = None size = size_data widget.scatterplot_item.setSize(size) widget.scatterplot_item_sel.setSize(size + SELECTION_WIDTH) if np.sum(current_size_data) / self.n_valid != -1 and np.sum(diff): # If encountered any strange behaviour when updating sizes, # implement it with threads self.timer = QTimer(self.scatterplot_item, interval=50) self.timer.timeout.connect(Timeout()) self.timer.start() else: self.scatterplot_item.setSize(size_data) self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH) update_point_size = update_sizes # backward compatibility (needed?!) update_size = update_sizes @classmethod def default_impute_sizes(cls, size_data): """ Fallback imputation for sizes. Set the size to two pixels smaller than the minimal size Returns: (bool): True if there was any missing data """ nans = np.isnan(size_data) if np.any(nans): size_data[nans] = cls.MinShapeSize - 2 return True else: return False # Colors def get_colors(self): """ Prepare data for colors of the points in the plot The method is called by `update_colors`. It gets the colors and the indices of the data subset from the widget (`get_color_data`, `get_subset_mask`), and constructs lists of pens and brushes for each data point. The method uses different palettes for discrete and continuous data, as determined by calling the widget's method `is_continuous_color`. If also marks the points that are in the subset as defined by, for instance the 'Data Subset' signal in the Scatter plot and similar widgets. (Do not confuse this with *selected points*, which are marked by circles around the points, which are colored by groups and thus independent of this method.) Returns: (tuple): a list of pens and list of brushes """ self.palette = self.master.get_palette() c_data = self.master.get_color_data() c_data = self._filter_visible(c_data) subset = self.master.get_subset_mask() subset = self._filter_visible(subset) self.subset_is_shown = subset is not None if c_data is None: # same color return self._get_same_colors(subset) elif self.master.is_continuous_color(): return self._get_continuous_colors(c_data, subset) else: return self._get_discrete_colors(c_data, subset) def _get_same_colors(self, subset): """ Return the same pen for all points while the brush color depends upon whether the point is in the subset or not Args: subset (np.ndarray): a bool array indicating whether a data point is in the subset or not (e.g. in the 'Data Subset' signal in the Scatter plot and similar widgets); Returns: (tuple): a list of pens and list of brushes """ color = self.plot_widget.palette().color(OWPalette.Data) pen = [_make_pen(color, 1.5) for _ in range(self.n_shown)] if subset is not None: brush = np.where( subset, *(QBrush(QColor(*col)) for col in (self.COLOR_SUBSET, self.COLOR_NOT_SUBSET))) else: color = QColor(*self.COLOR_DEFAULT) color.setAlpha(self.alpha_value) brush = [QBrush(color) for _ in range(self.n_shown)] return pen, brush def _get_continuous_colors(self, c_data, subset): """ Return the pens and colors whose color represent an index into a continuous palette. The same color is used for pen and brush, except the former is darker. If the data has a subset, the brush is transparent for points that are not in the subset. """ if np.isnan(c_data).all(): self.scale = None else: self.scale = DiscretizedScale(np.nanmin(c_data), np.nanmax(c_data)) c_data -= self.scale.offset c_data /= self.scale.width c_data = np.floor(c_data) + 0.5 c_data /= self.scale.bins c_data = np.clip(c_data, 0, 1) pen = self.palette.getRGB(c_data) brush = np.hstack( [pen, np.full((len(pen), 1), self.alpha_value, dtype=int)]) pen *= 100 pen //= self.DarkerValue pen = [_make_pen(QColor(*col), 1.5) for col in pen.tolist()] if subset is not None: brush[:, 3] = 0 brush[subset, 3] = 255 brush = np.array([QBrush(QColor(*col)) for col in brush.tolist()]) return pen, brush def _get_discrete_colors(self, c_data, subset): """ Return the pens and colors whose color represent an index into a discrete palette. The same color is used for pen and brush, except the former is darker. If the data has a subset, the brush is transparent for points that are not in the subset. """ n_colors = self.palette.number_of_colors c_data = c_data.copy() c_data[np.isnan(c_data)] = n_colors c_data = c_data.astype(int) colors = np.r_[self.palette.getRGB(np.arange(n_colors)), [[128, 128, 128]]] pens = np.array( [_make_pen(QColor(*col).darker(self.DarkerValue), 1.5) for col in colors]) pen = pens[c_data] alpha = self.alpha_value if subset is None else 255 brushes = np.array([ [QBrush(QColor(0, 0, 0, 0)), QBrush(QColor(col[0], col[1], col[2], alpha))] for col in colors]) brush = brushes[c_data] if subset is not None: brush = np.where(subset, brush[:, 1], brush[:, 0]) else: brush = brush[:, 1] return pen, brush def update_colors(self): """ Trigger an update of point sizes The method calls `self.get_colors`, which in turn calls the widget's `get_color_data` to get the indices in the pallette. `get_colors` returns a list of pens and brushes to which this method uses to update the colors. Finally, the method triggers the update of the legend and the density plot. """ if self.scatterplot_item is not None: pen_data, brush_data = self.get_colors() self.scatterplot_item.setPen(pen_data, update=False, mask=None) self.scatterplot_item.setBrush(brush_data, mask=None) self.update_legends() self.update_density() update_alpha_value = update_colors def update_density(self): """ Remove the existing density plot (if there is one) and replace it with a new one (if enabled). The method gets the colors from the pens of the currently plotted points. """ if self.density_img: self.plot_widget.removeItem(self.density_img) self.density_img = None if self.class_density and self.scatterplot_item is not None: rgb_data = [ pen.color().getRgb()[:3] if pen is not None else (255, 255, 255) for pen in self.scatterplot_item.data['pen']] if len(set(rgb_data)) <= 1: return [min_x, max_x], [min_y, max_y] = self.view_box.viewRange() x_data, y_data = self.scatterplot_item.getData() self.density_img = classdensity.class_density_image( min_x, max_x, min_y, max_y, self.resolution, x_data, y_data, rgb_data) self.plot_widget.addItem(self.density_img) def update_selection_colors(self): """ Trigger an update of selection markers This update method is usually not called by the widget but by the plot, since it is the plot that handles the selections. Like other update methods, it calls the corresponding get method (`get_colors_sel`) which returns a list of pens and brushes. """ if self.scatterplot_item_sel is None: return pen, brush = self.get_colors_sel() self.scatterplot_item_sel.setPen(pen, update=False, mask=None) self.scatterplot_item_sel.setBrush(brush, mask=None) def get_colors_sel(self): """ Return pens and brushes for selection markers. A pen can is set to `Qt.NoPen` if a point is not selected. All brushes are completely transparent whites. Returns: (tuple): a list of pens and a list of brushes """ nopen = QPen(Qt.NoPen) if self.selection is None: pen = [nopen] * self.n_shown else: sels = np.max(self.selection) if sels == 1: pen = np.where( self._filter_visible(self.selection), _make_pen(QColor(255, 190, 0, 255), SELECTION_WIDTH + 1), nopen) else: palette = ColorPaletteGenerator(number_of_colors=sels + 1) pen = np.choose( self._filter_visible(self.selection), [nopen] + [_make_pen(palette[i], SELECTION_WIDTH + 1) for i in range(sels)]) return pen, [QBrush(QColor(255, 255, 255, 0))] * self.n_shown # Labels def get_labels(self): """ Prepare data for labels for points The method returns the results of the widget's `get_label_data` Returns: (labels): a sequence of labels """ return self._filter_visible(self.master.get_label_data()) def update_labels(self): """ Trigger an update of labels The method calls `get_labels` which in turn calls the widget's `get_label_data`. The obtained labels are shown if the corresponding points are selected or if `label_only_selected` is `false`. """ for label in self.labels: self.plot_widget.removeItem(label) self.labels = [] mask = None if self.scatterplot_item is not None: x, y = self.scatterplot_item.getData() mask = self._label_mask(x, y) if mask is not None: labels = self.get_labels() if labels is None: mask = None self._signal_too_many_labels( mask is not None and mask.sum() > self.MAX_VISIBLE_LABELS) if self._too_many_labels or mask is None or not np.any(mask): return black = pg.mkColor(0, 0, 0) labels = labels[mask] x = x[mask] y = y[mask] for label, xp, yp in zip(labels, x, y): ti = TextItem(label, black) ti.setPos(xp, yp) self.plot_widget.addItem(ti) self.labels.append(ti) def _signal_too_many_labels(self, too_many): if self._too_many_labels != too_many: self._too_many_labels = too_many self.too_many_labels.emit(too_many) def _label_mask(self, x, y): (x0, x1), (y0, y1) = self.view_box.viewRange() mask = np.logical_and( np.logical_and(x >= x0, x <= x1), np.logical_and(y >= y0, y <= y1)) if self.label_only_selected: sub_mask = self._filter_visible(self.master.get_subset_mask()) if self.selection is None: if sub_mask is None: return None else: sel_mask = sub_mask else: sel_mask = self._filter_visible(self.selection) != 0 if sub_mask is not None: sel_mask = np.logical_or(sel_mask, sub_mask) mask = np.logical_and(mask, sel_mask) return mask # Shapes def get_shapes(self): """ Prepare data for shapes of points in the plot The method is called by `update_shapes`. It gets the data from the widget's `get_shape_data`, and then calls its `impute_shapes` to impute the missing shape (usually with some default shape). Returns: (np.ndarray): an array of symbols (e.g. o, x, + ...) """ shape_data = self.master.get_shape_data() shape_data = self._filter_visible(shape_data) # Data has to be copied so the imputation can change it in-place # TODO: Try avoiding this when we move imputation to the widget if shape_data is not None: shape_data = np.copy(shape_data) shape_imputer = getattr( self.master, "impute_shapes", self.default_impute_shapes) shape_imputer(shape_data, len(self.CurveSymbols) - 1) if isinstance(shape_data, np.ndarray): shape_data = shape_data.astype(int) else: shape_data = np.zeros(self.n_shown, dtype=int) return self.CurveSymbols[shape_data] @staticmethod def default_impute_shapes(shape_data, default_symbol): """ Fallback imputation for shapes. Use the default symbol, usually the last symbol in the list. Returns: (bool): True if there was any missing data """ if shape_data is None: return False nans = np.isnan(shape_data) if np.any(nans): shape_data[nans] = default_symbol return True else: return False def update_shapes(self): """ Trigger an update of point symbols The method calls `get_shapes` to obtain an array with a symbol for each point and uses it to update the symbols. Finally, the method updates the legend. """ if self.scatterplot_item: shape_data = self.get_shapes() self.scatterplot_item.setSymbol(shape_data) self.update_legends() def update_grid_visibility(self): """Show or hide the grid""" self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid) def update_legend_visibility(self): """ Show or hide legends based on whether they are enabled and non-empty """ self.shape_legend.setVisible( self.show_legend and bool(self.shape_legend.items)) self.color_legend.setVisible( self.show_legend and bool(self.color_legend.items)) def update_legends(self): """Update content of legends and their visibility""" cont_color = self.master.is_continuous_color() shape_labels = self.master.get_shape_labels() color_labels = None if cont_color else self.master.get_color_labels() if shape_labels == color_labels and shape_labels is not None: self._update_combined_legend(shape_labels) else: self._update_shape_legend(shape_labels) if cont_color: self._update_continuous_color_legend() else: self._update_color_legend(color_labels) self.update_legend_visibility() def _update_shape_legend(self, labels): self.shape_legend.clear() if labels is None or self.scatterplot_item is None: return color = QColor(0, 0, 0) color.setAlpha(self.alpha_value) for label, symbol in zip(labels, self.CurveSymbols): self.shape_legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, symbol=symbol), escape(label)) def _update_continuous_color_legend(self): self.color_legend.clear() if self.scale is None or self.scatterplot_item is None: return label = PaletteItemSample(self.palette, self.scale) self.color_legend.addItem(label, "") self.color_legend.setGeometry(label.boundingRect()) def _update_color_legend(self, labels): self.color_legend.clear() if labels is None: return self._update_colored_legend(self.color_legend, labels, 'o') def _update_combined_legend(self, labels): # update_colored_legend will already clear the shape legend # so we remove colors here use_legend = \ self.shape_legend if self.shape_legend.items else self.color_legend self.color_legend.clear() self.shape_legend.clear() self._update_colored_legend(use_legend, labels, self.CurveSymbols) def _update_colored_legend(self, legend, labels, symbols): if self.scatterplot_item is None or not self.palette: return if isinstance(symbols, str): symbols = itertools.repeat(symbols, times=len(labels)) for i, (label, symbol) in enumerate(zip(labels, symbols)): color = QColor(*self.palette.getRGB(i)) pen = _make_pen(color.darker(self.DarkerValue), 1.5) color.setAlpha(255 if self.subset_is_shown else self.alpha_value) brush = QBrush(color) legend.addItem( ScatterPlotItem(pen=pen, brush=brush, size=10, symbol=symbol), escape(label)) def zoom_button_clicked(self): self.plot_widget.getViewBox().setMouseMode( self.plot_widget.getViewBox().RectMode) def pan_button_clicked(self): self.plot_widget.getViewBox().setMouseMode( self.plot_widget.getViewBox().PanMode) def select_button_clicked(self): self.plot_widget.getViewBox().setMouseMode( self.plot_widget.getViewBox().RectMode) def reset_button_clicked(self): self.plot_widget.getViewBox().autoRange() self.update_labels() def select_by_click(self, _, points): if self.scatterplot_item is not None: self.select(points) def select_by_rectangle(self, value_rect): if self.scatterplot_item is not None: x0, y0 = value_rect.topLeft().x(), value_rect.topLeft().y() x1, y1 = value_rect.bottomRight().x(), value_rect.bottomRight().y() x, y = self.master.get_coordinates_data() indices = np.flatnonzero( (x0 <= x) & (x <= x1) & (y0 <= y) & (y <= y1)) self.select_by_indices(indices.astype(int)) def unselect_all(self): if self.selection is not None: self.selection = None self.update_selection_colors() if self.label_only_selected: self.update_labels() self.master.selection_changed() def select(self, points): # noinspection PyArgumentList if self.scatterplot_item is None: return indices = [p.data() for p in points] self.select_by_indices(indices) def select_by_indices(self, indices): if self.selection is None: self.selection = np.zeros(self.n_valid, dtype=np.uint8) keys = QApplication.keyboardModifiers() if keys & Qt.AltModifier: self.selection_remove(indices) elif keys & Qt.ShiftModifier and keys & Qt.ControlModifier: self.selection_append(indices) elif keys & Qt.ShiftModifier: self.selection_new_group(indices) else: self.selection_select(indices) def selection_select(self, indices): self.selection = np.zeros(self.n_valid, dtype=np.uint8) self.selection[indices] = 1 self._update_after_selection() def selection_append(self, indices): self.selection[indices] = np.max(self.selection) self._update_after_selection() def selection_new_group(self, indices): self.selection[indices] = np.max(self.selection) + 1 self._update_after_selection() def selection_remove(self, indices): self.selection[indices] = 0 self._update_after_selection() def _update_after_selection(self): self._compress_indices() self.update_selection_colors() if self.label_only_selected: self.update_labels() self.master.selection_changed() def _compress_indices(self): indices = sorted(set(self.selection) | {0}) if len(indices) == max(indices) + 1: return mapping = np.zeros((max(indices) + 1,), dtype=int) for i, ind in enumerate(indices): mapping[ind] = i self.selection = mapping[self.selection] def get_selection(self): if self.selection is None: return np.array([], dtype=np.uint8) else: return np.flatnonzero(self.selection) def help_event(self, event): """ Create a `QToolTip` for the point hovered by the mouse """ if self.scatterplot_item is None: return False act_pos = self.scatterplot_item.mapFromScene(event.scenePos()) point_data = [p.data() for p in self.scatterplot_item.pointsAt(act_pos)] text = self.master.get_tooltip(point_data) if text: QToolTip.showText(event.screenPos(), text, widget=self.plot_widget) return True else: return False
def test_nodeitem(self): one_item = NodeItem() one_item.setWidgetDescription(self.one_desc) one_item.setWidgetCategory(self.const_desc) one_item.setTitle("Neo") self.assertEqual(one_item.title(), "Neo") one_item.setProcessingState(True) self.assertEqual(one_item.processingState(), True) one_item.setProgress(50) self.assertEqual(one_item.progress(), 50) one_item.setProgress(100) self.assertEqual(one_item.progress(), 100) one_item.setProgress(101) self.assertEqual(one_item.progress(), 100, "Progress overshots") one_item.setProcessingState(False) self.assertEqual(one_item.processingState(), False) self.assertEqual(one_item.progress(), -1, "setProcessingState does not clear the progress.") self.scene.addItem(one_item) one_item.setPos(100, 100) negate_item = NodeItem() negate_item.setWidgetDescription(self.negate_desc) negate_item.setWidgetCategory(self.const_desc) self.scene.addItem(negate_item) negate_item.setPos(300, 100) nb_item = NodeItem() nb_item.setWidgetDescription(self.add_desc) nb_item.setWidgetCategory(self.operator_desc) self.scene.addItem(nb_item) nb_item.setPos(500, 100) positions = [] anchor = one_item.newOutputAnchor() anchor.scenePositionChanged.connect(positions.append) one_item.setPos(110, 100) self.assertTrue(len(positions) > 0) one_item.setErrorMessage("message") one_item.setWarningMessage("message") one_item.setInfoMessage("I am alive") one_item.setErrorMessage(None) one_item.setWarningMessage(None) one_item.setInfoMessage(None) one_item.setInfoMessage("I am back.") nb_item.setProcessingState(1) negate_item.setProcessingState(1) negate_item.shapeItem.startSpinner() def progress(): p = (nb_item.progress() + 25) % 100 nb_item.setProgress(p) if p > 50: nb_item.setInfoMessage("Over 50%") one_item.setWarningMessage("Second") else: nb_item.setInfoMessage(None) one_item.setWarningMessage(None) negate_item.setAnchorRotation(50 - p) timer = QTimer(nb_item, interval=5) timer.start() timer.timeout.connect(progress) self.qWait() timer.stop()
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) features = Input("Features", AttributeList) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) features = Output("Features", AttributeList, dynamic=False) settings_version = 2 settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting(None) attr_y = ContextSetting(None) #: Serialized selection state to be restored selection_group = Setting(None, schema_only=True) graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QPen(self.palette().color(QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) #: Remember the saved state to restore self.__pending_selection_restore = self.selection_group self.selection_group = None common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) self.cb_attr_x = gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) g = self.graph.gui g.add_widgets([g.JitterSizeSlider, g.JitterNumericValues], box) self.sampling = gui.auto_commit( self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) g.point_properties_box(self.controlArea) self.models = [self.xy_model] + g.points_models box_plot_prop = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines, g.ToolTipShowsAll, g.ClassDensity, g.RegressionLine, g.LabelOnlySelected], box_plot_prop) self.graph.box_zoom_select(self.controlArea) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") self.graph.zoom_actions(self) def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() def _vizrank_color_change(self): self.vizrank.initialize() is_enabled = self.data is not None and not self.data.is_sparse() and \ len([v for v in chain(self.data.domain.variables, self.data.domain.metas) if v.is_primitive]) > 2\ and len(self.data) > 1 self.vizrank_button.setEnabled( is_enabled and self.graph.attr_color is not None and not np.isnan(self.data.get_column_view(self.graph.attr_color)[0].astype(float)).all()) if is_enabled and self.graph.attr_color is None: self.vizrank_button.setToolTip("Color variable has to be selected.") else: self.vizrank_button.setToolTip("") @Inputs.data def set_data(self, data): self.clear_messages() self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data if not same_domain: self.init_attr_values() self.openContext(self.data) self._vizrank_color_change() def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Orange.data.Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.graph.attr_label, str): self.graph.attr_label = findvar( self.graph.attr_label, self.graph.gui.label_model) if isinstance(self.graph.attr_color, str): self.graph.attr_color = findvar( self.graph.attr_color, self.graph.gui.color_model) if isinstance(self.graph.attr_shape, str): self.graph.attr_shape = findvar( self.graph.attr_shape, self.graph.gui.shape_model) if isinstance(self.graph.attr_size, str): self.graph.attr_size = findvar( self.graph.attr_size, self.graph.gui.size_model) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() @Inputs.data_subset def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = subset_data self.controls.graph.alpha_value.setEnabled(subset_data is None) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data, self.subset_data) if self.attribute_selection_list and self.graph.domain and \ all(attr in self.graph.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) if self.data is not None and self.__pending_selection_restore is not None: self.apply_selection(self.__pending_selection_restore) self.__pending_selection_restore = None self.unconditional_commit() def apply_selection(self, selection): """Apply `selection` to the current plot.""" if self.data is not None: self.graph.selection = np.zeros(len(self.data), dtype=np.uint8) self.selection_group = [x for x in selection if x[0] < len(self.data)] selection_array = np.array(self.selection_group).T self.graph.selection[selection_array[0]] = selection_array[1] self.graph.update_colors(keep_colors=True) @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x self.graph.attr_color = self.data.domain.class_var if domain else None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.send_features() def update_colors(self): self._vizrank_color_change() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): # Store current selection in a setting that is stored in workflow if isinstance(self.data, SqlTable): selection = None elif self.data is not None: selection = self.graph.get_selection() else: selection = None if selection is not None and len(selection): self.selection_group = list(zip(selection, self.graph.selection[selection])) else: self.selection_group = None self.commit() def send_data(self): # TODO: Implement selection for sql data def _get_selected(): if not len(selection): return None return create_groups_table(data, graph.selection, False, "Group") def _get_annotated(): if graph.selection is not None and np.max(graph.selection) > 1: return create_groups_table(data, graph.selection) else: return create_annotated_table(data, selection) graph = self.graph data = self.data selection = graph.get_selection() self.Outputs.annotated_data.send(_get_annotated()) self.Outputs.selected_data.send(_get_selected()) def send_features(self): features = [attr for attr in [self.attr_x, self.attr_y] if attr] self.Outputs.features.send(features or None) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert(( ("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]]
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(int(minTime * 1000)).toUTC() to_dt = QDateTime.fromMSecsSinceEpoch(int(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(int(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 = fromtimestamp(self.slider.scale(value)) newTime = add_time(time, step_amount, -1 if backward else 1) return self.slider.unscale(timestamp(newTime)) 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 = fromtimestamp(round(time_values.min())) max_dt = fromtimestamp(round(time_values.max())) # 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(int(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