def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: downPos = event.buttonDownPos(Qt.LeftButton) if not self.__tmpLine and self.__dragStartItem and \ (downPos - event.pos()).manhattanLength() > \ QApplication.instance().startDragDistance(): # Start a line drag line = QGraphicsLineItem(self) start = self.__dragStartItem.boundingRect().center() start = self.mapFromItem(self.__dragStartItem, start) line.setLine(start.x(), start.y(), event.pos().x(), event.pos().y()) pen = QPen(self.palette().color(QPalette.Foreground), 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) line.show() self.__tmpLine = line if self.__tmpLine: # Update the temp line line = self.__tmpLine.line() line.setP2(event.pos()) self.__tmpLine.setLine(line) QGraphicsWidget.mouseMoveEvent(self, event)
def __init__(self, parent=None, root=None, orientation=Left, hoverHighlightEnabled=True, selectionMode=ExtendedSelection, **kwargs): super().__init__(None, **kwargs) # Filter all events from children (`ClusterGraphicsItem`s) self.setFiltersChildEvents(True) self.orientation = orientation self._root = None #: A tree with dendrogram geometry self._layout = None self._highlighted_item = None #: a list of selected items self._selection = OrderedDict() #: a {node: item} mapping self._items = { } # type: Dict[Tree, DendrogramWidget.ClusterGraphicsItem] #: container for all cluster items. self._itemgroup = QGraphicsWidget(self) self._itemgroup.setGeometry(self.contentsRect()) #: Transform mapping from 'dendrogram' to widget local coordinate #: system self._transform = QTransform() self._cluster_parent = {} self.__hoverHighlightEnabled = hoverHighlightEnabled self.__selectionMode = selectionMode self.setContentsMargins(0, 0, 0, 0) self.setRoot(root) if parent is not None: self.setParentItem(parent)
def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton and self.__tmpLine: endItem = find_item_at(self.scene(), event.scenePos(), type=ChannelAnchor) if endItem is not None: startItem = self.__dragStartItem startChannel = startItem.channel() endChannel = endItem.channel() possible = False # Make sure the drag was from input to output (or reversed) and # not between input -> input or output -> output if type(startChannel) != type(endChannel): if isinstance(startChannel, InputSignal): startChannel, endChannel = endChannel, startChannel possible = compatible_channels(startChannel, endChannel) if possible: self.addLink(startChannel, endChannel) self.scene().removeItem(self.__tmpLine) self.__tmpLine = None self.__dragStartItem = None QGraphicsWidget.mouseReleaseEvent(self, event)
def clear(self): """ Clear the widget. """ scene = self.scene() if scene is not None: scene.removeItem(self._itemgroup) else: self._itemgroup.setParentItem(None) self._itemgroup = QGraphicsWidget(self) self._itemgroup.setGeometry(self.contentsRect()) self._items.clear() for item in self._selection.values(): if scene is not None: scene.removeItem(item) else: item.setParentItem(None) self._root = None self._items = {} self._selection = OrderedDict() self._highlighted_item = None self._cluster_parent = {} self.updateGeometry()
def mousePressEvent(self, event): if event.button() == Qt.LeftButton: startItem = find_item_at(self.scene(), event.pos(), type=ChannelAnchor) if startItem is not None and startItem.isEnabled(): # Start a connection line drag. self.__dragStartItem = startItem self.__tmpLine = None event.accept() return lineItem = find_item_at(self.scene(), event.scenePos(), type=QGraphicsLineItem) if lineItem is not None: # Remove a connection under the mouse for link in self.__links: if link.lineItem == lineItem: self.removeLink(link.output, link.input) event.accept() return QGraphicsWidget.mousePressEvent(self, event)
def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: downPos = event.buttonDownPos(Qt.LeftButton) if not self.__tmpLine and self.__dragStartItem and \ (downPos - event.pos()).manhattanLength() > \ QApplication.instance().startDragDistance(): # Start a line drag line = QGraphicsLineItem(self) start = self.__dragStartItem.boundingRect().center() start = self.mapFromItem(self.__dragStartItem, start) line.setLine(start.x(), start.y(), event.pos().x(), event.pos().y()) pen = QPen(Qt.black, 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) line.show() self.__tmpLine = line if self.__tmpLine: # Update the temp line line = self.__tmpLine.line() line.setP2(event.pos()) self.__tmpLine.setLine(line) QGraphicsWidget.mouseMoveEvent(self, event)
def main(argv=None): # pragma: no cover # pylint: disable=import-outside-toplevel import sys from AnyQt.QtWidgets import QGraphicsScene, QMenu from AnyQt.QtGui import QBrush app = QApplication(argv or sys.argv) scene = QGraphicsScene() view = GraphicsWidgetView(scene) scene.setParent(view) view.setContextMenuPolicy(Qt.CustomContextMenu) def context(pos): menu = QMenu(view) menu.addActions(view.actions()) a = menu.addAction("Aspect mode") am = QMenu(menu) am.addAction("Ignore", lambda: view.setAspectMode(Qt.IgnoreAspectRatio)) am.addAction("Keep", lambda: view.setAspectMode(Qt.KeepAspectRatio)) am.addAction("Keep by expanding", lambda: view.setAspectMode(Qt.KeepAspectRatioByExpanding)) a.setMenu(am) menu.popup(view.viewport().mapToGlobal(pos)) view.customContextMenuRequested.connect(context) w = QGraphicsWidget() w.setPreferredSize(500, 500) palette = w.palette() palette.setBrush(palette.Window, QBrush(Qt.red, Qt.BDiagPattern)) w.setPalette(palette) w.setAutoFillBackground(True) scene.addItem(w) view.setCentralWidget(w) view.show() return app.exec()
def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: downPos = event.buttonDownPos(Qt.LeftButton) if not self.__tmpLine and self.__dragStartItem and \ (downPos - event.pos()).manhattanLength() > \ QApplication.instance().startDragDistance(): # Start a line drag line = LinkLineItem(self) start = self.__dragStartItem.boundingRect().center() start = self.mapFromItem(self.__dragStartItem, start) eventPos = event.pos() line.setLine(start.x(), start.y(), eventPos.x(), eventPos.y()) pen = QPen(self.palette().color(QPalette.Foreground), 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) line.show() self.__tmpLine = line if self.__dragStartItem in self.sourceNodeWidget.channelAnchors: for anchor in self.sinkNodeWidget.channelAnchors: self.__updateAnchorState(anchor, [self.__dragStartItem]) else: for anchor in self.sourceNodeWidget.channelAnchors: self.__updateAnchorState(anchor, [self.__dragStartItem]) if self.__tmpLine: # Update the temp line line = self.__tmpLine.line() maybe_anchor = find_item_at(self.scene(), event.scenePos(), type=ChannelAnchor) # If hovering over anchor if maybe_anchor is not None and maybe_anchor.isEnabled(): target_pos = maybe_anchor.boundingRect().center() target_pos = self.mapFromItem(maybe_anchor, target_pos) line.setP2(target_pos) else: target_pos = event.pos() line.setP2(target_pos) self.__tmpLine.setLine(line) QGraphicsWidget.mouseMoveEvent(self, event)
def __init__( self, parent=None, direction=Qt.LeftToRight, node=None, icon=None, iconSize=None, **args ): QGraphicsWidget.__init__(self, parent, **args) self.setAcceptedMouseButtons(Qt.NoButton) self.__direction = direction self.setLayout(QGraphicsLinearLayout(Qt.Horizontal)) # Set the maximum size, otherwise the layout can't grow beyond its # sizeHint (and we need it to grow so the widget can grow and keep the # contents centered vertically. self.layout().setMaximumSize(QSizeF(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.__iconSize = iconSize or QSize(64, 64) self.__icon = icon self.__iconItem = QGraphicsPixmapItem(self) self.__iconLayoutItem = GraphicsItemLayoutItem(item=self.__iconItem) self.__channelLayout = QGraphicsGridLayout() self.__channelAnchors = [] if self.__direction == Qt.LeftToRight: self.layout().addItem(self.__iconLayoutItem) self.layout().addItem(self.__channelLayout) channel_alignemnt = Qt.AlignRight else: self.layout().addItem(self.__channelLayout) self.layout().addItem(self.__iconLayoutItem) channel_alignemnt = Qt.AlignLeft self.layout().setAlignment(self.__iconLayoutItem, Qt.AlignCenter) self.layout().setAlignment( self.__channelLayout, Qt.AlignVCenter | channel_alignemnt ) if node is not None: self.setSchemeNode(node)
def __init__(self, parent=None, direction=Qt.LeftToRight, node=None, icon=None, iconSize=None, **args): QGraphicsWidget.__init__(self, parent, **args) self.setAcceptedMouseButtons(Qt.NoButton) self.__direction = direction self.setLayout(QGraphicsLinearLayout(Qt.Horizontal)) # Set the maximum size, otherwise the layout can't grow beyond its # sizeHint (and we need it to grow so the widget can grow and keep the # contents centered vertically. self.layout().setMaximumSize(QSizeF(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.__iconSize = iconSize or QSize(64, 64) self.__icon = icon self.__iconItem = QGraphicsPixmapItem(self) self.__iconLayoutItem = GraphicsItemLayoutItem(item=self.__iconItem) self.__channelLayout = QGraphicsGridLayout() self.__channelAnchors = [] if self.__direction == Qt.LeftToRight: self.layout().addItem(self.__iconLayoutItem) self.layout().addItem(self.__channelLayout) channel_alignemnt = Qt.AlignRight else: self.layout().addItem(self.__channelLayout) self.layout().addItem(self.__iconLayoutItem) channel_alignemnt = Qt.AlignLeft self.layout().setAlignment(self.__iconLayoutItem, Qt.AlignCenter) self.layout().setAlignment(self.__channelLayout, Qt.AlignVCenter | channel_alignemnt) if node is not None: self.setSchemeNode(node)
def __init__(self, *args, **kwargs): QGraphicsWidget.__init__(self, *args, **kwargs) self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton) self.source = None self.sink = None # QGraphicsWidget/Items in the scene. self.sourceNodeWidget = None self.sourceNodeTitle = None self.sinkNodeWidget = None self.sinkNodeTitle = None self.__links = [] self.__textItems = [] self.__iconItems = [] self.__tmpLine = None self.__dragStartItem = None self.setLayout(QGraphicsLinearLayout(Qt.Vertical)) self.layout().setContentsMargins(0, 0, 0, 0)
def mousePressEvent(self, event): if event.button() == Qt.LeftButton: startItem = find_item_at(self.scene(), event.pos(), type=ChannelAnchor) if startItem is not None: # Start a connection line drag. self.__dragStartItem = startItem self.__tmpLine = None event.accept() return lineItem = find_item_at(self.scene(), event.scenePos(), type=QGraphicsLineItem) if lineItem is not None: # Remove a connection under the mouse for link in self.__links: if link.lineItem == lineItem: self.removeLink(link.output, link.input) event.accept() return QGraphicsWidget.mousePressEvent(self, event)
def sizeHint(self, which, constraint=QSizeF()): if which == Qt.PreferredSize: doc = self.document() textwidth = doc.textWidth() if textwidth != constraint.width(): cloned = doc.clone(self) cloned.setTextWidth(constraint.width()) sh = cloned.size() cloned.deleteLater() else: sh = doc.size() return sh else: return QGraphicsWidget.sizeHint(self, which, constraint)
def resizeEvent(self, event): width = event.newSize().width() left, _, right, _ = self.textMargins() self.__textItem.setTextWidth(max(width - left - right, 0)) self.__updateFrame() QGraphicsWidget.resizeEvent(self, event)
def __updateState(self): """ Update the widget with the new source/sink node signal descriptions. """ widget = QGraphicsWidget() widget.setLayout(QGraphicsGridLayout()) # Space between left and right anchors widget.layout().setHorizontalSpacing(50) left_node = EditLinksNode(self, direction=Qt.LeftToRight, node=self.source) left_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) right_node = EditLinksNode(self, direction=Qt.RightToLeft, node=self.sink) right_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) left_node.setMinimumWidth(150) right_node.setMinimumWidth(150) widget.layout().addItem(left_node, 0, 0,) widget.layout().addItem(right_node, 0, 1,) title_template = "<center><b>{0}<b></center>" left_title = GraphicsTextWidget(self) left_title.setHtml(title_template.format(escape(self.source.title))) left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) right_title = GraphicsTextWidget(self) right_title.setHtml(title_template.format(escape(self.sink.title))) right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) widget.layout().addItem(left_title, 1, 0, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.layout().addItem(right_title, 1, 1, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.setParentItem(self) max_w = max(left_node.sizeHint(Qt.PreferredSize).width(), right_node.sizeHint(Qt.PreferredSize).width()) # fix same size left_node.setMinimumWidth(max_w) right_node.setMinimumWidth(max_w) left_title.setMinimumWidth(max_w) right_title.setMinimumWidth(max_w) self.layout().addItem(widget) self.layout().activate() self.sourceNodeWidget = left_node self.sinkNodeWidget = right_node self.sourceNodeTitle = left_title self.sinkNodeTitle = right_title self.__resetAnchorStates() # AnchorHover hover over anchor before hovering over line class AnchorHover(QGraphicsRectItem): def __init__(self, anchor, parent=None): super().__init__(parent=parent) self.setAcceptHoverEvents(True) self.anchor = anchor self.setRect(anchor.boundingRect()) self.setPos(self.mapFromScene(anchor.scenePos())) self.setFlag(QGraphicsItem.ItemHasNoContents, True) def hoverEnterEvent(self, event): if self.anchor.isEnabled(): self.anchor.hoverEnterEvent(event) else: event.ignore() def hoverLeaveEvent(self, event): if self.anchor.isEnabled(): self.anchor.hoverLeaveEvent(event) else: event.ignore() for anchor in left_node.channelAnchors + right_node.channelAnchors: anchor_hover = AnchorHover(anchor, parent=self) anchor_hover.setZValue(2.0)
def setGeometry(self, rect): QGraphicsWidget.setGeometry(self, rect) self.geometryChanged.emit()
def setGeometry(self, rect): QGraphicsWidget.setGeometry(self, rect)
def __init__(self, parent=None, **kwargs): QGraphicsWidget.__init__(self, parent, **kwargs)
def __init__(self): super().__init__() self.matrix = None self.items = None self.linkmatrix = None self.root = None self._displayed_root = None self.cutoff_height = 0.0 gui.comboBox(self.controlArea, self, "linkage", items=LINKAGE, box="Linkage", callback=self._invalidate_clustering) model = itemmodels.VariableListModel() model[:] = self.basic_annotations box = gui.widgetBox(self.controlArea, "Annotations") self.label_cb = cb = combobox.ComboBoxSearch( minimumContentsLength=14, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) cb.setModel(model) cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole)) def on_annotation_activated(): self.annotation = cb.currentData(Qt.EditRole) self._update_labels() cb.activated.connect(on_annotation_activated) def on_annotation_changed(value): cb.setCurrentIndex(cb.findData(value, Qt.EditRole)) self.connect_control("annotation", on_annotation_changed) box.layout().addWidget(self.label_cb) box = gui.radioButtons(self.controlArea, self, "pruning", box="Pruning", callback=self._invalidate_pruning) grid = QGridLayout() box.layout().addLayout(grid) grid.addWidget(gui.appendRadioButton(box, "None", addToLayout=False), 0, 0) self.max_depth_spin = gui.spin(box, self, "max_depth", minv=1, maxv=100, callback=self._invalidate_pruning, keyboardTracking=False, addToLayout=False) grid.addWidget( gui.appendRadioButton(box, "Max depth:", addToLayout=False), 1, 0) grid.addWidget(self.max_depth_spin, 1, 1) self.selection_box = gui.radioButtons( self.controlArea, self, "selection_method", box="Selection", callback=self._selection_method_changed) grid = QGridLayout() self.selection_box.layout().addLayout(grid) grid.addWidget( gui.appendRadioButton(self.selection_box, "Manual", addToLayout=False), 0, 0) grid.addWidget( gui.appendRadioButton(self.selection_box, "Height ratio:", addToLayout=False), 1, 0) self.cut_ratio_spin = gui.spin(self.selection_box, self, "cut_ratio", 0, 100, step=1e-1, spinType=float, callback=self._selection_method_changed, addToLayout=False) self.cut_ratio_spin.setSuffix("%") grid.addWidget(self.cut_ratio_spin, 1, 1) grid.addWidget( gui.appendRadioButton(self.selection_box, "Top N:", addToLayout=False), 2, 0) self.top_n_spin = gui.spin(self.selection_box, self, "top_n", 1, 20, callback=self._selection_method_changed, addToLayout=False) grid.addWidget(self.top_n_spin, 2, 1) self.zoom_slider = gui.hSlider(self.controlArea, self, "zoom_factor", box="Zoom", minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False, callback=self.__update_font_scale) zoom_in = QAction("Zoom in", self, shortcut=QKeySequence.ZoomIn, triggered=self.__zoom_in) zoom_out = QAction("Zoom out", self, shortcut=QKeySequence.ZoomOut, triggered=self.__zoom_out) zoom_reset = QAction("Reset zoom", self, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=self.__zoom_reset) self.addActions([zoom_in, zoom_out, zoom_reset]) self.controlArea.layout().addStretch() gui.auto_send(self.buttonsArea, self, "autocommit") self.scene = QGraphicsScene(self) self.view = StickyGraphicsView( self.scene, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn, alignment=Qt.AlignLeft | Qt.AlignVCenter) self.mainArea.layout().setSpacing(1) self.mainArea.layout().addWidget(self.view) def axis_view(orientation): ax = AxisItem(orientation=orientation, maxTickLength=7) ax.mousePressed.connect(self._activate_cut_line) ax.mouseMoved.connect(self._activate_cut_line) ax.mouseReleased.connect(self._activate_cut_line) ax.setRange(1.0, 0.0) return ax self.top_axis = axis_view("top") self.bottom_axis = axis_view("bottom") self._main_graphics = QGraphicsWidget() scenelayout = QGraphicsGridLayout() scenelayout.setHorizontalSpacing(10) scenelayout.setVerticalSpacing(10) self._main_graphics.setLayout(scenelayout) self.scene.addItem(self._main_graphics) self.dendrogram = DendrogramWidget() self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.dendrogram.selectionChanged.connect(self._invalidate_output) self.dendrogram.selectionEdited.connect(self._selection_edited) self.labels = TextListWidget() self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.labels.setAlignment(Qt.AlignLeft) self.labels.setMaximumWidth(200) scenelayout.addItem(self.top_axis, 0, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) scenelayout.addItem(self.dendrogram, 1, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) scenelayout.addItem(self.labels, 1, 1, alignment=Qt.AlignLeft | Qt.AlignVCenter) scenelayout.addItem(self.bottom_axis, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) self.view.viewport().installEventFilter(self) self._main_graphics.installEventFilter(self) self.top_axis.setZValue(self.dendrogram.zValue() + 10) self.bottom_axis.setZValue(self.dendrogram.zValue() + 10) self.cut_line = SliderLine(self.top_axis, orientation=Qt.Horizontal) self.cut_line.valueChanged.connect(self._dendrogram_slider_changed) self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed) self._set_cut_line_visible(self.selection_method == 1) self.__update_font_scale()
def __updateState(self): """ Update the widget with the new source/sink node signal descriptions. """ widget = QGraphicsWidget() widget.setLayout(QGraphicsGridLayout()) # Space between left and right anchors widget.layout().setHorizontalSpacing(50) left_node = EditLinksNode(self, direction=Qt.LeftToRight, node=self.source) left_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) right_node = EditLinksNode(self, direction=Qt.RightToLeft, node=self.sink) right_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) left_node.setMinimumWidth(150) right_node.setMinimumWidth(150) widget.layout().addItem(left_node, 0, 0,) widget.layout().addItem(right_node, 0, 1,) title_template = "<center><b>{0}<b></center>" left_title = GraphicsTextWidget(self) left_title.setHtml(title_template.format(escape(self.source.title))) left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) right_title = GraphicsTextWidget(self) right_title.setHtml(title_template.format(escape(self.sink.title))) right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) widget.layout().addItem(left_title, 1, 0, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.layout().addItem(right_title, 1, 1, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.setParentItem(self) max_w = max(left_node.sizeHint(Qt.PreferredSize).width(), right_node.sizeHint(Qt.PreferredSize).width()) # fix same size left_node.setMinimumWidth(max_w) right_node.setMinimumWidth(max_w) left_title.setMinimumWidth(max_w) right_title.setMinimumWidth(max_w) self.layout().addItem(widget) self.layout().activate() self.sourceNodeWidget = left_node self.sinkNodeWidget = right_node self.sourceNodeTitle = left_title self.sinkNodeTitle = right_title
class OWHierarchicalClustering(widget.OWWidget): name = "Hierarchical Clustering" description = "Display a dendrogram of a hierarchical clustering " \ "constructed from the input distance matrix." icon = "icons/HierarchicalClustering.svg" priority = 2100 keywords = [] class Inputs: distances = Input("Distances", Orange.misc.DistMatrix) class Outputs: selected_data = Output("Selected Data", Orange.data.Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table) settingsHandler = _DomainContextHandler() #: Selected linkage linkage = settings.Setting(1) #: Index of the selected annotation item (variable, ...) annotation = settings.ContextSetting("Enumeration") #: Out-of-context setting for the case when the "Name" option is available annotation_if_names = settings.Setting("Name") #: Out-of-context setting for the case with just "Enumerate" and "None" annotation_if_enumerate = settings.Setting("Enumerate") #: Selected tree pruning (none/max depth) pruning = settings.Setting(0) #: Maximum depth when max depth pruning is selected max_depth = settings.Setting(10) #: Selected cluster selection method (none, cut distance, top n) selection_method = settings.Setting(0) #: Cut height ratio wrt root height cut_ratio = settings.Setting(75.0) #: Number of top clusters to select top_n = settings.Setting(3) #: Dendrogram zoom factor zoom_factor = settings.Setting(0) autocommit = settings.Setting(True) graph_name = "scene" basic_annotations = ["None", "Enumeration"] class Error(widget.OWWidget.Error): not_finite_distances = Msg("Some distances are infinite") #: Stored (manual) selection state (from a saved workflow) to restore. __pending_selection_restore = None # type: Optional[SelectionState] def __init__(self): super().__init__() self.matrix = None self.items = None self.linkmatrix = None self.root = None self._displayed_root = None self.cutoff_height = 0.0 gui.comboBox(self.controlArea, self, "linkage", items=LINKAGE, box="Linkage", callback=self._invalidate_clustering) model = itemmodels.VariableListModel() model[:] = self.basic_annotations box = gui.widgetBox(self.controlArea, "Annotations") self.label_cb = cb = combobox.ComboBoxSearch( minimumContentsLength=14, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) cb.setModel(model) cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole)) def on_annotation_activated(): self.annotation = cb.currentData(Qt.EditRole) self._update_labels() cb.activated.connect(on_annotation_activated) def on_annotation_changed(value): cb.setCurrentIndex(cb.findData(value, Qt.EditRole)) self.connect_control("annotation", on_annotation_changed) box.layout().addWidget(self.label_cb) box = gui.radioButtons(self.controlArea, self, "pruning", box="Pruning", callback=self._invalidate_pruning) grid = QGridLayout() box.layout().addLayout(grid) grid.addWidget(gui.appendRadioButton(box, "None", addToLayout=False), 0, 0) self.max_depth_spin = gui.spin(box, self, "max_depth", minv=1, maxv=100, callback=self._invalidate_pruning, keyboardTracking=False, addToLayout=False) grid.addWidget( gui.appendRadioButton(box, "Max depth:", addToLayout=False), 1, 0) grid.addWidget(self.max_depth_spin, 1, 1) self.selection_box = gui.radioButtons( self.controlArea, self, "selection_method", box="Selection", callback=self._selection_method_changed) grid = QGridLayout() self.selection_box.layout().addLayout(grid) grid.addWidget( gui.appendRadioButton(self.selection_box, "Manual", addToLayout=False), 0, 0) grid.addWidget( gui.appendRadioButton(self.selection_box, "Height ratio:", addToLayout=False), 1, 0) self.cut_ratio_spin = gui.spin(self.selection_box, self, "cut_ratio", 0, 100, step=1e-1, spinType=float, callback=self._selection_method_changed, addToLayout=False) self.cut_ratio_spin.setSuffix("%") grid.addWidget(self.cut_ratio_spin, 1, 1) grid.addWidget( gui.appendRadioButton(self.selection_box, "Top N:", addToLayout=False), 2, 0) self.top_n_spin = gui.spin(self.selection_box, self, "top_n", 1, 20, callback=self._selection_method_changed, addToLayout=False) grid.addWidget(self.top_n_spin, 2, 1) self.zoom_slider = gui.hSlider(self.controlArea, self, "zoom_factor", box="Zoom", minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False, callback=self.__update_font_scale) zoom_in = QAction("Zoom in", self, shortcut=QKeySequence.ZoomIn, triggered=self.__zoom_in) zoom_out = QAction("Zoom out", self, shortcut=QKeySequence.ZoomOut, triggered=self.__zoom_out) zoom_reset = QAction("Reset zoom", self, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=self.__zoom_reset) self.addActions([zoom_in, zoom_out, zoom_reset]) self.controlArea.layout().addStretch() gui.auto_send(self.buttonsArea, self, "autocommit") self.scene = QGraphicsScene(self) self.view = StickyGraphicsView( self.scene, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn, alignment=Qt.AlignLeft | Qt.AlignVCenter) self.mainArea.layout().setSpacing(1) self.mainArea.layout().addWidget(self.view) def axis_view(orientation): ax = AxisItem(orientation=orientation, maxTickLength=7) ax.mousePressed.connect(self._activate_cut_line) ax.mouseMoved.connect(self._activate_cut_line) ax.mouseReleased.connect(self._activate_cut_line) ax.setRange(1.0, 0.0) return ax self.top_axis = axis_view("top") self.bottom_axis = axis_view("bottom") self._main_graphics = QGraphicsWidget() scenelayout = QGraphicsGridLayout() scenelayout.setHorizontalSpacing(10) scenelayout.setVerticalSpacing(10) self._main_graphics.setLayout(scenelayout) self.scene.addItem(self._main_graphics) self.dendrogram = DendrogramWidget() self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.dendrogram.selectionChanged.connect(self._invalidate_output) self.dendrogram.selectionEdited.connect(self._selection_edited) self.labels = TextListWidget() self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.labels.setAlignment(Qt.AlignLeft) self.labels.setMaximumWidth(200) scenelayout.addItem(self.top_axis, 0, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) scenelayout.addItem(self.dendrogram, 1, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) scenelayout.addItem(self.labels, 1, 1, alignment=Qt.AlignLeft | Qt.AlignVCenter) scenelayout.addItem(self.bottom_axis, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) self.view.viewport().installEventFilter(self) self._main_graphics.installEventFilter(self) self.top_axis.setZValue(self.dendrogram.zValue() + 10) self.bottom_axis.setZValue(self.dendrogram.zValue() + 10) self.cut_line = SliderLine(self.top_axis, orientation=Qt.Horizontal) self.cut_line.valueChanged.connect(self._dendrogram_slider_changed) self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed) self._set_cut_line_visible(self.selection_method == 1) self.__update_font_scale() @Inputs.distances def set_distances(self, matrix): if self.__pending_selection_restore is not None: selection_state = self.__pending_selection_restore else: # save the current selection to (possibly) restore later selection_state = self._save_selection() self.error() self.Error.clear() if matrix is not None: N, _ = matrix.shape if N < 2: self.error("Empty distance matrix") matrix = None if matrix is not None: if not np.all(np.isfinite(matrix)): self.Error.not_finite_distances() matrix = None self.matrix = matrix if matrix is not None: self._set_items(matrix.row_items, matrix.axis) else: self._set_items(None) self._invalidate_clustering() # Can now attempt to restore session state from a saved workflow. if self.root and selection_state is not None: self._restore_selection(selection_state) self.__pending_selection_restore = None self.commit.now() def _set_items(self, items, axis=1): self.closeContext() self.items = items model = self.label_cb.model() if len(model) == 3: self.annotation_if_names = self.annotation elif len(model) == 2: self.annotation_if_enumerate = self.annotation if isinstance(items, Orange.data.Table) and axis: model[:] = chain( self.basic_annotations, [model.Separator], items.domain.class_vars, items.domain.metas, [model.Separator] if (items.domain.class_vars or items.domain.metas) and next( filter_visible(items.domain.attributes), False) else [], filter_visible(items.domain.attributes)) if items.domain.class_vars: self.annotation = items.domain.class_vars[0] else: self.annotation = "Enumeration" self.openContext(items.domain) else: name_option = bool( items is not None and (not axis or isinstance(items, list) and all( isinstance(var, Orange.data.Variable) for var in items))) model[:] = self.basic_annotations + ["Name"] * name_option self.annotation = self.annotation_if_names if name_option \ else self.annotation_if_enumerate def _clear_plot(self): self.dendrogram.set_root(None) self.labels.setItems([]) def _set_displayed_root(self, root): self._clear_plot() self._displayed_root = root self.dendrogram.set_root(root) self._update_labels() self._main_graphics.resize( self._main_graphics.size().width(), self._main_graphics.sizeHint(Qt.PreferredSize).height()) self._main_graphics.layout().activate() def _update(self): self._clear_plot() distances = self.matrix if distances is not None: method = LINKAGE[self.linkage].lower() Z = dist_matrix_linkage(distances, linkage=method) tree = tree_from_linkage(Z) self.linkmatrix = Z self.root = tree self.top_axis.setRange(tree.value.height, 0.0) self.bottom_axis.setRange(tree.value.height, 0.0) if self.pruning: self._set_displayed_root(prune(tree, level=self.max_depth)) else: self._set_displayed_root(tree) else: self.linkmatrix = None self.root = None self._set_displayed_root(None) self._apply_selection() def _update_labels(self): labels = [] if self.root and self._displayed_root: indices = [leaf.value.index for leaf in leaves(self.root)] if self.annotation == "None": labels = [] elif self.annotation == "Enumeration": labels = [str(i + 1) for i in indices] elif self.annotation == "Name": attr = self.matrix.row_items.domain.attributes labels = [str(attr[i]) for i in indices] elif isinstance(self.annotation, Orange.data.Variable): col_data, _ = self.items.get_column_view(self.annotation) labels = [self.annotation.str_val(val) for val in col_data] labels = [labels[idx] for idx in indices] else: labels = [] if labels and self._displayed_root is not self.root: joined = leaves(self._displayed_root) labels = [ ", ".join(labels[leaf.value.first:leaf.value.last]) for leaf in joined ] self.labels.setItems(labels) self.labels.setMinimumWidth(1 if labels else -1) def _restore_selection(self, state): # type: (SelectionState) -> bool """ Restore the (manual) node selection state. Return True if successful; False otherwise. """ linkmatrix = self.linkmatrix if self.selection_method == 0 and self.root: selected, linksaved = state linkstruct = np.array(linksaved, dtype=float) selected = set(selected) # type: Set[Tuple[int]] if not selected: return False if linkmatrix.shape[0] != linkstruct.shape[0]: return False # check that the linkage matrix structure matches. Use isclose for # the height column to account for inexact floating point math # (e.g. summation order in different ?gemm implementations for # euclidean distances, ...) if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \ not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])): return False selection = [] indices = np.array([n.value.index for n in leaves(self.root)], dtype=int) # mapping from ranges to display (pruned) nodes mapping = { node.value.range: node for node in postorder(self._displayed_root) } for node in postorder(self.root): # type: Tree r = tuple(indices[node.value.first:node.value.last]) if r in selected: if node.value.range not in mapping: # the node was pruned from display and cannot be # selected break selection.append(mapping[node.value.range]) selected.remove(r) if not selected: break # found all, nothing more to do if selection and selected: # Could not restore all selected nodes (only partial match) return False self._set_selected_nodes(selection) return True return False def _set_selected_nodes(self, selection): # type: (List[Tree]) -> None """ Set the nodes in `selection` to be the current selected nodes. The selection nodes must be subtrees of the current `_displayed_root`. """ self.dendrogram.selectionChanged.disconnect(self._invalidate_output) try: self.dendrogram.set_selected_clusters(selection) finally: self.dendrogram.selectionChanged.connect(self._invalidate_output) def _invalidate_clustering(self): self._update() self._update_labels() self._invalidate_output() def _invalidate_output(self): self.commit.deferred() def _invalidate_pruning(self): if self.root: selection = self.dendrogram.selected_nodes() ranges = [node.value.range for node in selection] if self.pruning: self._set_displayed_root(prune(self.root, level=self.max_depth)) else: self._set_displayed_root(self.root) selected = [ node for node in preorder(self._displayed_root) if node.value.range in ranges ] self.dendrogram.set_selected_clusters(selected) self._apply_selection() @gui.deferred def commit(self): items = getattr(self.matrix, "items", self.items) if not items: self.Outputs.selected_data.send(None) self.Outputs.annotated_data.send(None) return selection = self.dendrogram.selected_nodes() selection = sorted(selection, key=lambda c: c.value.first) indices = [leaf.value.index for leaf in leaves(self.root)] maps = [ indices[node.value.first:node.value.last] for node in selection ] selected_indices = list(chain(*maps)) unselected_indices = sorted( set(range(self.root.value.last)) - set(selected_indices)) if not selected_indices: self.Outputs.selected_data.send(None) annotated_data = create_annotated_table(items, []) \ if self.selection_method == 0 and self.matrix.axis else None self.Outputs.annotated_data.send(annotated_data) return selected_data = None if isinstance(items, Orange.data.Table) and self.matrix.axis == 1: # Select rows c = np.zeros(self.matrix.shape[0]) for i, indices in enumerate(maps): c[indices] = i c[unselected_indices] = len(maps) mask = c != len(maps) data, domain = items, items.domain attrs = domain.attributes classes = domain.class_vars metas = domain.metas var_name = get_unique_names(domain, "Cluster") values = [f"C{i + 1}" for i in range(len(maps))] clust_var = Orange.data.DiscreteVariable(var_name, values=values + ["Other"]) domain = Orange.data.Domain(attrs, classes, metas + (clust_var, )) data = items.transform(domain) with data.unlocked(data.metas): data.get_column_view(clust_var)[0][:] = c if selected_indices: selected_data = data[mask] clust_var = Orange.data.DiscreteVariable(var_name, values=values) selected_data.domain = Domain(attrs, classes, metas + (clust_var, )) annotated_data = create_annotated_table(data, selected_indices) elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0: # Select columns attrs = [] for clust, indices in chain(enumerate(maps, start=1), [(0, unselected_indices)]): for i in indices: attr = items.domain[i].copy() attr.attributes["cluster"] = clust attrs.append(attr) domain = Orange.data.Domain( # len(unselected_indices) can be 0 attrs[:len(attrs) - len(unselected_indices)], items.domain.class_vars, items.domain.metas) selected_data = items.from_table(domain, items) domain = Orange.data.Domain(attrs, items.domain.class_vars, items.domain.metas) annotated_data = items.from_table(domain, items) self.Outputs.selected_data.send(selected_data) self.Outputs.annotated_data.send(annotated_data) def eventFilter(self, obj, event): if obj is self.view.viewport() and event.type() == QEvent.Resize: # NOTE: not using viewport.width(), due to 'transient' scroll bars # (macOS). Viewport covers the whole view, but QGraphicsView still # scrolls left, right with scroll bar extent (other # QAbstractScrollArea widgets behave as expected). w_frame = self.view.frameWidth() margin = self.view.viewportMargins() w_scroll = self.view.verticalScrollBar().width() width = (self.view.width() - w_frame * 2 - margin.left() - margin.right() - w_scroll) # layout with new width constraint self.__layout_main_graphics(width=width) elif obj is self._main_graphics and \ event.type() == QEvent.LayoutRequest: # layout preserving the width (vertical re layout) self.__layout_main_graphics() return super().eventFilter(obj, event) @Slot(QPointF) def _activate_cut_line(self, pos: QPointF): """Activate cut line selection an set cut value to `pos.x()`.""" self.selection_method = 1 self.cut_line.setValue(pos.x()) self._selection_method_changed() def onDeleteWidget(self): super().onDeleteWidget() self._clear_plot() self.dendrogram.clear() self.dendrogram.deleteLater() def _dendrogram_geom_changed(self): pos = self.dendrogram.pos_at_height(self.cutoff_height) geom = self.dendrogram.geometry() self._set_slider_value(pos.x(), geom.width()) self.cut_line.setLength(self.bottom_axis.geometry().bottom() - self.top_axis.geometry().top()) geom = self._main_graphics.geometry() assert geom.topLeft() == QPointF(0, 0) def adjustLeft(rect): rect = QRectF(rect) rect.setLeft(geom.left()) return rect margin = 3 self.scene.setSceneRect(geom) self.view.setSceneRect(geom) self.view.setHeaderSceneRect( adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)) self.view.setFooterSceneRect( adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)) def _dendrogram_slider_changed(self, value): p = QPointF(value, 0) cl_height = self.dendrogram.height_at(p) self.set_cutoff_height(cl_height) def _set_slider_value(self, value, span): with blocked(self.cut_line): self.cut_line.setRange(0, span) self.cut_line.setValue(value) def set_cutoff_height(self, height): self.cutoff_height = height if self.root: self.cut_ratio = 100 * height / self.root.value.height self.select_max_height(height) def _set_cut_line_visible(self, visible): self.cut_line.setVisible(visible) def select_top_n(self, n): root = self._displayed_root if root: clusters = top_clusters(root, n) self.dendrogram.set_selected_clusters(clusters) def select_max_height(self, height): root = self._displayed_root if root: clusters = clusters_at_height(root, height) self.dendrogram.set_selected_clusters(clusters) def _selection_method_changed(self): self._set_cut_line_visible(self.selection_method == 1) if self.root: self._apply_selection() def _apply_selection(self): if not self.root: return if self.selection_method == 0: pass elif self.selection_method == 1: height = self.cut_ratio * self.root.value.height / 100 self.set_cutoff_height(height) pos = self.dendrogram.pos_at_height(height) self._set_slider_value(pos.x(), self.dendrogram.size().width()) elif self.selection_method == 2: self.select_top_n(self.top_n) def _selection_edited(self): # Selection was edited by clicking on a cluster in the # dendrogram view. self.selection_method = 0 self._selection_method_changed() self._invalidate_output() def _save_selection(self): # Save the current manual node selection state selection_state = None if self.selection_method == 0 and self.root: assert self.linkmatrix is not None linkmat = [(int(_0), int(_1), _2) for _0, _1, _2 in self.linkmatrix[:, :3].tolist()] nodes_ = self.dendrogram.selected_nodes() # match the display (pruned) nodes back (by ranges) mapping = {node.value.range: node for node in postorder(self.root)} nodes = [mapping[node.value.range] for node in nodes_] indices = [ tuple(node.value.index for node in leaves(node)) for node in nodes ] if nodes: selection_state = (indices, linkmat) return selection_state def save_state(self): # type: () -> Dict[str, Any] """ Save state for `set_restore_state` """ selection = self._save_selection() res = {"version": (0, 0, 0)} if selection is not None: res["selection_state"] = selection return res def set_restore_state(self, state): # type: (Dict[str, Any]) -> bool """ Restore session data from a saved state. Parameters ---------- state : Dict[str, Any] NOTE ---- This is method called while the instance (self) is being constructed, even before its `__init__` is called. Consider `self` to be only a `QObject` at this stage. """ if "selection_state" in state: selection = state["selection_state"] self.__pending_selection_restore = selection return True def __zoom_in(self): def clip(minval, maxval, val): return min(max(val, minval), maxval) self.zoom_factor = clip(self.zoom_slider.minimum(), self.zoom_slider.maximum(), self.zoom_factor + 1) self.__update_font_scale() def __zoom_out(self): def clip(minval, maxval, val): return min(max(val, minval), maxval) self.zoom_factor = clip(self.zoom_slider.minimum(), self.zoom_slider.maximum(), self.zoom_factor - 1) self.__update_font_scale() def __zoom_reset(self): self.zoom_factor = 0 self.__update_font_scale() def __layout_main_graphics(self, width=-1): if width < 0: # Preserve current width. width = self._main_graphics.size().width() preferred = self._main_graphics.effectiveSizeHint(Qt.PreferredSize, constraint=QSizeF( width, -1)) self._main_graphics.resize(QSizeF(width, preferred.height())) mw = self._main_graphics.minimumWidth() + 4 self.view.setMinimumWidth(mw + self.view.verticalScrollBar().width()) def __update_font_scale(self): font = self.scene.font() factor = (1.25**self.zoom_factor) font = qfont_scaled(font, factor) self._main_graphics.setFont(font) def send_report(self): annot = self.label_cb.currentText() if isinstance(self.annotation, str): annot = annot.lower() if self.selection_method == 0: sel = "manual" elif self.selection_method == 1: sel = "at {:.1f} of height".format(self.cut_ratio) else: sel = "top {} clusters".format(self.top_n) self.report_items(( ("Linkage", LINKAGE[self.linkage].lower()), ("Annotation", annot), ("Prunning", self.pruning != 0 and "{} levels".format(self.max_depth)), ("Selection", sel), )) self.report_plot()
def setGeometry(self, rect): QGraphicsWidget.setGeometry(self, rect) self.__textItem.setTextWidth(rect.width())
def __updateState(self): """ Update the widget with the new source/sink node signal descriptions. """ widget = QGraphicsWidget() widget.setLayout(QGraphicsGridLayout()) # Space between left and right anchors widget.layout().setHorizontalSpacing(50) left_node = EditLinksNode(self, direction=Qt.LeftToRight, node=self.source) left_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) right_node = EditLinksNode(self, direction=Qt.RightToLeft, node=self.sink) right_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) left_node.setMinimumWidth(150) right_node.setMinimumWidth(150) widget.layout().addItem(left_node, 0, 0,) widget.layout().addItem(right_node, 0, 1,) title_template = "<center><b>{0}<b></center>" left_title = GraphicsTextWidget(self) left_title.setHtml(title_template.format(escape(self.source.title))) left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) right_title = GraphicsTextWidget(self) right_title.setHtml(title_template.format(escape(self.sink.title))) right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) widget.layout().addItem(left_title, 1, 0, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.layout().addItem(right_title, 1, 1, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.setParentItem(self) max_w = max(left_node.sizeHint(Qt.PreferredSize).width(), right_node.sizeHint(Qt.PreferredSize).width()) # fix same size left_node.setMinimumWidth(max_w) right_node.setMinimumWidth(max_w) left_title.setMinimumWidth(max_w) right_title.setMinimumWidth(max_w) self.layout().addItem(widget) self.layout().activate() self.sourceNodeWidget = left_node self.sinkNodeWidget = right_node self.sourceNodeTitle = left_title self.sinkNodeTitle = right_title self.__resetAnchorStates() # AnchorHover hover over anchor before hovering over line class AnchorHover(QGraphicsRectItem): def __init__(self, anchor, parent=None): QGraphicsRectItem.__init__(self, parent=parent) self.setAcceptHoverEvents(True) self.anchor = anchor self.setRect(anchor.boundingRect()) self.setPos(self.mapFromScene(anchor.scenePos())) self.setFlag(QGraphicsItem.ItemHasNoContents, True) def hoverEnterEvent(self, event): if self.anchor.isEnabled(): self.anchor.hoverEnterEvent(event) else: event.ignore() def hoverLeaveEvent(self, event): if self.anchor.isEnabled(): self.anchor.hoverLeaveEvent(event) else: event.ignore() for anchor in left_node.channelAnchors + right_node.channelAnchors: anchor_hover = AnchorHover(anchor, parent=self) anchor_hover.setZValue(2.0)
def __init__(self, parent=None, rug=None): QGraphicsWidget.__init__(self, parent) self.rug_items = [] self.set_rug(rug) self.setMaximumHeight(30) self.setMinimumHeight(30)
def sizeHint(self, which, constraint=QSizeF()): if which == Qt.PreferredSize: return QSizeF(self._pixmap.size()) else: return QGraphicsWidget.sizeHint(self, which, constraint)
def widget(): w = QGraphicsWidget() w.setMinimumSize(QSizeF(10, 10)) w.setMaximumSize(QSizeF(10, 10)) return w
def __updateState(self): """ Update the widget with the new source/sink node signal descriptions. """ widget = QGraphicsWidget() widget.setLayout(QGraphicsGridLayout()) # Space between left and right anchors widget.layout().setHorizontalSpacing(50) left_node = EditLinksNode(self, direction=Qt.LeftToRight, node=self.source) left_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) right_node = EditLinksNode(self, direction=Qt.RightToLeft, node=self.sink) right_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) left_node.setMinimumWidth(150) right_node.setMinimumWidth(150) widget.layout().addItem( left_node, 0, 0, ) widget.layout().addItem( right_node, 0, 1, ) title_template = "<center><b>{0}<b></center>" left_title = GraphicsTextWidget(self) left_title.setHtml(title_template.format(escape(self.source.title))) left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) right_title = GraphicsTextWidget(self) right_title.setHtml(title_template.format(escape(self.sink.title))) right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) widget.layout().addItem(left_title, 1, 0, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.layout().addItem(right_title, 1, 1, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.setParentItem(self) max_w = max( left_node.sizeHint(Qt.PreferredSize).width(), right_node.sizeHint(Qt.PreferredSize).width()) # fix same size left_node.setMinimumWidth(max_w) right_node.setMinimumWidth(max_w) left_title.setMinimumWidth(max_w) right_title.setMinimumWidth(max_w) self.layout().addItem(widget) self.layout().activate() self.sourceNodeWidget = left_node self.sinkNodeWidget = right_node self.sourceNodeTitle = left_title self.sinkNodeTitle = right_title
def replot_experiments(self): """Replot the whole quality plot. """ self.scene.clear() labels = [] max_dist = numpy.nanmax(list(filter(None, self.distances))) rug_widgets = [] group_pen = QPen(Qt.black) group_pen.setWidth(2) group_pen.setCapStyle(Qt.RoundCap) background_pen = QPen(QColor(0, 0, 250, 150)) background_pen.setWidth(1) background_pen.setCapStyle(Qt.RoundCap) main_widget = QGraphicsWidget() layout = QGraphicsGridLayout() attributes = self.data.domain.attributes if self.data is not None: for (group, indices), dist_vec in zip(self.groups, self.distances): indices_set = set(indices) rug_items = [] if dist_vec is not None: for i, attr in enumerate(attributes): # Is this a within group distance or background in_group = i in indices_set if in_group: rug_item = ClickableRugItem(dist_vec[i] / max_dist, 1.0, self.on_rug_item_clicked) rug_item.setPen(group_pen) tooltip = experiment_description(attr) rug_item.setToolTip(tooltip) rug_item.group_index = indices.index(i) rug_item.setZValue(rug_item.zValue() + 1) else: rug_item = ClickableRugItem(dist_vec[i] / max_dist, 0.85, self.on_rug_item_clicked) rug_item.setPen(background_pen) tooltip = experiment_description(attr) rug_item.setToolTip(tooltip) rug_item.group = group rug_item.index = i rug_item.in_group = in_group rug_items.append(rug_item) rug_widget = RugGraphicsWidget(parent=main_widget) rug_widget.set_rug(rug_items) rug_widgets.append(rug_widget) label = group_label(self.selected_split_by_labels(), group) label_item = QGraphicsSimpleTextItem(label, main_widget) label_item = GraphicsSimpleTextLayoutItem(label_item, parent=layout) label_item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) labels.append(label_item) for i, (label, rug_w) in enumerate(zip(labels, rug_widgets)): layout.addItem(label, i, 0, Qt.AlignVCenter) layout.addItem(rug_w, i, 1) layout.setRowMaximumHeight(i, 30) main_widget.setLayout(layout) self.scene.addItem(main_widget) self.main_widget = main_widget self.rug_widgets = rug_widgets self.labels = labels self.on_view_resize(self.scene_view.size())
def resizeEvent(self, event): """Reimplemented from QGraphicsWidget """ QGraphicsWidget.resizeEvent(self, event) self.update_rug_geometry()
class DendrogramWidget(QGraphicsWidget): """A Graphics Widget displaying a dendrogram.""" class ClusterGraphicsItem(QGraphicsPathItem): #: The untransformed source path in 'dendrogram' logical coordinate #: system sourcePath = QPainterPath() # type: QPainterPath sourceAreaShape = QPainterPath() # type: QPainterPath __shape = None # type: Optional[QPainterPath] __boundingRect = None # type: Optional[QRectF] #: An extended path describing the full mouse hit area #: (extends all the way to the base of the dendrogram) __mouseAreaShape = QPainterPath() # type: QPainterPath def setGeometryData(self, path, hitArea): # type: (QPainterPath, QPainterPath) -> None """ Set the geometry (path) and the mouse hit area (hitArea) for this item. """ super().setPath(path) self.prepareGeometryChange() self.__boundingRect = self.__shape = None self.__mouseAreaShape = hitArea def shape(self): # type: () -> QPainterPath if self.__shape is None: path = super().shape() # type: QPainterPath self.__shape = path.united(self.__mouseAreaShape) return self.__shape def boundingRect(self): # type: () -> QRectF if self.__boundingRect is None: sh = self.shape() pw = self.pen().widthF() / 2.0 self.__boundingRect = sh.boundingRect().adjusted( -pw, -pw, pw, pw) return self.__boundingRect class _SelectionItem(QGraphicsItemGroup): def __init__(self, parent, path, unscaled_path, label=""): super().__init__(parent) self.path = QGraphicsPathItem(path, self) self.path.setPen(make_pen(width=1, cosmetic=True)) self.addToGroup(self.path) self.label = QGraphicsSimpleTextItem(label) self._update_label_pos() self.addToGroup(self.label) self.unscaled_path = unscaled_path def set_path(self, path): self.path.setPath(path) self._update_label_pos() def set_label(self, label): self.label.setText(label) self._update_label_pos() def set_color(self, color): self.path.setBrush(QColor(color)) def _update_label_pos(self): path = self.path.path() elements = (path.elementAt(i) for i in range(path.elementCount())) points = ((p.x, p.y) for p in elements) p1, p2, *rest = sorted(points) x, y = p1[0], (p1[1] + p2[1]) / 2 brect = self.label.boundingRect() # leaf nodes' paths are 4 pixels higher; leafs are `len(rest) == 3` self.label.setPos(x - brect.width() - 4, y - brect.height() + 4 * (len(rest) == 3)) #: Orientation Left, Top, Right, Bottom = 1, 2, 3, 4 #: Selection flags NoSelection, SingleSelection, ExtendedSelection = 0, 1, 2 #: Emitted when a user clicks on the cluster item. itemClicked = Signal(ClusterGraphicsItem) #: Signal emitted when the selection changes. selectionChanged = Signal() #: Signal emitted when the selection was changed by the user. selectionEdited = Signal() def __init__(self, parent=None, root=None, orientation=Left, hoverHighlightEnabled=True, selectionMode=ExtendedSelection, **kwargs): super().__init__(None, **kwargs) # Filter all events from children (`ClusterGraphicsItem`s) self.setFiltersChildEvents(True) self.orientation = orientation self._root = None #: A tree with dendrogram geometry self._layout = None self._highlighted_item = None #: a list of selected items self._selection = OrderedDict() #: a {node: item} mapping self._items = { } # type: Dict[Tree, DendrogramWidget.ClusterGraphicsItem] #: container for all cluster items. self._itemgroup = QGraphicsWidget(self) self._itemgroup.setGeometry(self.contentsRect()) #: Transform mapping from 'dendrogram' to widget local coordinate #: system self._transform = QTransform() self._cluster_parent = {} self.__hoverHighlightEnabled = hoverHighlightEnabled self.__selectionMode = selectionMode self.setContentsMargins(0, 0, 0, 0) self.setRoot(root) if parent is not None: self.setParentItem(parent) def setSelectionMode(self, mode): """ Set the selection mode. """ assert mode in [ DendrogramWidget.NoSelection, DendrogramWidget.SingleSelection, DendrogramWidget.ExtendedSelection ] if self.__selectionMode != mode: self.__selectionMode = mode if self.__selectionMode == DendrogramWidget.NoSelection and \ self._selection: self.setSelectedClusters([]) elif self.__selectionMode == DendrogramWidget.SingleSelection and \ len(self._selection) > 1: self.setSelectedClusters([self.selected_nodes()[-1]]) def selectionMode(self): """ Return the current selection mode. """ return self.__selectionMode def setHoverHighlightEnabled(self, enabled): if self.__hoverHighlightEnabled != bool(enabled): self.__hoverHighlightEnabled = bool(enabled) if self._highlighted_item is not None: self._set_hover_item(None) def isHoverHighlightEnabled(self): return self.__hoverHighlightEnabled def clear(self): """ Clear the widget. """ scene = self.scene() if scene is not None: scene.removeItem(self._itemgroup) else: self._itemgroup.setParentItem(None) self._itemgroup = QGraphicsWidget(self) self._itemgroup.setGeometry(self.contentsRect()) self._items.clear() for item in self._selection.values(): if scene is not None: scene.removeItem(item) else: item.setParentItem(None) self._root = None self._items = {} self._selection = OrderedDict() self._highlighted_item = None self._cluster_parent = {} self.updateGeometry() def setRoot(self, root): # type: (Tree) -> None """ Set the root cluster tree node for display. Parameters ---------- root : Tree The tree root node. """ self.clear() self._root = root if root is not None: foreground = self.palette().color(QPalette.Foreground) pen = make_pen(foreground, width=1, cosmetic=True, join_style=Qt.MiterJoin) for node in postorder(root): item = DendrogramWidget.ClusterGraphicsItem(self._itemgroup) item.setAcceptHoverEvents(True) item.setPen(pen) item.node = node for branch in node.branches: assert branch in self._items self._cluster_parent[branch] = node self._items[node] = item self._relayout() self._rescale() self.updateGeometry() set_root = setRoot def root(self): # type: () -> Tree """ Return the cluster tree root node. Returns ------- root : Tree """ return self._root def item(self, node): # type: (Tree) -> DendrogramWidget.ClusterGraphicsItem """ Return the ClusterGraphicsItem instance representing the cluster `node`. """ return self._items.get(node) def heightAt(self, point): # type: (QPointF) -> float """ Return the cluster height at the point in widget local coordinates. """ if not self._root: return 0 tinv, ok = self._transform.inverted() if not ok: return 0 tpoint = tinv.map(point) if self.orientation in [self.Left, self.Right]: height = tpoint.x() else: height = tpoint.y() # Undo geometry prescaling base = self._root.value.height scale = self._height_scale_factor() # Use better better precision then double provides. Fr = fractions.Fraction if scale > 0: height = Fr(height) / Fr(scale) else: height = 0 if self.orientation in [self.Left, self.Bottom]: height = Fr(base) - Fr(height) return float(height) height_at = heightAt def posAtHeight(self, height): # type: (float) -> float """ Return a point in local coordinates for `height` (in cluster """ if not self._root: return QPointF() scale = self._height_scale_factor() base = self._root.value.height height = scale * height if self.orientation in [self.Left, self.Bottom]: height = scale * base - height if self.orientation in [self.Left, self.Right]: p = QPointF(height, 0) else: p = QPointF(0, height) return self._transform.map(p) pos_at_height = posAtHeight def _set_hover_item(self, item): """Set the currently highlighted item.""" if self._highlighted_item is item: return def set_pen(item, pen): def branches(item): return [self._items[ch] for ch in item.node.branches] for it in postorder(item, branches): it.setPen(pen) if self._highlighted_item: # Restore the previous item highlight = self.palette().color(QPalette.Foreground) set_pen(self._highlighted_item, make_pen(highlight, width=1, cosmetic=True)) self._highlighted_item = item if item: hpen = make_pen(self.palette().color(QPalette.Highlight), width=2, cosmetic=True) set_pen(item, hpen) def leafItems(self): """Iterate over the dendrogram leaf items (:class:`QGraphicsItem`). """ if self._root: return (self._items[leaf] for leaf in leaves(self._root)) else: return iter(()) leaf_items = leafItems def leafAnchors(self): """Iterate over the dendrogram leaf anchor points (:class:`QPointF`). The points are in the widget local coordinates. """ for item in self.leafItems(): anchor = QPointF(item.element.anchor) yield self.mapFromItem(item, anchor) leaf_anchors = leafAnchors def selectedNodes(self): """ Return the selected cluster nodes. """ return [item.node for item in self._selection] selected_nodes = selectedNodes def setSelectedItems(self, items: List[ClusterGraphicsItem]): """Set the item selection.""" to_remove = set(self._selection) - set(items) to_add = set(items) - set(self._selection) for sel in to_remove: self._remove_selection(sel) for sel in to_add: self._add_selection(sel) if to_add or to_remove: self._re_enumerate_selections() self.selectionChanged.emit() set_selected_items = setSelectedItems def setSelectedClusters(self, clusters: List[Tree]) -> None: """Set the selected clusters. """ self.setSelectedItems(list(map(self.item, clusters))) set_selected_clusters = setSelectedClusters def isItemSelected(self, item: ClusterGraphicsItem) -> bool: """Is `item` selected (is a root of a selection).""" return item in self._selection def isItemIncludedInSelection(self, item: ClusterGraphicsItem) -> bool: """Is item included in any selection.""" return self._selected_super_item(item) is not None is_included = isItemIncludedInSelection def setItemSelected(self, item, state): # type: (ClusterGraphicsItem, bool) -> None """Set the `item`s selection state to `state`.""" if state is False and item not in self._selection or \ state is True and item in self._selection: return # State unchanged if item in self._selection: if state is False: self._remove_selection(item) self._re_enumerate_selections() self.selectionChanged.emit() else: # If item is already inside another selected item, # remove that selection super_selection = self._selected_super_item(item) if super_selection: self._remove_selection(super_selection) # Remove selections this selection will override. sub_selections = self._selected_sub_items(item) for sub in sub_selections: self._remove_selection(sub) if state: self._add_selection(item) elif item in self._selection: self._remove_selection(item) self._re_enumerate_selections() self.selectionChanged.emit() select_item = setItemSelected @staticmethod def _create_path(item, path): ppath = QPainterPath() if item.node.is_leaf: ppath.addRect(path.boundingRect().adjusted(-8, -4, 0, 4)) else: ppath.addPolygon(path) ppath = path_outline(ppath, width=-8) return ppath @staticmethod def _create_label(i): return f"C{i + 1}" def _add_selection(self, item): """Add selection rooted at item """ outline = self._selection_poly(item) path = self._transform.map(outline) ppath = self._create_path(item, path) label = self._create_label(len(self._selection)) selection_item = self._SelectionItem(self, ppath, outline, label) selection_item.label.setBrush(self.palette().color(QPalette.Link)) selection_item.setPos(self.contentsRect().topLeft()) self._selection[item] = selection_item def _remove_selection(self, item): """Remove selection rooted at item.""" selection_item = self._selection[item] selection_item.hide() selection_item.setParentItem(None) if self.scene(): self.scene().removeItem(selection_item) del self._selection[item] def _selected_sub_items(self, item): """Return all selected subclusters under item.""" def branches(item): return [self._items[ch] for ch in item.node.branches] res = [] for item in list(preorder(item, branches))[1:]: if item in self._selection: res.append(item) return res def _selected_super_item(self, item): """Return the selected super item if it exists.""" def branches(item): return [self._items[ch] for ch in item.node.branches] for selected_item in self._selection: if item in set(preorder(selected_item, branches)): return selected_item return None def _re_enumerate_selections(self): """Re enumerate the selection items and update the colors.""" # Order the clusters items = sorted(self._selection.items(), key=lambda item: item[0].node.value.first) palette = colorpalettes.LimitedDiscretePalette(len(items)) for i, (item, selection_item) in enumerate(items): # delete and then reinsert to update the ordering del self._selection[item] self._selection[item] = selection_item selection_item.set_label(self._create_label(i)) color = palette[i] color.setAlpha(150) selection_item.set_color(color) def _selection_poly(self, item): # type: (Tree) -> QPolygonF """ Return an selection geometry covering item and all its children. """ def left(item): return [self._items[ch] for ch in item.node.branches[:1]] def right(item): return [self._items[ch] for ch in item.node.branches[-1:]] itemsleft = list(preorder(item, left))[::-1] itemsright = list(preorder(item, right)) # itemsleft + itemsright walks from the leftmost leaf up to the root # and down to the rightmost leaf assert itemsleft[0].node.is_leaf assert itemsright[-1].node.is_leaf if item.node.is_leaf: # a single anchor point vert = [itemsleft[0].element.anchor] else: vert = [] for it in itemsleft[1:]: vert.extend([ it.element.path[0], it.element.path[1], it.element.anchor ]) for it in itemsright[:-1]: vert.extend([ it.element.anchor, it.element.path[-2], it.element.path[-1] ]) # close the polygon vert.append(vert[0]) def isclose(a, b, rel_tol=1e-6): return abs(a - b) < rel_tol * max(abs(a), abs(b)) def isclose_p(p1, p2, rel_tol=1e-6): return isclose(p1.x, p2.x, rel_tol) and \ isclose(p1.y, p2.y, rel_tol) # merge consecutive vertices that are (too) close acc = [vert[0]] for v in vert[1:]: if not isclose_p(v, acc[-1]): acc.append(v) vert = acc return QPolygonF([QPointF(*p) for p in vert]) def _update_selection_items(self): """Update the shapes of selection items after a scale change. """ transform = self._transform for item, selection in self._selection.items(): path = transform.map(selection.unscaled_path) ppath = self._create_path(item, path) selection.set_path(ppath) def _height_scale_factor(self): # Internal dendrogram height scale factor. The dendrogram geometry is # scaled by this factor to better condition the geometry if self._root is None: return 1 base = self._root.value.height # implicitly scale the geometry to 0..1 scale or flush to 0 for fuzz if base >= np.finfo(base).eps: return 1 / base else: return 0 def _relayout(self): if self._root is None: return scale = self._height_scale_factor() base = scale * self._root.value.height self._layout = dendrogram_path(self._root, self.orientation, scaleh=scale) for node_geom in postorder(self._layout): node, geom = node_geom.value item = self._items[node] item.element = geom # the untransformed source path item.sourcePath = path_toQtPath(geom) r = item.sourcePath.boundingRect() if self.orientation == Left: r.setRight(base) elif self.orientation == Right: r.setLeft(0) elif self.orientation == Top: r.setBottom(base) else: r.setTop(0) hitarea = QPainterPath() hitarea.addRect(r) item.sourceAreaShape = hitarea item.setGeometryData(item.sourcePath, item.sourceAreaShape) item.setZValue(-node.value.height) def _rescale(self): if self._root is None: return scale = self._height_scale_factor() base = scale * self._root.value.height crect = self.contentsRect() leaf_count = len(list(leaves(self._root))) if self.orientation in [Left, Right]: drect = QSizeF(base, leaf_count) else: drect = QSizeF(leaf_count, base) eps = np.finfo(np.float64).eps if abs(drect.width()) < eps: sx = 1.0 else: sx = crect.width() / drect.width() if abs(drect.height()) < eps: sy = 1.0 else: sy = crect.height() / drect.height() transform = QTransform().scale(sx, sy) self._transform = transform self._itemgroup.setPos(crect.topLeft()) self._itemgroup.setGeometry(crect) for node_geom in postorder(self._layout): node, _ = node_geom.value item = self._items[node] item.setGeometryData(transform.map(item.sourcePath), transform.map(item.sourceAreaShape)) self._selection_items = None self._update_selection_items() def sizeHint(self, which: Qt.SizeHint, constraint=QSizeF()) -> QSizeF: # reimplemented fm = QFontMetrics(self.font()) spacing = fm.lineSpacing() mleft, mtop, mright, mbottom = self.getContentsMargins() if self._root and which == Qt.PreferredSize: nleaves = len( [node for node in self._items.keys() if not node.branches]) base = max(10, min(spacing * 16, 250)) if self.orientation in [self.Left, self.Right]: return QSizeF(base, spacing * nleaves + mleft + mright) else: return QSizeF(spacing * nleaves + mtop + mbottom, base) elif which == Qt.MinimumSize: return QSizeF(mleft + mright + 10, mtop + mbottom + 10) else: return QSizeF() def sceneEventFilter(self, obj, event): if isinstance(obj, DendrogramWidget.ClusterGraphicsItem): if event.type() == QEvent.GraphicsSceneHoverEnter and \ self.__hoverHighlightEnabled: self._set_hover_item(obj) event.accept() return True elif event.type() == QEvent.GraphicsSceneMousePress and \ event.button() == Qt.LeftButton: is_selected = self.isItemSelected(obj) is_included = self.is_included(obj) current_selection = list(self._selection) if self.__selectionMode == DendrogramWidget.SingleSelection: if event.modifiers() & Qt.ControlModifier: self.setSelectedItems([obj] if not is_selected else []) elif event.modifiers() & Qt.AltModifier: self.setSelectedItems([]) elif event.modifiers() & Qt.ShiftModifier: if not is_included: self.setSelectedItems([obj]) elif current_selection != [obj]: self.setSelectedItems([obj]) elif self.__selectionMode == DendrogramWidget.ExtendedSelection: if event.modifiers() & Qt.ControlModifier: self.setItemSelected(obj, not is_selected) elif event.modifiers() & Qt.AltModifier: self.setItemSelected(self._selected_super_item(obj), False) elif event.modifiers() & Qt.ShiftModifier: if not is_included: self.setItemSelected(obj, True) elif current_selection != [obj]: self.setSelectedItems([obj]) if current_selection != self._selection: self.selectionEdited.emit() self.itemClicked.emit(obj) event.accept() return True if event.type() == QEvent.GraphicsSceneHoverLeave: self._set_hover_item(None) return super().sceneEventFilter(obj, event) def changeEvent(self, event): # reimplemented super().changeEvent(event) if event.type() == QEvent.FontChange: self.updateGeometry() elif event.type() == QEvent.PaletteChange: self._update_colors() elif event.type() == QEvent.ContentsRectChange: self._rescale() def resizeEvent(self, event): # reimplemented super().resizeEvent(event) self._rescale() def mousePressEvent(self, event): # reimplemented super().mousePressEvent(event) # A mouse press on an empty widget part if event.modifiers() == Qt.NoModifier and self._selection: self.set_selected_clusters([]) def _update_colors(self): def set_color(item: DendrogramWidget.ClusterGraphicsItem, color: QColor): def branches(item): return [self._items[ch] for ch in item.node.branches] for it in postorder(item, branches): it.setPen(update_pen(it.pen(), brush=color)) if self._root is not None: foreground = self.palette().color(QPalette.Foreground) item = self.item(self._root) set_color(item, foreground) highlight = self.palette().color(QPalette.Highlight) if self._highlighted_item is not None: set_color(self._highlighted_item, highlight) accent = self.palette().color(QPalette.Link) for item in self._selection.values(): item.label.setBrush(accent)