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 setUp(self): super(TestAnchorLayout, self).setUp() self.scene = CanvasScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing) self.view.show() self.view.resize(600, 400)
def mousePressEvent(self, event): self._start_pos = event.pos() if event.button() == Qt.LeftButton: # Save the current selection and restore it on mouse{Move,Release} if not event.modifiers() & Qt.ShiftModifier: self._selection = [] QGraphicsView.mousePressEvent(self, event)
def setUp(self): import logging from AnyQt.QtWidgets import QApplication, QGraphicsScene, QGraphicsView from AnyQt.QtGui import QPainter from AnyQt.QtCore import QTimer logging.basicConfig() self.app = QApplication([]) self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing) self.view.resize(500, 300) self.view.show() QTimer.singleShot(10000, self.app.exit) def my_excepthook(*args): sys.setrecursionlimit(1010) traceback.print_exc(limit=4) self._orig_excepthook = sys.excepthook sys.excepthook = my_excepthook self.singleShot = QTimer.singleShot
def __setupUi(self): layout = QVBoxLayout() # Scene with the link editor. self.scene = LinksEditScene() self.view = QGraphicsView(self.scene) self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setRenderHint(QPainter.Antialiasing) self.scene.editWidget.geometryChanged.connect(self.__onGeometryChanged) # Ok/Cancel/Clear All buttons. buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset, Qt.Horizontal) clear_button = buttons.button(QDialogButtonBox.Reset) clear_button.setText(self.tr("Clear All")) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) clear_button.clicked.connect(self.scene.editWidget.clearLinks) layout.addWidget(self.view) layout.addWidget(buttons) self.setLayout(layout) layout.setSizeConstraint(QVBoxLayout.SetFixedSize) self.setSizeGripEnabled(False)
def test_editlinksnode(self): from ...registry.tests import small_testing_registry reg = small_testing_registry() file_desc = reg.widget("Orange.widgets.data.owfile.OWFile") bayes_desc = reg.widget("Orange.widgets.classify.ownaivebayes." "OWNaiveBayes") source_node = SchemeNode(file_desc, title="This is File") sink_node = SchemeNode(bayes_desc) scene = QGraphicsScene() view = QGraphicsView(scene) node = EditLinksNode(node=source_node) scene.addItem(node) node = EditLinksNode(direction=Qt.RightToLeft) node.setSchemeNode(sink_node) node.setPos(300, 0) scene.addItem(node) view.show() view.resize(800, 300) self.app.exec_()
def setUp(self): QAppTestCase.setUp(self) self.scene = CanvasScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing) self.view.show() self.view.resize(600, 400)
def setUp(self) -> None: super().setUp() self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.resize(300, 300) self.widget = DendrogramWidget() self.scene.addItem(self.widget)
def __init__(self): super().__init__() # Diagram update is in progress self._updating = False # Input update is in progress self._inputUpdate = False # Input datasets in the order they were 'connected'. self.data = {} # Extracted input item sets in the order they were 'connected' self.itemsets = {} # A list with 2 ** len(self.data) elements that store item sets # belonging to each area self.disjoint = [] # A list with 2 ** len(self.data) elements that store keys of tables # intersected in each area self.area_keys = [] # Main area view self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing) self.view.setBackgroundRole(QPalette.Window) self.view.setFrameStyle(QGraphicsView.StyledPanel) self.mainArea.layout().addWidget(self.view) self.vennwidget = VennDiagram() self._resize() self.vennwidget.itemTextEdited.connect(self._on_itemTextEdited) self.scene.selectionChanged.connect(self._on_selectionChanged) self.scene.addItem(self.vennwidget) controls = gui.hBox(self.mainArea) box = gui.radioButtonsInBox(controls, self, 'rowwise', [ "Columns (features)", "Rows (instances), matched by", ], box="Elements", callback=self._on_matching_changed) gui.comboBox(gui.indentedBox(box), self, "selected_feature", model=itemmodels.VariableListModel( placeholder="Instance identity"), callback=self._on_inputAttrActivated) box.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) self.outputs_box = box = gui.vBox(controls, "Output") self.output_duplicates_cb = gui.checkBox( box, self, "output_duplicates", "Output duplicates", callback=lambda: self.commit()) # pylint: disable=unnecessary-lambda gui.auto_send(box, self, "autocommit", box=False) self.output_duplicates_cb.setEnabled(bool(self.rowwise)) self._queue = []
def setUp(self): super().setUp() self.scene = CanvasScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing) self.view.show() self.view.resize(400, 300)
def setUp(self): super().setUp() self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.item = GraphicsTextItem() self.item.setPlainText("AAA") self.item.setTextInteractionFlags(Qt.TextEditable) self.scene.addItem(self.item) self.view.setFocus()
def __init__(self, parent=None): super().__init__(parent) ## Attributes self.data = None self.distances = None self.groups = None self.unique_pos = None self.base_group_index = 0 ## GUI box = gui.widgetBox(self.controlArea, "Info") self.info_box = gui.widgetLabel(box, "\n") ## Separate By box box = gui.widgetBox(self.controlArea, "Separate By") self.split_by_model = itemmodels.PyListModel(parent=self) self.split_by_view = QListView() self.split_by_view.setSelectionMode(QListView.ExtendedSelection) self.split_by_view.setModel(self.split_by_model) box.layout().addWidget(self.split_by_view) self.split_by_view.selectionModel().selectionChanged.connect( self.on_split_key_changed) ## Sort By box box = gui.widgetBox(self.controlArea, "Sort By") self.sort_by_model = itemmodels.PyListModel(parent=self) self.sort_by_view = QListView() self.sort_by_view.setSelectionMode(QListView.ExtendedSelection) self.sort_by_view.setModel(self.sort_by_model) box.layout().addWidget(self.sort_by_view) self.sort_by_view.selectionModel().selectionChanged.connect( self.on_sort_key_changed) ## Distance box box = gui.widgetBox(self.controlArea, "Distance Measure") gui.comboBox(box, self, "selected_distance_index", items=[name for name, _ in self.DISTANCE_FUNCTIONS], callback=self.on_distance_measure_changed) self.scene = QGraphicsScene() self.scene_view = QGraphicsView(self.scene) self.scene_view.setRenderHints(QPainter.Antialiasing) self.scene_view.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.mainArea.layout().addWidget(self.scene_view) self.scene_view.installEventFilter(self) self._disable_updates = False self._cached_distances = {} self._base_index_hints = {} self.main_widget = None self.resize(800, 600)
def __init__(self): super().__init__() self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.setFixedSize(50, 50) view = QGraphicsView() self.layout().addWidget(view) self.scene = QGraphicsScene(view) view.setScene(self.scene)
def setUp(self): super().setUp() self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing) self.view.resize(500, 300) self.view.show()
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 effective_background(scene: QGraphicsScene, view: QGraphicsView): background = scene.backgroundBrush() if background.style() != Qt.NoBrush: return background background = view.backgroundBrush() if background.style() != Qt.NoBrush: return background viewport = view.viewport() role = viewport.backgroundRole() if role != QPalette.NoRole: return viewport.palette().brush(role) return viewport.palette().brush(QPalette.Window)
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)
def __init__(self): super().__init__() self.dataset = None self.attrs = DomainModel(valid_types=Orange.data.DiscreteVariable, separators=False) cb = gui.comboBox(self.controlArea, self, "attribute", box=True, model=self.attrs, callback=self.update_scene, contentsLength=12) grid = QGridLayout() self.legend = gui.widgetBox(gui.indentedBox(cb.box), orientation=grid) grid.setColumnStretch(1, 1) grid.setHorizontalSpacing(6) self.legend_items = [] self.split_vars = DomainModel( valid_types=Orange.data.DiscreteVariable, separators=False, placeholder="None", ) self.split_combobox = gui.comboBox(self.controlArea, self, "split_var", box="Split by", model=self.split_vars, callback=self.update_scene) self.explode_checkbox = gui.checkBox(self.controlArea, self, "explode", "Explode pies", box=True, callback=self.update_scene) gui.rubber(self.controlArea) gui.widgetLabel( gui.hBox(self.controlArea, box=True), "The aim of this widget is to\n" "demonstrate that pie charts are\n" "a terrible visualization. Please\n" "don't use it for any other purpose.") self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.mainArea.layout().addWidget(self.view) self.mainArea.setMinimumWidth(500)
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)
def __init__(self): super().__init__() self.data = None self._effective_data = None self._matrix = None self._silhouette = None self._labels = None self._silplot = None gui.comboBox( self.controlArea, self, "distance_idx", box="Distance", items=[name for name, _ in OWSilhouettePlot.Distances], orientation=Qt.Horizontal, callback=self._invalidate_distances) box = gui.vBox(self.controlArea, "Cluster Label") self.cluster_var_cb = gui.comboBox( box, self, "cluster_var_idx", addSpace=4, callback=self._invalidate_scores) gui.checkBox( box, self, "group_by_cluster", "Group by cluster", callback=self._replot) self.cluster_var_model = itemmodels.VariableListModel(parent=self) self.cluster_var_cb.setModel(self.cluster_var_model) box = gui.vBox(self.controlArea, "Bars") gui.widgetLabel(box, "Bar width:") gui.hSlider( box, self, "bar_size", minValue=1, maxValue=10, step=1, callback=self._update_bar_size, addSpace=6) gui.widgetLabel(box, "Annotations:") self.annotation_cb = gui.comboBox( box, self, "annotation_var_idx", callback=self._update_annotations) self.annotation_var_model = itemmodels.VariableListModel(parent=self) self.annotation_var_model[:] = ["None"] self.annotation_cb.setModel(self.annotation_var_model) ibox = gui.indentedBox(box, 5) self.ann_hidden_warning = warning = gui.widgetLabel( ibox, "(increase the width to show)") ibox.setFixedWidth(ibox.sizeHint().width()) warning.setVisible(False) gui.rubber(self.controlArea) gui.separator(self.buttonsArea) box = gui.vBox(self.buttonsArea, "Output") # Thunk the call to commit to call conditional commit gui.checkBox(box, self, "add_scores", "Add silhouette scores", callback=lambda: self.commit()) gui.auto_commit( box, self, "auto_commit", "Commit", auto_label="Auto commit", box=False) # Ensure that the controlArea is not narrower than buttonsArea self.controlArea.layout().addWidget(self.buttonsArea) self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing, True) self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.mainArea.layout().addWidget(self.view)
def setUp(self): import logging from AnyQt.QtWidgets import \ QApplication, QGraphicsScene, QGraphicsView from AnyQt.QtGui import QPainter from AnyQt.QtCore import QTimer logging.basicConfig() self.app = QApplication([]) self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints( QPainter.Antialiasing | \ QPainter.SmoothPixmapTransform | \ QPainter.TextAntialiasing ) self.view.resize(500, 300) self.view.show() QTimer.singleShot(10000, self.app.exit) def my_excepthook(*args): sys.setrecursionlimit(1010) traceback.print_exc(limit=4) self._orig_excepthook = sys.excepthook sys.excepthook = my_excepthook self.singleShot = QTimer.singleShot
def __init__(self, master, *args): QGraphicsView.__init__(self, *args) self.master = master self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setRenderHints(QPainter.Antialiasing) scene = QGraphicsScene(self) self.pixmapGraphicsItem = QGraphicsPixmapItem(None) scene.addItem(self.pixmapGraphicsItem) self.setScene(scene) self.setMouseTracking(True) self.viewport().setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus)
class _GraphicsGuiTest(GuiTest): scene: QGraphicsScene view: QGraphicsView def setUp(self) -> None: super().setUp() self.view = QGraphicsView() self.scene = QGraphicsScene(self.view) self.view.setScene(self.scene) def tearDown(self) -> None: self.scene.clear() self.scene.deleteLater() self.scene = None self.view.deleteLater() self.view = None super().tearDown()
def __init__(self, parent=None): self.canvas = QGraphicsScene(0, 0, 1000, ColorButtonSize) QGraphicsView.__init__(self, self.canvas, parent) self.ensureVisible(0, 0, 1, 1) self.color1 = None self.color2 = None self.rgbColors = [] self.passThroughColors = None #self.setFrameStyle(QFrame.NoFrame) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setFixedHeight(ColorButtonSize) self.setMinimumWidth(ColorButtonSize) if parent and parent.layout() is not None: parent.layout().addWidget(self)
def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.p = -1.0 self.scale_x = self.scene_min_x = self.scene_width = 0 self.label_width = 0 common_options = dict( callback=self.attr_changed, sizeHint=(200, 100)) self.attrs = VariableListModel() gui.listView( self.controlArea, self, "attribute", box="Variable", model=self.attrs, **common_options) self.group_vars = VariableListModel() gui.listView( self.controlArea, self, "group_var", box="Grouping", model=self.group_vars, **common_options) # TODO: move Compare median/mean to grouping box self.display_box = gui.vBox(self.controlArea, "Display") gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.display_changed) self.stretching_box = gui.checkBox( self.controlArea, self, 'stretched', "Stretch bars", box='Display', callback=self.display_changed).box gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>No test results.</center>") self.mainArea.setMinimumWidth(650) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box()
def qgraphicsview_map_rect_from_scene(view: QGraphicsView, rect: QRectF) -> QPolygonF: """Like QGraphicsView.mapFromScene(QRectF) but returning a QPolygonF (without rounding). """ tr = view.viewportTransform() p1 = tr.map(rect.topLeft()) p2 = tr.map(rect.topRight()) p3 = tr.map(rect.bottomRight()) p4 = tr.map(rect.bottomLeft()) return QPolygonF([p1, p2, p3, p4])
def setUp(self): super().setUp() self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints( QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing ) self.view.resize(500, 300) self.view.show()
class TestGraphicsPixmapWidget(GuiTest): def setUp(self) -> None: super().setUp() self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) def tearDown(self) -> None: self.scene.clear() self.scene.deleteLater() self.view.deleteLater() del self.scene del self.view def test_graphicspixmapwidget(self): w = GraphicsPixmapWidget() self.scene.addItem(w) w.setPixmap(QPixmap(100, 100)) p = w.pixmap() self.assertEqual(p.size(), QSize(100, 100)) self.view.grab() w.setScaleContents(True) w.setAspectRatioMode(Qt.KeepAspectRatio) s = w.sizeHint(Qt.PreferredSize) self.assertEqual(s, QSizeF(100., 100.)) s = w.sizeHint(Qt.PreferredSize, QSizeF(200., -1.)) self.assertEqual(s, QSizeF(200., 200.)) s = w.sizeHint(Qt.PreferredSize, QSizeF(-1., 200.)) self.assertEqual(s, QSizeF(200., 200.)) self.view.grab()
def mouseReleaseEvent(self, event): QGraphicsView.mouseReleaseEvent(self, event) if event.button() != Qt.LeftButton: return _start_pos = self.plotItem.items[0].mapFromScene( self._start_pos) _end_pos = self.plotItem.items[0].mapFromScene(event.pos()) sx, sy = _start_pos.x(), _start_pos.y() ex, ey = _end_pos.x(), _end_pos.y() if sx > ex: sx, ex = ex, sx if sy < ey: sy, ey = ey, sy data = self.scatter.data selected_indices = ((sx <= data['x']) & (data['x'] <= ex) & (ey <= data['y']) & (data['y'] <= sy)).nonzero()[0] self._selection.extend(selected_indices) data['pen'][selected_indices] = self.SELECTED_PEN self.scatter.points() for item in data['item'][selected_indices]: item.updateItem() print(data['data'][selected_indices])
def test_graphicstextwidget(self): scene = QGraphicsScene() view = QGraphicsView(scene) text = GraphicsTextWidget() text.setHtml("<center><b>a text</b></center><p>paragraph</p>") scene.addItem(text) view.show() view.resize(400, 300) self.app.exec_()
class TestItems(QAppTestCase): def setUp(self): super().setUp() self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing) self.view.resize(500, 300) self.view.show() def tearDown(self): self.scene.clear() self.scene.deleteLater() self.view.deleteLater() del self.scene del self.view super().tearDown()
def test_tool_tips(self): scene = GraphicsScene() view = QGraphicsView(scene) w = TextListWidget() text = "A" * 10 w.setItems([text, text]) scene.addItem(w) view.grab() # ensure w is laid out wrect = view.mapFromScene(w.mapToScene( w.contentsRect())).boundingRect() p = QPoint(wrect.topLeft() + QPoint(5, 5)) ev = QHelpEvent(QHelpEvent.ToolTip, p, view.viewport().mapToGlobal(p)) try: QApplication.sendEvent(view.viewport(), ev) self.assertEqual(QToolTip.text(), text) finally: QToolTip.hideText()
class TestItems(QAppTestCase): def setUp(self): super().setUp() self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints( QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing ) self.view.resize(500, 300) self.view.show() def tearDown(self): self.scene.clear() self.scene.deleteLater() self.view.deleteLater() del self.scene del self.view super().tearDown()
class TestItems(unittest.TestCase): def setUp(self): import logging from AnyQt.QtWidgets import \ QApplication, QGraphicsScene, QGraphicsView from AnyQt.QtGui import QPainter from AnyQt.QtCore import QTimer logging.basicConfig() self.app = QApplication([]) self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints( QPainter.Antialiasing | \ QPainter.SmoothPixmapTransform | \ QPainter.TextAntialiasing ) self.view.resize(500, 300) self.view.show() QTimer.singleShot(10000, self.app.exit) def my_excepthook(*args): sys.setrecursionlimit(1010) traceback.print_exc(limit=4) self._orig_excepthook = sys.excepthook sys.excepthook = my_excepthook self.singleShot = QTimer.singleShot def tearDown(self): self.scene.clear() self.scene.deleteLater() self.view.deleteLater() del self.scene del self.view self.app.processEvents() del self.app sys.excepthook = self._orig_excepthook
def __init__(self): super().__init__() self.dataset = None self.attrs = DomainModel( valid_types=Orange.data.DiscreteVariable, separators=False) cb = gui.comboBox( self.controlArea, self, "attribute", box=True, model=self.attrs, callback=self.update_scene, contentsLength=12) grid = QGridLayout() self.legend = gui.widgetBox(gui.indentedBox(cb.box), orientation=grid) grid.setColumnStretch(1, 1) grid.setHorizontalSpacing(6) self.legend_items = [] self.split_vars = DomainModel( valid_types=Orange.data.DiscreteVariable, separators=False, placeholder="None", ) gui.comboBox( self.controlArea, self, "split_var", box="Split by", model=self.split_vars, callback=self.update_scene) gui.checkBox( self.controlArea, self, "explode", "Explode pies", box=True, callback=self.update_scene) gui.rubber(self.controlArea) gui.widgetLabel( gui.hBox(self.controlArea, box=True), "The aim of this widget is to\n" "demonstrate that pie charts are\n" "a terrible visualization. Please\n" "don't use it for any other purpose.") self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHints( QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.mainArea.layout().addWidget(self.view) self.mainArea.setMinimumWidth(600)
def __updateView(self, view: QGraphicsView, rect: QRectF) -> None: view.setSceneRect(rect) viewrect = view.mapFromScene(rect).boundingRect() view.setFixedHeight(int(math.ceil(viewrect.height()))) container = view.parent() if rect.isEmpty(): container.setVisible(False) return # map the rect to (main) viewport coordinates viewrect = qgraphicsview_map_rect_from_scene(self, rect).boundingRect() viewrect = qrectf_to_inscribed_rect(viewrect) viewportrect = self.viewport().rect() visible = (viewrect.top() < viewportrect.top() or viewrect.y() + viewrect.height() > viewportrect.y() + viewportrect.height()) container.setVisible(visible) # force immediate layout of the container overlay QCoreApplication.sendEvent(container, QEvent(QEvent.LayoutRequest))
def test_editlinksnode(self): reg = small_testing_registry() one_desc = reg.widget("one") negate_desc = reg.widget("negate") source_node = SchemeNode(one_desc, title="This is 1") sink_node = SchemeNode(negate_desc) scene = QGraphicsScene() view = QGraphicsView(scene) node = EditLinksNode(node=source_node) scene.addItem(node) node = EditLinksNode(direction=Qt.RightToLeft) node.setSchemeNode(sink_node) node.setPos(300, 0) scene.addItem(node) view.show() view.resize(800, 300) self.app.exec_()
class OWSilhouettePlot(widget.OWWidget): name = "Silhouette Plot" description = "Visually assess cluster quality and " \ "the degree of cluster membership." icon = "icons/SilhouettePlot.svg" priority = 300 keywords = [] class Inputs: data = Input("Data", 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) replaces = [ "orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot", "Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot" ] settingsHandler = settings.PerfectDomainContextHandler() #: Distance metric index distance_idx = settings.Setting(0) #: Group/cluster variable index cluster_var_idx = settings.ContextSetting(0) #: Annotation variable index annotation_var_idx = settings.ContextSetting(0) #: Group the (displayed) silhouettes by cluster group_by_cluster = settings.Setting(True) #: A fixed size for an instance bar bar_size = settings.Setting(3) #: Add silhouette scores to output data add_scores = settings.Setting(False) auto_commit = settings.Setting(True) Distances = [("Euclidean", Orange.distance.Euclidean), ("Manhattan", Orange.distance.Manhattan), ("Cosine", Orange.distance.Cosine)] graph_name = "scene" buttons_area_orientation = Qt.Vertical class Error(widget.OWWidget.Error): need_two_clusters = Msg("Need at least two non-empty clusters") singleton_clusters_all = Msg("All clusters are singletons") memory_error = Msg("Not enough memory") value_error = Msg("Distances could not be computed: '{}'") class Warning(widget.OWWidget.Warning): missing_cluster_assignment = Msg( "{} instance{s} omitted (missing cluster assignment)") nan_distances = Msg("{} instance{s} omitted (undefined distances)") ignoring_categorical = Msg("Ignoring categorical features") def __init__(self): super().__init__() #: The input data self.data = None # type: Optional[Orange.data.Table] #: Distance matrix computed from data self._matrix = None # type: Optional[Orange.misc.DistMatrix] #: An bool mask (size == len(data)) indicating missing group/cluster #: assignments self._mask = None # type: Optional[np.ndarray] #: An array of cluster/group labels for instances with valid group #: assignment self._labels = None # type: Optional[np.ndarray] #: An array of silhouette scores for instances with valid group #: assignment self._silhouette = None # type: Optional[np.ndarray] self._silplot = None # type: Optional[SilhouettePlot] gui.comboBox( self.controlArea, self, "distance_idx", box="Distance", items=[name for name, _ in OWSilhouettePlot.Distances], orientation=Qt.Horizontal, callback=self._invalidate_distances) box = gui.vBox(self.controlArea, "Cluster Label") self.cluster_var_cb = gui.comboBox( box, self, "cluster_var_idx", contentsLength=14, addSpace=4, callback=self._invalidate_scores ) gui.checkBox( box, self, "group_by_cluster", "Group by cluster", callback=self._replot) self.cluster_var_model = itemmodels.VariableListModel(parent=self) self.cluster_var_cb.setModel(self.cluster_var_model) box = gui.vBox(self.controlArea, "Bars") gui.widgetLabel(box, "Bar width:") gui.hSlider( box, self, "bar_size", minValue=1, maxValue=10, step=1, callback=self._update_bar_size, addSpace=6) gui.widgetLabel(box, "Annotations:") self.annotation_cb = gui.comboBox( box, self, "annotation_var_idx", contentsLength=14, callback=self._update_annotations) self.annotation_var_model = itemmodels.VariableListModel(parent=self) self.annotation_var_model[:] = ["None"] self.annotation_cb.setModel(self.annotation_var_model) ibox = gui.indentedBox(box, 5) self.ann_hidden_warning = warning = gui.widgetLabel( ibox, "(increase the width to show)") ibox.setFixedWidth(ibox.sizeHint().width()) warning.setVisible(False) gui.rubber(self.controlArea) gui.separator(self.buttonsArea) box = gui.vBox(self.buttonsArea, "Output") # Thunk the call to commit to call conditional commit gui.checkBox(box, self, "add_scores", "Add silhouette scores", callback=lambda: self.commit()) gui.auto_commit( box, self, "auto_commit", "Commit", auto_label="Auto commit", box=False) # Ensure that the controlArea is not narrower than buttonsArea self.controlArea.layout().addWidget(self.buttonsArea) self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing, True) self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.mainArea.layout().addWidget(self.view) def sizeHint(self): sh = self.controlArea.sizeHint() return sh.expandedTo(QSize(600, 720)) @Inputs.data @check_sql_input def set_data(self, data): """ Set the input dataset. """ self.closeContext() self.clear() error_msg = "" warning_msg = "" candidatevars = [] if data is not None: candidatevars = [ v for v in data.domain.variables + data.domain.metas if v.is_discrete and len(v.values) >= 2] if not candidatevars: error_msg = "Input does not have any suitable labels." data = None self.data = data if data is not None: self.cluster_var_model[:] = candidatevars if data.domain.class_var in candidatevars: self.cluster_var_idx = \ candidatevars.index(data.domain.class_var) else: self.cluster_var_idx = 0 annotvars = [var for var in data.domain.metas if var.is_string] self.annotation_var_model[:] = ["None"] + annotvars self.annotation_var_idx = 1 if len(annotvars) else 0 self.openContext(Orange.data.Domain(candidatevars)) self.error(error_msg) self.warning(warning_msg) def handleNewSignals(self): if self.data is not None: self._update() self._replot() self.unconditional_commit() def clear(self): """ Clear the widget state. """ self.data = None self._matrix = None self._mask = None self._silhouette = None self._labels = None self.cluster_var_model[:] = [] self.annotation_var_model[:] = ["None"] self._clear_scene() self.Error.clear() self.Warning.clear() def _clear_scene(self): # Clear the graphics scene and associated objects self.scene.clear() self.scene.setSceneRect(QRectF()) self._silplot = None def _invalidate_distances(self): # Invalidate the computed distance matrix and recompute the silhouette. self._matrix = None self._invalidate_scores() def _invalidate_scores(self): # Invalidate and recompute the current silhouette scores. self._labels = self._silhouette = self._mask = None self._update() self._replot() if self.data is not None: self.commit() def _update(self): # Update/recompute the distances/scores as required self._clear_messages() if self.data is None or not len(self.data): self._reset_all() return if self._matrix is None and self.data is not None: _, metric = self.Distances[self.distance_idx] data = self.data if not metric.supports_discrete and any( a.is_discrete for a in data.domain.attributes): self.Warning.ignoring_categorical() data = Orange.distance.remove_discrete_features(data) try: self._matrix = np.asarray(metric(data)) except MemoryError: self.Error.memory_error() return except ValueError as err: self.Error.value_error(str(err)) return self._update_labels() def _reset_all(self): self._mask = None self._silhouette = None self._labels = None self._matrix = None self._clear_scene() def _clear_messages(self): self.Error.clear() self.Warning.clear() def _update_labels(self): labelvar = self.cluster_var_model[self.cluster_var_idx] labels, _ = self.data.get_column_view(labelvar) labels = np.asarray(labels, dtype=float) cluster_mask = np.isnan(labels) dist_mask = np.isnan(self._matrix).all(axis=0) mask = cluster_mask | dist_mask labels = labels.astype(int) labels = labels[~mask] labels_unq, _ = np.unique(labels, return_counts=True) if len(labels_unq) < 2: self.Error.need_two_clusters() labels = silhouette = mask = None elif len(labels_unq) == len(labels): self.Error.singleton_clusters_all() labels = silhouette = mask = None else: silhouette = sklearn.metrics.silhouette_samples( self._matrix[~mask, :][:, ~mask], labels, metric="precomputed") self._mask = mask self._labels = labels self._silhouette = silhouette if mask is not None: count_missing = np.count_nonzero(cluster_mask) if count_missing: self.Warning.missing_cluster_assignment( count_missing, s="s" if count_missing > 1 else "") count_nandist = np.count_nonzero(dist_mask) if count_nandist: self.Warning.nan_distances( count_nandist, s="s" if count_nandist > 1 else "") def _set_bar_height(self): visible = self.bar_size >= 5 self._silplot.setBarHeight(self.bar_size) self._silplot.setRowNamesVisible(visible) self.ann_hidden_warning.setVisible( not visible and self.annotation_var_idx > 0) def _replot(self): # Clear and replot/initialize the scene self._clear_scene() if self._silhouette is not None and self._labels is not None: var = self.cluster_var_model[self.cluster_var_idx] self._silplot = silplot = SilhouettePlot() self._set_bar_height() if self.group_by_cluster: silplot.setScores(self._silhouette, self._labels, var.values, var.colors) else: silplot.setScores( self._silhouette, np.zeros(len(self._silhouette), dtype=int), [""], np.array([[63, 207, 207]]) ) self.scene.addItem(silplot) self._update_annotations() silplot.selectionChanged.connect(self.commit) silplot.layout().activate() self._update_scene_rect() silplot.geometryChanged.connect(self._update_scene_rect) def _update_bar_size(self): if self._silplot is not None: self._set_bar_height() def _update_annotations(self): if 0 < self.annotation_var_idx < len(self.annotation_var_model): annot_var = self.annotation_var_model[self.annotation_var_idx] else: annot_var = None self.ann_hidden_warning.setVisible( self.bar_size < 5 and annot_var is not None) if self._silplot is not None: if annot_var is not None: column, _ = self.data.get_column_view(annot_var) if self._mask is not None: assert column.shape == self._mask.shape # pylint: disable=invalid-unary-operand-type column = column[~self._mask] self._silplot.setRowNames( [annot_var.str_val(value) for value in column]) else: self._silplot.setRowNames(None) def _update_scene_rect(self): self.scene.setSceneRect(self._silplot.geometry()) def commit(self): """ Commit/send the current selection to the output. """ selected = indices = data = None if self.data is not None: selectedmask = np.full(len(self.data), False, dtype=bool) if self._silplot is not None: indices = self._silplot.selection() assert (np.diff(indices) > 0).all(), "strictly increasing" if self._mask is not None: # pylint: disable=invalid-unary-operand-type indices = np.flatnonzero(~self._mask)[indices] selectedmask[indices] = True if self._mask is not None: scores = np.full(shape=selectedmask.shape, fill_value=np.nan) # pylint: disable=invalid-unary-operand-type scores[~self._mask] = self._silhouette else: scores = self._silhouette silhouette_var = None if self.add_scores: var = self.cluster_var_model[self.cluster_var_idx] silhouette_var = Orange.data.ContinuousVariable( "Silhouette ({})".format(escape(var.name))) domain = Orange.data.Domain( self.data.domain.attributes, self.data.domain.class_vars, self.data.domain.metas + (silhouette_var, )) data = self.data.transform(domain) else: domain = self.data.domain data = self.data if np.count_nonzero(selectedmask): selected = self.data.from_table( domain, self.data, np.flatnonzero(selectedmask)) if self.add_scores: if selected is not None: selected[:, silhouette_var] = np.c_[scores[selectedmask]] data[:, silhouette_var] = np.c_[scores] self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(create_annotated_table(data, indices)) def send_report(self): if not len(self.cluster_var_model): return self.report_plot() caption = "Silhouette plot ({} distance), clustered by '{}'".format( self.Distances[self.distance_idx][0], self.cluster_var_model[self.cluster_var_idx]) if self.annotation_var_idx and self._silplot.rowNamesVisible(): caption += ", annotated with '{}'".format( self.annotation_var_model[self.annotation_var_idx]) self.report_caption(caption) def onDeleteWidget(self): self.clear() super().onDeleteWidget()
def __init__(self): super().__init__() # Instance variables self.forest_type = self.CLASSIFICATION self.model = None self.forest_adapter = None self.dataset = None self.clf_dataset = None # We need to store refernces to the trees and grid items self.grid_items, self.ptrees = [], [] self.color_palette = None # Different methods to calculate the size of squares self.SIZE_CALCULATION = [ ('Normal', lambda x: x), ('Square root', lambda x: sqrt(x)), ('Logarithmic', lambda x: log(x * self.size_log_scale)), ] self.REGRESSION_COLOR_CALC = [ ('None', lambda _, __: QColor(255, 255, 255)), ('Class mean', self._color_class_mean), ('Standard deviation', self._color_stddev), ] # CONTROL AREA # Tree info area box_info = gui.widgetBox(self.controlArea, 'Forest') self.ui_info = gui.widgetLabel(box_info, label='') # Display controls area box_display = gui.widgetBox(self.controlArea, 'Display') self.ui_depth_slider = gui.hSlider( box_display, self, 'depth_limit', label='Depth', ticks=False, callback=self.max_depth_changed) self.ui_target_class_combo = gui.comboBox( box_display, self, 'target_class_index', label='Target class', orientation=Qt.Horizontal, items=[], contentsLength=8, callback=self.target_colors_changed) self.ui_size_calc_combo = gui.comboBox( box_display, self, 'size_calc_idx', label='Size', orientation=Qt.Horizontal, items=list(zip(*self.SIZE_CALCULATION))[0], contentsLength=8, callback=self.size_calc_changed) self.ui_zoom_slider = gui.hSlider( box_display, self, 'zoom', label='Zoom', ticks=False, minValue=20, maxValue=150, callback=self.zoom_changed, createLabel=False) # Stretch to fit the rest of the unsused area gui.rubber(self.controlArea) self.controlArea.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) # MAIN AREA self.scene = QGraphicsScene(self) self.scene.selectionChanged.connect(self.commit) self.grid = OWGrid() self.grid.geometryChanged.connect(self._update_scene_rect) self.scene.addItem(self.grid) self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing, True) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.mainArea.layout().addWidget(self.view) self.resize(800, 500) self.clear()
class OWPythagoreanForest(OWWidget): name = 'Pythagorean Forest' description = 'Pythagorean forest for visualising random forests.' icon = 'icons/PythagoreanForest.svg' priority = 1001 inputs = [('Random forest', RandomForestModel, 'set_rf')] outputs = [('Tree', TreeModel)] # Enable the save as feature graph_name = 'scene' # Settings depth_limit = settings.ContextSetting(10) target_class_index = settings.ContextSetting(0) size_calc_idx = settings.Setting(0) size_log_scale = settings.Setting(2) zoom = settings.Setting(50) selected_tree_index = settings.ContextSetting(-1) CLASSIFICATION, REGRESSION = range(2) def __init__(self): super().__init__() # Instance variables self.forest_type = self.CLASSIFICATION self.model = None self.forest_adapter = None self.dataset = None self.clf_dataset = None # We need to store refernces to the trees and grid items self.grid_items, self.ptrees = [], [] self.color_palette = None # Different methods to calculate the size of squares self.SIZE_CALCULATION = [ ('Normal', lambda x: x), ('Square root', lambda x: sqrt(x)), ('Logarithmic', lambda x: log(x * self.size_log_scale)), ] self.REGRESSION_COLOR_CALC = [ ('None', lambda _, __: QColor(255, 255, 255)), ('Class mean', self._color_class_mean), ('Standard deviation', self._color_stddev), ] # CONTROL AREA # Tree info area box_info = gui.widgetBox(self.controlArea, 'Forest') self.ui_info = gui.widgetLabel(box_info, label='') # Display controls area box_display = gui.widgetBox(self.controlArea, 'Display') self.ui_depth_slider = gui.hSlider( box_display, self, 'depth_limit', label='Depth', ticks=False, callback=self.max_depth_changed) self.ui_target_class_combo = gui.comboBox( box_display, self, 'target_class_index', label='Target class', orientation=Qt.Horizontal, items=[], contentsLength=8, callback=self.target_colors_changed) self.ui_size_calc_combo = gui.comboBox( box_display, self, 'size_calc_idx', label='Size', orientation=Qt.Horizontal, items=list(zip(*self.SIZE_CALCULATION))[0], contentsLength=8, callback=self.size_calc_changed) self.ui_zoom_slider = gui.hSlider( box_display, self, 'zoom', label='Zoom', ticks=False, minValue=20, maxValue=150, callback=self.zoom_changed, createLabel=False) # Stretch to fit the rest of the unsused area gui.rubber(self.controlArea) self.controlArea.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) # MAIN AREA self.scene = QGraphicsScene(self) self.scene.selectionChanged.connect(self.commit) self.grid = OWGrid() self.grid.geometryChanged.connect(self._update_scene_rect) self.scene.addItem(self.grid) self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing, True) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.mainArea.layout().addWidget(self.view) self.resize(800, 500) self.clear() def set_rf(self, model=None): """When a different forest is given.""" self.clear() self.model = model if model is not None: if isinstance(model, RandomForestClassifier): self.forest_type = self.CLASSIFICATION elif isinstance(model, RandomForestRegressor): self.forest_type = self.REGRESSION else: raise RuntimeError('Invalid type of forest.') self.forest_adapter = self._get_forest_adapter(self.model) self.color_palette = self._type_specific('_get_color_palette')() self._draw_trees() self.dataset = model.instances # this bit is important for the regression classifier if self.dataset is not None and \ self.dataset.domain != model.domain: self.clf_dataset = Table.from_table( self.model.domain, self.dataset) else: self.clf_dataset = self.dataset self._update_info_box() self._type_specific('_update_target_class_combo')() self._update_depth_slider() self.selected_tree_index = -1 def clear(self): """Clear all relevant data from the widget.""" self.model = None self.forest_adapter = None self.ptrees = [] self.grid_items = [] self.grid.clear() self._clear_info_box() self._clear_target_class_combo() self._clear_depth_slider() # CONTROL AREA CALLBACKS def max_depth_changed(self): """When the max depth slider is changed.""" for tree in self.ptrees: tree.set_depth_limit(self.depth_limit) def target_colors_changed(self): """When the target class or coloring method is changed.""" for tree in self.ptrees: tree.target_class_has_changed() def size_calc_changed(self): """When the size calculation of the trees is changed.""" if self.model is not None: self.forest_adapter = self._get_forest_adapter(self.model) self.grid.clear() self._draw_trees() # Keep the selected item if self.selected_tree_index != -1: self.grid_items[self.selected_tree_index].setSelected(True) self.max_depth_changed() def zoom_changed(self): """When we update the "Zoom" slider.""" for item in self.grid_items: item.set_max_size(self._calculate_zoom(self.zoom)) width = (self.view.width() - self.view.verticalScrollBar().width()) self.grid.reflow(width) self.grid.setPreferredWidth(width) # MODEL CHANGED METHODS def _update_info_box(self): self.ui_info.setText( 'Trees: {}'.format(len(self.forest_adapter.get_trees())) ) def _update_depth_slider(self): self.depth_limit = self._get_max_depth() self.ui_depth_slider.parent().setEnabled(True) self.ui_depth_slider.setMaximum(self.depth_limit) self.ui_depth_slider.setValue(self.depth_limit) # MODEL CLEARED METHODS def _clear_info_box(self): self.ui_info.setText('No forest on input.') def _clear_target_class_combo(self): self.ui_target_class_combo.clear() self.target_class_index = 0 self.ui_target_class_combo.setCurrentIndex(self.target_class_index) def _clear_depth_slider(self): self.ui_depth_slider.parent().setEnabled(False) self.ui_depth_slider.setMaximum(0) # HELPFUL METHODS def _get_max_depth(self): return max([tree.tree_adapter.max_depth for tree in self.ptrees]) def _get_forest_adapter(self, model): return SklRandomForestAdapter(model) def _draw_trees(self): self.ui_size_calc_combo.setEnabled(False) self.grid_items, self.ptrees = [], [] with self.progressBar(len(self.forest_adapter.get_trees())) as prg: for tree in self.forest_adapter.get_trees(): ptree = PythagorasTreeViewer( None, tree, node_color_func=self._type_specific('_get_node_color'), interactive=False, padding=100) self.grid_items.append(GridItem( ptree, self.grid, max_size=self._calculate_zoom(self.zoom) )) self.ptrees.append(ptree) prg.advance() self.grid.set_items(self.grid_items) # This is necessary when adding items for the first time if self.grid: width = (self.view.width() - self.view.verticalScrollBar().width()) self.grid.reflow(width) self.grid.setPreferredWidth(width) self.ui_size_calc_combo.setEnabled(True) @staticmethod def _calculate_zoom(zoom_level): """Calculate the max size for grid items from zoom level setting.""" return zoom_level * 5 def onDeleteWidget(self): """When deleting the widget.""" super().onDeleteWidget() self.clear() def commit(self): """Commit the selected tree to output.""" if len(self.scene.selectedItems()) == 0: self.send('Tree', None) # The selected tree index should only reset when model changes if self.model is None: self.selected_tree_index = -1 return selected_item = self.scene.selectedItems()[0] self.selected_tree_index = self.grid_items.index(selected_item) obj = self.model.trees[self.selected_tree_index] obj.instances = self.dataset obj.meta_target_class_index = self.target_class_index obj.meta_size_calc_idx = self.size_calc_idx obj.meta_size_log_scale = self.size_log_scale obj.meta_depth_limit = self.depth_limit self.send('Tree', obj) def send_report(self): """Send report.""" self.report_plot() def _update_scene_rect(self): self.scene.setSceneRect(self.scene.itemsBoundingRect()) def resizeEvent(self, ev): width = (self.view.width() - self.view.verticalScrollBar().width()) self.grid.reflow(width) self.grid.setPreferredWidth(width) super().resizeEvent(ev) def _type_specific(self, method): """A best effort method getter that somewhat separates logic specific to classification and regression trees. This relies on conventional naming of specific methods, e.g. a method name _get_tooltip would need to be defined like so: _classification_get_tooltip and _regression_get_tooltip, since they are both specific. Parameters ---------- method : str Method name that we would like to call. Returns ------- callable or None """ if self.forest_type == self.CLASSIFICATION: return getattr(self, '_classification' + method) elif self.forest_type == self.REGRESSION: return getattr(self, '_regression' + method) else: return None # CLASSIFICATION FOREST SPECIFIC METHODS def _classification_update_target_class_combo(self): self._clear_target_class_combo() self.ui_target_class_combo.addItem('None') values = [c.title() for c in self.model.domain.class_vars[0].values] self.ui_target_class_combo.addItems(values) def _classification_get_color_palette(self): return [QColor(*c) for c in self.model.domain.class_var.colors] def _classification_get_node_color(self, adapter, tree_node): # this is taken almost directly from the existing classification tree # viewer colors = self.color_palette distribution = adapter.get_distribution(tree_node.label)[0] total = np.sum(distribution) if self.target_class_index: p = distribution[self.target_class_index - 1] / total color = colors[self.target_class_index - 1].lighter(200 - 100 * p) else: modus = np.argmax(distribution) p = distribution[modus] / (total or 1) color = colors[int(modus)].lighter(400 - 300 * p) return color # REGRESSION FOREST SPECIFIC METHODS def _regression_update_target_class_combo(self): self._clear_target_class_combo() self.ui_target_class_combo.addItems( list(zip(*self.REGRESSION_COLOR_CALC))[0]) self.ui_target_class_combo.setCurrentIndex(self.target_class_index) def _regression_get_color_palette(self): return ContinuousPaletteGenerator( *self.forest_adapter.domain.class_var.colors) def _regression_get_node_color(self, adapter, tree_node): return self.REGRESSION_COLOR_CALC[self.target_class_index][1]( adapter, tree_node ) def _color_class_mean(self, adapter, tree_node): # calculate node colors relative to the mean of the node samples min_mean = np.min(self.clf_dataset.Y) max_mean = np.max(self.clf_dataset.Y) instances = adapter.get_instances_in_nodes(self.clf_dataset, tree_node.label) mean = np.mean(instances.Y) return self.color_palette[(mean - min_mean) / (max_mean - min_mean)] def _color_stddev(self, adapter, tree_node): # calculate node colors relative to the standard deviation in the node # samples min_mean, max_mean = 0, np.std(self.clf_dataset.Y) instances = adapter.get_instances_in_nodes(self.clf_dataset, tree_node.label) std = np.std(instances.Y) return self.color_palette[(std - min_mean) / (max_mean - min_mean)]
class OWBoxPlot(widget.OWWidget): """ Here's how the widget's functions call each other: - `set_data` is a signal handler fills the list boxes and calls `grouping_changed`. - `grouping_changed` handles changes of grouping attribute: it enables or disables the box for ordering, orders attributes and calls `attr_changed`. - `attr_changed` handles changes of attribute. It recomputes box data by calling `compute_box_data`, shows the appropriate display box (discrete/continuous) and then calls`layout_changed` - `layout_changed` constructs all the elements for the scene (as lists of QGraphicsItemGroup) and calls `display_changed`. It is called when the attribute or grouping is changed (by attr_changed) and on resize event. - `display_changed` puts the elements corresponding to the current display settings on the scene. It is called when the elements are reconstructed (layout is changed due to selection of attributes or resize event), or when the user changes display settings or colors. For discrete attributes, the flow is a bit simpler: the elements are not constructed in advance (by layout_changed). Instead, layout_changed and display_changed call display_changed_disc that draws everything. """ name = "Box Plot" description = "Visualize the distribution of feature values in a box plot." icon = "icons/BoxPlot.svg" priority = 100 keywords = ["whisker"] class Inputs: data = Input("Data", 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) #: Comparison types for continuous variables CompareNone, CompareMedians, CompareMeans = 0, 1, 2 settingsHandler = DomainContextHandler() conditions = ContextSetting([]) attribute = ContextSetting(None) order_by_importance = Setting(False) group_var = ContextSetting(None) show_annotations = Setting(True) compare = Setting(CompareMeans) stattest = Setting(0) sig_threshold = Setting(0.05) stretched = Setting(True) show_labels = Setting(True) sort_freqs = Setting(False) auto_commit = Setting(True) _sorting_criteria_attrs = { CompareNone: "", CompareMedians: "median", CompareMeans: "mean" } _pen_axis_tick = QPen(Qt.white, 5) _pen_axis = QPen(Qt.darkGray, 3) _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2) _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2) _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1) _pen_dotted.setStyle(Qt.DotLine) _post_line_pen = QPen(Qt.lightGray, 2) _post_grp_pen = QPen(Qt.lightGray, 4) for pen in (_pen_paramet, _pen_median, _pen_dotted, _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen): pen.setCosmetic(True) pen.setCapStyle(Qt.RoundCap) pen.setJoinStyle(Qt.RoundJoin) _pen_axis_tick.setCapStyle(Qt.FlatCap) _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0)) _axis_font = QFont() _axis_font.setPixelSize(12) _label_font = QFont() _label_font.setPixelSize(11) _attr_brush = QBrush(QColor(0x33, 0x00, 0xff)) graph_name = "box_scene" def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.scale_x = self.scene_min_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() view = gui.listView( self.controlArea, self, "attribute", box="Variable", model=self.attrs, callback=self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) gui.separator(view.box, 6, 6) self.cb_order = gui.checkBox( view.box, self, "order_by_importance", "Order by relevance", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_sorting) self.group_vars = DomainModel( placeholder="None", separators=False, valid_types=Orange.data.DiscreteVariable) self.group_view = view = gui.listView( self.controlArea, self, "group_var", box="Subgroups", model=self.group_vars, callback=self.grouping_changed) view.setEnabled(False) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox( self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum), addSpace=False) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.layout_changed) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox( self.controlArea, box="Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox( box, self, 'stretched', "Stretch bars", callback=self.display_changed) gui.checkBox( box, self, 'show_labels', "Show box labels", callback=self.display_changed) self.sort_cb = gui.checkBox( box, self, 'sort_freqs', "Sort by subgroup frequencies", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_scene.selectionChanged.connect(self.commit) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>No test results.</center>") self.mainArea.setMinimumWidth(300) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box() def sizeHint(self): return QSize(900, 500) def eventFilter(self, obj, event): if obj is self.box_view.viewport() and \ event.type() == QEvent.Resize: self.layout_changed() return super().eventFilter(obj, event) def reset_attrs(self, domain): self.attrs[:] = [ var for var in chain( domain.class_vars, domain.metas, domain.attributes) if var.is_primitive()] # noinspection PyTypeChecker @Inputs.data def set_data(self, dataset): if dataset is not None and ( not bool(dataset) or not len(dataset.domain) and not any(var.is_primitive() for var in dataset.domain.metas)): dataset = None self.closeContext() self.dataset = dataset self.dist = self.stats = self.conts = [] self.group_var = None self.attribute = None if dataset: domain = dataset.domain self.group_vars.set_domain(domain) self.group_view.setEnabled(len(self.group_vars) > 1) self.reset_attrs(domain) self.select_default_variables(domain) self.openContext(self.dataset) self.grouping_changed() else: self.reset_all_data() self.commit() def select_default_variables(self, domain): # visualize first non-class variable, group by class (if present) if len(self.attrs) > len(domain.class_vars): self.attribute = self.attrs[len(domain.class_vars)] elif self.attrs: self.attribute = self.attrs[0] if domain.class_var and domain.class_var.is_discrete: self.group_var = domain.class_var else: self.group_var = None # Reset to trigger selection via callback def apply_sorting(self): def compute_score(attr): if attr is group_var: return 3 if attr.is_continuous: # One-way ANOVA col = data.get_column_view(attr)[0].astype(float) groups = (col[group_col == i] for i in range(n_groups)) groups = (col[~np.isnan(col)] for col in groups) groups = [group for group in groups if len(group)] p = f_oneway(*groups)[1] if len(groups) > 1 else 2 else: # Chi-square with the given distribution into groups # (see degrees of freedom in computation of the p-value) if not attr.values or not group_var.values: return 2 observed = np.array( contingency.get_contingency(data, group_var, attr)) observed = observed[observed.sum(axis=1) != 0, :] observed = observed[:, observed.sum(axis=0) != 0] if min(observed.shape) < 2: return 2 expected = \ np.outer(observed.sum(axis=1), observed.sum(axis=0)) / \ np.sum(observed) p = chisquare(observed.ravel(), f_exp=expected.ravel(), ddof=n_groups - 1)[1] if math.isnan(p): return 2 return p data = self.dataset if data is None: return domain = data.domain attribute = self.attribute group_var = self.group_var if self.order_by_importance and group_var is not None: n_groups = len(group_var.values) group_col = data.get_column_view(group_var)[0] if \ domain.has_continuous_attributes( include_class=True, include_metas=True) else None self.attrs.sort(key=compute_score) else: self.reset_attrs(domain) self.attribute = attribute def reset_all_data(self): self.clear_scene() self.infot1.setText("") self.attrs.clear() self.group_vars.set_domain(None) self.group_view.setEnabled(False) self.is_continuous = False self.update_display_box() def grouping_changed(self): self.cb_order.setEnabled(self.group_var is not None) self.apply_sorting() self.attr_changed() def select_box_items(self): temp_cond = self.conditions.copy() for box in self.box_scene.items(): if isinstance(box, FilterGraphicsRectItem): box.setSelected(box.filter.conditions in [c.conditions for c in temp_cond]) def attr_changed(self): self.compute_box_data() self.update_display_box() self.layout_changed() if self.is_continuous: heights = 90 if self.show_annotations else 60 self.box_view.centerOn(self.scene_min_x + self.scene_width / 2, -30 - len(self.stats) * heights / 2 + 45) else: self.box_view.centerOn(self.scene_width / 2, -30 - len(self.boxes) * 40 / 2 + 45) def compute_box_data(self): attr = self.attribute if not attr: return dataset = self.dataset self.is_continuous = attr.is_continuous if dataset is None or not self.is_continuous and not attr.values or \ self.group_var and not self.group_var.values: self.stats = self.dist = self.conts = [] return if self.group_var: self.dist = [] self.conts = contingency.get_contingency( dataset, attr, self.group_var) if self.is_continuous: stats, label_texts = [], [] for i, cont in enumerate(self.conts): if np.sum(cont[1]): stats.append(BoxData(cont, attr, i, self.group_var)) label_texts.append(self.group_var.values[i]) self.stats = stats self.label_txts_all = label_texts else: self.label_txts_all = \ [v for v, c in zip(self.group_var.values, self.conts) if np.sum(c) > 0] else: self.dist = distribution.get_distribution(dataset, attr) self.conts = [] if self.is_continuous: self.stats = [BoxData(self.dist, attr, None)] self.label_txts_all = [""] self.label_txts = [txts for stat, txts in zip(self.stats, self.label_txts_all) if stat.n > 0] self.stats = [stat for stat in self.stats if stat.n > 0] def update_display_box(self): if self.is_continuous: self.stretching_box.hide() self.display_box.show() self.compare_rb.setEnabled(self.group_var is not None) else: self.stretching_box.show() self.display_box.hide() self.sort_cb.setEnabled(self.group_var is not None) def clear_scene(self): self.closeContext() self.box_scene.clearSelection() self.box_scene.clear() self.box_view.viewport().update() self.attr_labels = [] self.labels = [] self.boxes = [] self.mean_labels = [] self.posthoc_lines = [] self.openContext(self.dataset) def layout_changed(self): attr = self.attribute if not attr: return self.clear_scene() if self.dataset is None or len(self.conts) == len(self.dist) == 0: return if not self.is_continuous: self.display_changed_disc() return self.mean_labels = [self.mean_label(stat, attr, lab) for stat, lab in zip(self.stats, self.label_txts)] self.draw_axis() self.boxes = [self.box_group(stat) for stat in self.stats] self.labels = [self.label_group(stat, attr, mean_lab) for stat, mean_lab in zip(self.stats, self.mean_labels)] self.attr_labels = [QGraphicsSimpleTextItem(lab) for lab in self.label_txts] for it in chain(self.labels, self.attr_labels): self.box_scene.addItem(it) self.display_changed() def display_changed(self): if self.dataset is None: return if not self.is_continuous: self.display_changed_disc() return self.order = list(range(len(self.stats))) criterion = self._sorting_criteria_attrs[self.compare] if criterion: vals = [getattr(stat, criterion) for stat in self.stats] overmax = max((val for val in vals if val is not None), default=0) \ + 1 vals = [val if val is not None else overmax for val in vals] self.order = sorted(self.order, key=vals.__getitem__) heights = 90 if self.show_annotations else 60 for row, box_index in enumerate(self.order): y = (-len(self.stats) + row) * heights + 10 for item in self.boxes[box_index]: self.box_scene.addItem(item) item.setY(y) labels = self.labels[box_index] if self.show_annotations: labels.show() labels.setY(y) else: labels.hide() label = self.attr_labels[box_index] label.setY(y - 15 - label.boundingRect().height()) if self.show_annotations: label.hide() else: stat = self.stats[box_index] if self.compare == OWBoxPlot.CompareMedians and \ stat.median is not None: pos = stat.median + 5 / self.scale_x elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None: pos = stat.mean + 5 / self.scale_x else: pos = stat.q25 label.setX(pos * self.scale_x) label.show() r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights, self.scene_width, len(self.stats) * heights + 90) self.box_scene.setSceneRect(r) self.compute_tests() self.show_posthoc() self.select_box_items() def display_changed_disc(self): assert not self.is_continuous self.clear_scene() self.attr_labels = [QGraphicsSimpleTextItem(lab) for lab in self.label_txts_all] if not self.stretched: if self.group_var: self.labels = [ QGraphicsTextItem("{}".format(int(sum(cont)))) for cont in self.conts if np.sum(cont) > 0] else: self.labels = [ QGraphicsTextItem(str(int(sum(self.dist))))] self.order = list(range(len(self.attr_labels))) self.draw_axis_disc() if self.group_var: self.boxes = \ [self.strudel(cont, i) for i, cont in enumerate(self.conts) if np.sum(cont) > 0] self.conts = self.conts[np.sum(np.array(self.conts), axis=1) > 0] if self.sort_freqs: # pylint: disable=invalid-unary-operand-type self.order = sorted(self.order, key=(-np.sum(self.conts, axis=1)).__getitem__) else: self.boxes = [self.strudel(self.dist)] for row, box_index in enumerate(self.order): y = (-len(self.boxes) + row) * 40 + 10 box = self.boxes[box_index] bars, labels = box[::2], box[1::2] self.__draw_group_labels(y, box_index) if not self.stretched: self.__draw_row_counts(y, box_index) if self.show_labels and self.attribute is not self.group_var: self.__draw_bar_labels(y, bars, labels) self.__draw_bars(y, bars) self.box_scene.setSceneRect(-self.label_width - 5, -30 - len(self.boxes) * 40, self.scene_width, len(self.boxes * 40) + 90) self.infot1.setText("") self.select_box_items() def __draw_group_labels(self, y, row): """Draw group labels Parameters ---------- y: int vertical offset of bars row: int row index """ label = self.attr_labels[row] b = label.boundingRect() label.setPos(-b.width() - 10, y - b.height() / 2) self.box_scene.addItem(label) def __draw_row_counts(self, y, row): """Draw row counts Parameters ---------- y: int vertical offset of bars row: int row index """ assert not self.is_continuous label = self.labels[row] b = label.boundingRect() if self.group_var: right = self.scale_x * sum(self.conts[row]) else: right = self.scale_x * sum(self.dist) label.setPos(right + 10, y - b.height() / 2) self.box_scene.addItem(label) def __draw_bar_labels(self, y, bars, labels): """Draw bar labels Parameters ---------- y: int vertical offset of bars bars: List[FilterGraphicsRectItem] list of bars being drawn labels: List[QGraphicsTextItem] list of labels for corresponding bars """ label = bar_part = None for text_item, bar_part in zip(labels, bars): label = self.Label( text_item.toPlainText()) label.setPos(bar_part.boundingRect().x(), y - label.boundingRect().height() - 8) label.setMaxWidth(bar_part.boundingRect().width()) self.box_scene.addItem(label) def __draw_bars(self, y, bars): """Draw bars Parameters ---------- y: int vertical offset of bars bars: List[FilterGraphicsRectItem] list of bars to draw """ for item in bars: item.setPos(0, y) self.box_scene.addItem(item) # noinspection PyPep8Naming def compute_tests(self): # The t-test and ANOVA are implemented here since they efficiently use # the widget-specific data in self.stats. # The non-parametric tests can't do this, so we use statistics.tests # pylint: disable=comparison-with-itself def stat_ttest(): d1, d2 = self.stats if d1.n < 2 or d2.n < 2: return np.nan, np.nan pooled_var = d1.var / d1.n + d2.var / d2.n # pylint: disable=comparison-with-itself if pooled_var == 0 or np.isnan(pooled_var): return np.nan, np.nan df = pooled_var ** 2 / \ ((d1.var / d1.n) ** 2 / (d1.n - 1) + (d2.var / d2.n) ** 2 / (d2.n - 1)) t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var) p = 2 * (1 - scipy.special.stdtr(df, t)) return t, p # TODO: Check this function # noinspection PyPep8Naming def stat_ANOVA(): if any(stat.n == 0 for stat in self.stats): return np.nan, np.nan n = sum(stat.n for stat in self.stats) grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n var_between = sum(stat.n * (stat.mean - grand_avg) ** 2 for stat in self.stats) df_between = len(self.stats) - 1 var_within = sum(stat.n * stat.var for stat in self.stats) df_within = n - len(self.stats) if var_within == 0 or df_within == 0 or df_between == 0: return np.nan, np.nan F = (var_between / df_between) / (var_within / df_within) p = 1 - scipy.special.fdtr(df_between, df_within, F) return F, p if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2: t = "" elif any(s.n <= 1 for s in self.stats): t = "At least one group has just one instance, " \ "cannot compute significance" elif len(self.stats) == 2: if self.compare == OWBoxPlot.CompareMedians: t = "" # z, p = tests.wilcoxon_rank_sum( # self.stats[0].dist, self.stats[1].dist) # t = "Mann-Whitney's z: %.1f (p=%.3f)" % (z, p) else: t, p = stat_ttest() t = "" if np.isnan(t) else f"Student's t: {t:.3f} (p={p:.3f})" else: if self.compare == OWBoxPlot.CompareMedians: t = "" # U, p = -1, -1 # t = "Kruskal Wallis's U: %.1f (p=%.3f)" % (U, p) else: F, p = stat_ANOVA() t = "" if np.isnan(F) else f"ANOVA: {F:.3f} (p={p:.3f})" self.infot1.setText("<center>%s</center>" % t) def mean_label(self, stat, attr, val_name): label = QGraphicsItemGroup() t = QGraphicsSimpleTextItem( "%.*f" % (attr.number_of_decimals + 1, stat.mean), label) t.setFont(self._label_font) bbox = t.boundingRect() w2, h = bbox.width() / 2, bbox.height() t.setPos(-w2, -h) tpm = QGraphicsSimpleTextItem( " \u00b1 " + "%.*f" % (attr.number_of_decimals + 1, stat.dev), label) tpm.setFont(self._label_font) tpm.setPos(w2, -h) if val_name: vnm = QGraphicsSimpleTextItem(val_name + ": ", label) vnm.setFont(self._label_font) vnm.setBrush(self._attr_brush) vb = vnm.boundingRect() label.min_x = -w2 - vb.width() vnm.setPos(label.min_x, -h) else: label.min_x = -w2 return label def draw_axis(self): """Draw the horizontal axis and sets self.scale_x""" misssing_stats = not self.stats stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)] mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, "")] bottom = min(stat.a_min for stat in stats) top = max(stat.a_max for stat in stats) first_val, step = compute_scale(bottom, top) while bottom <= first_val: first_val -= step bottom = first_val no_ticks = math.ceil((top - first_val) / step) + 1 top = max(top, first_val + no_ticks * step) gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats)) gtop = max(top, max(stat.mean + stat.dev for stat in stats)) bv = self.box_view viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30) self.scale_x = scale_x = viewrect.width() / (gtop - gbottom) # In principle we should repeat this until convergence since the new # scaling is too conservative. (No chance am I doing this.) mlb = min(stat.mean + mean_lab.min_x / scale_x for stat, mean_lab in zip(stats, mean_labels)) if mlb < gbottom: gbottom = mlb self.scale_x = scale_x = viewrect.width() / (gtop - gbottom) self.scene_min_x = gbottom * scale_x self.scene_width = (gtop - gbottom) * scale_x val = first_val decimals = max(3, 4 - int(math.log10(step))) while True: l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1, self._pen_axis_tick) l.setZValue(100) t = self.box_scene.addSimpleText( repr(round(val, decimals)) if not misssing_stats else "?", self._axis_font) t.setFlags( t.flags() | QGraphicsItem.ItemIgnoresTransformations) r = t.boundingRect() t.setPos(val * scale_x - r.width() / 2, 8) if val >= top: break val += step self.box_scene.addLine( bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis) def draw_axis_disc(self): """ Draw the horizontal axis and sets self.scale_x for discrete attributes """ assert not self.is_continuous if self.stretched: if not self.attr_labels: return step = steps = 10 else: if self.group_var: max_box = max(float(np.sum(dist)) for dist in self.conts) else: max_box = float(np.sum(self.dist)) if max_box == 0: self.scale_x = 1 return _, step = compute_scale(0, max_box) step = int(step) if step > 1 else 1 steps = int(math.ceil(max_box / step)) max_box = step * steps bv = self.box_view viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30) self.scene_width = viewrect.width() lab_width = max(lab.boundingRect().width() for lab in self.attr_labels) lab_width = max(lab_width, 40) lab_width = min(lab_width, self.scene_width / 3) self.label_width = lab_width right_offset = 0 # offset for the right label if not self.stretched and self.labels: if self.group_var: rows = list(zip(self.conts, self.labels)) else: rows = [(self.dist, self.labels[0])] # available space left of the 'group labels' available = self.scene_width - lab_width - 10 scale_x = (available - right_offset) / max_box max_right = max(sum(dist) * scale_x + 10 + lbl.boundingRect().width() for dist, lbl in rows) right_offset = max(0, max_right - max_box * scale_x) self.scale_x = scale_x = \ (self.scene_width - lab_width - 10 - right_offset) / max_box self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis) for val in range(0, step * steps + 1, step): l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1, self._pen_axis_tick) l.setZValue(100) t = self.box_scene.addSimpleText(str(val), self._axis_font) t.setPos(val * scale_x - t.boundingRect().width() / 2, 8) if self.stretched: self.scale_x *= 100 def label_group(self, stat, attr, mean_lab): def centered_text(val, pos): t = QGraphicsSimpleTextItem( "%.*f" % (attr.number_of_decimals + 1, val), labels) t.setFont(self._label_font) bbox = t.boundingRect() t.setPos(pos - bbox.width() / 2, 22) return t def line(x, down=1): QGraphicsLineItem(x, 12 * down, x, 20 * down, labels) def move_label(label, frm, to): label.setX(to) to += t_box.width() / 2 path = QPainterPath() path.lineTo(0, 4) path.lineTo(to - frm, 4) path.lineTo(to - frm, 8) p = QGraphicsPathItem(path) p.setPos(frm, 12) labels.addToGroup(p) labels = QGraphicsItemGroup() labels.addToGroup(mean_lab) m = stat.mean * self.scale_x mean_lab.setPos(m, -22) line(m, -1) if stat.median is not None: msc = stat.median * self.scale_x med_t = centered_text(stat.median, msc) med_box_width2 = med_t.boundingRect().width() / 2 line(msc) if stat.q25 is not None: x = stat.q25 * self.scale_x t = centered_text(stat.q25, x) t_box = t.boundingRect() med_left = msc - med_box_width2 if x + t_box.width() / 2 >= med_left - 5: move_label(t, x, med_left - t_box.width() - 5) else: line(x) if stat.q75 is not None: x = stat.q75 * self.scale_x t = centered_text(stat.q75, x) t_box = t.boundingRect() med_right = msc + med_box_width2 if x - t_box.width() / 2 <= med_right + 5: move_label(t, x, med_right + 5) else: line(x) return labels def box_group(self, stat, height=20): def line(x0, y0, x1, y1, *args): return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args) scale_x = self.scale_x box = [] whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5) whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5) vert_line = line(stat.a_min, 0, stat.a_max, 0) mean_line = line(stat.mean, -height / 3, stat.mean, height / 3) for it in (whisker1, whisker2, mean_line): it.setPen(self._pen_paramet) vert_line.setPen(self._pen_dotted) var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0) var_line.setPen(self._pen_paramet) box.extend([whisker1, whisker2, vert_line, mean_line, var_line]) if stat.q25 is not None and stat.q75 is not None: mbox = FilterGraphicsRectItem( stat.conditions, stat.q25 * scale_x, -height / 2, (stat.q75 - stat.q25) * scale_x, height) mbox.setBrush(self._box_brush) mbox.setPen(QPen(Qt.NoPen)) mbox.setZValue(-200) box.append(mbox) if stat.median is not None: median_line = line(stat.median, -height / 2, stat.median, height / 2) median_line.setPen(self._pen_median) median_line.setZValue(-150) box.append(median_line) return box def strudel(self, dist, group_val_index=None): attr = self.attribute ss = np.sum(dist) box = [] if ss < 1e-6: cond = [FilterDiscrete(attr, None)] if group_val_index is not None: cond.append(FilterDiscrete(self.group_var, [group_val_index])) box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10)) cum = 0 for i, v in enumerate(dist): if v < 1e-6: continue if self.stretched: v /= ss v *= self.scale_x cond = [FilterDiscrete(attr, [i])] if group_val_index is not None: cond.append(FilterDiscrete(self.group_var, [group_val_index])) rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12) rect.setBrush(QBrush(QColor(*attr.colors[i]))) rect.setPen(QPen(Qt.NoPen)) if self.stretched: tooltip = "{}: {:.2f}%".format(attr.values[i], 100 * dist[i] / sum(dist)) else: tooltip = "{}: {}".format(attr.values[i], int(dist[i])) rect.setToolTip(tooltip) text = QGraphicsTextItem(attr.values[i]) box.append(rect) box.append(text) cum += v return box def commit(self): self.conditions = [item.filter for item in self.box_scene.selectedItems() if item.filter] selected, selection = None, [] if self.conditions: selected = Values(self.conditions, conjunction=False)(self.dataset) selection = np.in1d( self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0] self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send( create_annotated_table(self.dataset, selection)) def show_posthoc(self): def line(y0, y1): it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen) it.setZValue(-100) self.posthoc_lines.append(it) while self.posthoc_lines: self.box_scene.removeItem(self.posthoc_lines.pop()) if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2: return if self.compare == OWBoxPlot.CompareMedians: crit_line = "median" else: crit_line = "mean" xs = [] height = 90 if self.show_annotations else 60 y_up = -len(self.stats) * height + 10 for pos, box_index in enumerate(self.order): stat = self.stats[box_index] x = getattr(stat, crit_line) if x is None: continue x *= self.scale_x xs.append(x * self.scale_x) by = y_up + pos * height line(by + 12, 3) line(by - 12, by - 25) used_to = [] last_to = to = 0 for frm, frm_x in enumerate(xs[:-1]): for to in range(frm + 1, len(xs)): if xs[to] - frm_x > 1.5: to -= 1 break if to in (last_to, frm): continue for rowi, used in enumerate(used_to): if used < frm: used_to[rowi] = to break else: rowi = len(used_to) used_to.append(to) y = - 6 - rowi * 6 it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y, self._post_grp_pen) self.posthoc_lines.append(it) last_to = to def get_widget_name_extension(self): return self.attribute.name if self.attribute else None def send_report(self): self.report_plot() text = "" if self.attribute: text += "Box plot for attribute '{}' ".format(self.attribute.name) if self.group_var: text += "grouped by '{}'".format(self.group_var.name) if text: self.report_caption(text) class Label(QGraphicsSimpleTextItem): """Boxplot Label with settable maxWidth""" # Minimum width to display label text MIN_LABEL_WIDTH = 25 # padding bellow the text PADDING = 3 __max_width = None def maxWidth(self): return self.__max_width def setMaxWidth(self, max_width): self.__max_width = max_width def paint(self, painter, option, widget): """Overrides QGraphicsSimpleTextItem.paint If label text is too long, it is elided to fit into the allowed region """ if self.__max_width is None: width = option.rect.width() else: width = self.__max_width if width < self.MIN_LABEL_WIDTH: # if space is too narrow, no label return fm = painter.fontMetrics() text = fm.elidedText(self.text(), Qt.ElideRight, width) painter.drawText( option.rect.x(), option.rect.y() + self.boundingRect().height() - self.PADDING, text)
def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.scale_x = self.scene_min_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() view = gui.listView( self.controlArea, self, "attribute", box="Variable", model=self.attrs, callback=self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) gui.separator(view.box, 6, 6) self.cb_order = gui.checkBox( view.box, self, "order_by_importance", "Order by relevance", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_sorting) self.group_vars = DomainModel( placeholder="None", separators=False, valid_types=Orange.data.DiscreteVariable) self.group_view = view = gui.listView( self.controlArea, self, "group_var", box="Subgroups", model=self.group_vars, callback=self.grouping_changed) view.setEnabled(False) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox( self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum), addSpace=False) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.layout_changed) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox( self.controlArea, box="Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox( box, self, 'stretched', "Stretch bars", callback=self.display_changed) gui.checkBox( box, self, 'show_labels', "Show box labels", callback=self.display_changed) self.sort_cb = gui.checkBox( box, self, 'sort_freqs', "Sort by subgroup frequencies", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_scene.selectionChanged.connect(self.commit) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>No test results.</center>") self.mainArea.setMinimumWidth(300) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box()
def resizeEvent(self, event): QGraphicsView.resizeEvent(self, event) self.update_view()
def __init__(self): super().__init__() # Diagram update is in progress self._updating = False # Input update is in progress self._inputUpdate = False # All input tables have the same domain. self.samedomain = True # Input datasets in the order they were 'connected'. self.data = OrderedDict() # Extracted input item sets in the order they were 'connected' self.itemsets = OrderedDict() # GUI box = gui.vBox(self.controlArea, "Info") self.info = gui.widgetLabel(box, "No data on input.\n") self.identifiersBox = gui.radioButtonsInBox( self.controlArea, self, "useidentifiers", [], box="Data Instance Identifiers", callback=self._on_useidentifiersChanged, ) self.useequalityButton = gui.appendRadioButton(self.identifiersBox, "Use instance equality") self.useidentifiersButton = rb = gui.appendRadioButton(self.identifiersBox, "Use identifiers") self.inputsBox = gui.indentedBox(self.identifiersBox, sep=gui.checkButtonOffsetHint(rb)) self.inputsBox.setEnabled(bool(self.useidentifiers)) for i in range(5): box = gui.vBox(self.inputsBox, "Data set #%i" % (i + 1), addSpace=False) box.setFlat(True) model = itemmodels.VariableListModel(parent=self) cb = QComboBox(minimumContentsLength=12, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) cb.setModel(model) cb.activated[int].connect(self._on_inputAttrActivated) box.setEnabled(False) # Store the combo in the box for later use. box.combo_box = cb box.layout().addWidget(cb) gui.rubber(self.controlArea) box = gui.vBox(self.controlArea, "Output") gui.checkBox(box, self, "output_duplicates", "Output duplicates", callback=lambda: self.commit()) gui.auto_commit(box, self, "autocommit", "Send Selection", "Send Automatically", box=False) # Main area view self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing) self.view.setBackgroundRole(QPalette.Window) self.view.setFrameStyle(QGraphicsView.StyledPanel) self.mainArea.layout().addWidget(self.view) self.vennwidget = VennDiagram() self.vennwidget.resize(400, 400) self.vennwidget.itemTextEdited.connect(self._on_itemTextEdited) self.scene.selectionChanged.connect(self._on_selectionChanged) self.scene.addItem(self.vennwidget) self.resize(self.controlArea.sizeHint().width() + 550, max(self.controlArea.sizeHint().height(), 550)) self._queue = []
class OWVennDiagram(widget.OWWidget): name = "Venn Diagram" description = "A graphical visualization of the overlap of data instances " "from a collection of input data sets." icon = "icons/VennDiagram.svg" priority = 280 inputs = [("Data", Orange.data.Table, "setData", widget.Multiple)] outputs = [("Selected Data", Orange.data.Table)] # Selected disjoint subset indices selection = settings.Setting([]) #: Stored input set hints #: {(index, inputname, attributes): (selectedattrname, itemsettitle)} #: The 'selectedattrname' can be None inputhints = settings.Setting({}) #: Use identifier columns for instance matching useidentifiers = settings.Setting(True) #: Output unique items (one output row for every unique instance `key`) #: or preserve all duplicates in the output. output_duplicates = settings.Setting(False) autocommit = settings.Setting(True) graph_name = "scene" def __init__(self): super().__init__() # Diagram update is in progress self._updating = False # Input update is in progress self._inputUpdate = False # All input tables have the same domain. self.samedomain = True # Input datasets in the order they were 'connected'. self.data = OrderedDict() # Extracted input item sets in the order they were 'connected' self.itemsets = OrderedDict() # GUI box = gui.vBox(self.controlArea, "Info") self.info = gui.widgetLabel(box, "No data on input.\n") self.identifiersBox = gui.radioButtonsInBox( self.controlArea, self, "useidentifiers", [], box="Data Instance Identifiers", callback=self._on_useidentifiersChanged, ) self.useequalityButton = gui.appendRadioButton(self.identifiersBox, "Use instance equality") self.useidentifiersButton = rb = gui.appendRadioButton(self.identifiersBox, "Use identifiers") self.inputsBox = gui.indentedBox(self.identifiersBox, sep=gui.checkButtonOffsetHint(rb)) self.inputsBox.setEnabled(bool(self.useidentifiers)) for i in range(5): box = gui.vBox(self.inputsBox, "Data set #%i" % (i + 1), addSpace=False) box.setFlat(True) model = itemmodels.VariableListModel(parent=self) cb = QComboBox(minimumContentsLength=12, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) cb.setModel(model) cb.activated[int].connect(self._on_inputAttrActivated) box.setEnabled(False) # Store the combo in the box for later use. box.combo_box = cb box.layout().addWidget(cb) gui.rubber(self.controlArea) box = gui.vBox(self.controlArea, "Output") gui.checkBox(box, self, "output_duplicates", "Output duplicates", callback=lambda: self.commit()) gui.auto_commit(box, self, "autocommit", "Send Selection", "Send Automatically", box=False) # Main area view self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing) self.view.setBackgroundRole(QPalette.Window) self.view.setFrameStyle(QGraphicsView.StyledPanel) self.mainArea.layout().addWidget(self.view) self.vennwidget = VennDiagram() self.vennwidget.resize(400, 400) self.vennwidget.itemTextEdited.connect(self._on_itemTextEdited) self.scene.selectionChanged.connect(self._on_selectionChanged) self.scene.addItem(self.vennwidget) self.resize(self.controlArea.sizeHint().width() + 550, max(self.controlArea.sizeHint().height(), 550)) self._queue = [] @check_sql_input def setData(self, data, key=None): self.error() if not self._inputUpdate: # Store hints only on the first setData call. self._storeHints() self._inputUpdate = True if key in self.data: if data is None: # Remove the input self._remove(key) else: # Update existing item self._update(key, data) elif data is not None: # TODO: Allow setting more them 5 inputs and let the user # select the 5 to display. if len(self.data) == 5: self.error("Venn diagram accepts at most five data sets.") return # Add a new input self._add(key, data) def handleNewSignals(self): self._inputUpdate = False # Check if all inputs are from the same domain. domains = [input.table.domain for input in self.data.values()] samedomain = all(domain_eq(d1, d2) for d1, d2 in pairwise(domains)) self.samedomain = samedomain has_identifiers = all(source_attributes(input.table.domain) for input in self.data.values()) has_any_identifiers = any(source_attributes(input.table.domain) for input in self.data.values()) self.useequalityButton.setEnabled(samedomain) self.useidentifiersButton.setEnabled(has_any_identifiers or len(self.data) == 0) self.inputsBox.setEnabled(has_any_identifiers) if not samedomain and has_any_identifiers and not self.useidentifiers: self.useidentifiers = 1 elif samedomain and not has_identifiers: self.useidentifiers = 0 incremental = all(inc for _, inc in self._queue) if incremental: # Only received updated data on existing link. self._updateItemsets() else: # Links were removed and/or added. self._createItemsets() self._restoreHints() self._updateItemsets() del self._queue[:] self._createDiagram() if self.data: self.info.setText("{} data sets on input.\n".format(len(self.data))) else: self.info.setText("No data on input\n") self._updateInfo() super().handleNewSignals() def _invalidate(self, keys=None, incremental=True): """ Invalidate input for a list of input keys. """ if keys is None: keys = list(self.data.keys()) self._queue.extend((key, incremental) for key in keys) def itemsetAttr(self, key): index = list(self.data.keys()).index(key) _, combo = self._controlAtIndex(index) model = combo.model() attr_index = combo.currentIndex() if attr_index >= 0: return model[attr_index] else: return None def _controlAtIndex(self, index): group_box = self.inputsBox.layout().itemAt(index).widget() combo = group_box.combo_box return group_box, combo def _setAttributes(self, index, attrs): box, combo = self._controlAtIndex(index) model = combo.model() if attrs is None: model[:] = [] box.setEnabled(False) else: if model[:] != attrs: model[:] = attrs box.setEnabled(True) def _add(self, key, table): name = table.name index = len(self.data) attrs = source_attributes(table.domain) self.data[key] = _InputData(key, name, table) self._setAttributes(index, attrs) self._invalidate([key], incremental=False) item = self.inputsBox.layout().itemAt(index) box = item.widget() box.setTitle("Data set: {}".format(name)) def _remove(self, key): index = list(self.data.keys()).index(key) # Clear possible warnings. self.warning() self._setAttributes(index, None) del self.data[key] layout = self.inputsBox.layout() item = layout.takeAt(index) layout.addItem(item) inputs = list(self.data.values()) for i in range(5): box, _ = self._controlAtIndex(i) if i < len(inputs): title = "Data set: {}".format(inputs[i].name) else: title = "Data set #{}".format(i + 1) box.setTitle(title) self._invalidate([key], incremental=False) def _update(self, key, table): name = table.name index = list(self.data.keys()).index(key) attrs = source_attributes(table.domain) self.data[key] = self.data[key]._replace(name=name, table=table) self._setAttributes(index, attrs) self._invalidate([key]) item = self.inputsBox.layout().itemAt(index) box = item.widget() box.setTitle("Data set: {}".format(name)) def _itemsForInput(self, key): useidentifiers = self.useidentifiers or not self.samedomain def items_by_key(key, input): attr = self.itemsetAttr(key) if attr is not None: return [str(inst[attr]) for inst in input.table if not numpy.isnan(inst[attr])] else: return [] def items_by_eq(key, input): return list(map(ComparableInstance, input.table)) input = self.data[key] if useidentifiers: items = items_by_key(key, input) else: items = items_by_eq(key, input) return items def _updateItemsets(self): assert list(self.data.keys()) == list(self.itemsets.keys()) for key, input in list(self.data.items()): items = self._itemsForInput(key) item = self.itemsets[key] item = item._replace(items=items) name = input.name if item.name != name: item = item._replace(name=name, title=name) self.itemsets[key] = item def _createItemsets(self): olditemsets = dict(self.itemsets) self.itemsets.clear() for key, input in self.data.items(): items = self._itemsForInput(key) name = input.name if key in olditemsets and olditemsets[key].name == name: # Reuse the title (which might have been changed by the user) title = olditemsets[key].title else: title = name itemset = _ItemSet(key=key, name=name, title=title, items=items) self.itemsets[key] = itemset def _storeHints(self): if self.data: self.inputhints.clear() for i, (key, input) in enumerate(self.data.items()): attrs = source_attributes(input.table.domain) attrs = tuple(attr.name for attr in attrs) selected = self.itemsetAttr(key) if selected is not None: attr_name = selected.name else: attr_name = None itemset = self.itemsets[key] self.inputhints[(i, input.name, attrs)] = (attr_name, itemset.title) def _restoreHints(self): settings = [] for i, (key, input) in enumerate(self.data.items()): attrs = source_attributes(input.table.domain) attrs = tuple(attr.name for attr in attrs) hint = self.inputhints.get((i, input.name, attrs), None) if hint is not None: attr, name = hint attr_ind = attrs.index(attr) if attr is not None else -1 settings.append((attr_ind, name)) else: return # all inputs match the stored hints for i, key in enumerate(self.itemsets): attr, itemtitle = settings[i] self.itemsets[key] = self.itemsets[key]._replace(title=itemtitle) _, cb = self._controlAtIndex(i) cb.setCurrentIndex(attr) def _createDiagram(self): self._updating = True oldselection = list(self.selection) self.vennwidget.clear() n = len(self.itemsets) self.disjoint = disjoint(set(s.items) for s in self.itemsets.values()) vennitems = [] colors = colorpalette.ColorPaletteHSV(n) for i, (key, item) in enumerate(self.itemsets.items()): count = len(set(item.items)) count_all = len(item.items) if count != count_all: fmt = "{} <i>(all: {})</i>" else: fmt = "{}" counts = fmt.format(count, count_all) gr = VennSetItem(text=item.title, informativeText=counts) color = colors[i] color.setAlpha(100) gr.setBrush(QBrush(color)) gr.setPen(QPen(Qt.NoPen)) vennitems.append(gr) self.vennwidget.setItems(vennitems) for i, area in enumerate(self.vennwidget.vennareas()): area_items = list(map(str, list(self.disjoint[i]))) if i: area.setText("{0}".format(len(area_items))) label = disjoint_set_label(i, n, simplify=False) head = "<h4>|{}| = {}</h4>".format(label, len(area_items)) if len(area_items) > 32: items_str = ", ".join(map(escape, area_items[:32])) hidden = len(area_items) - 32 tooltip = "{}<span>{}, ...</br>({} items not shown)<span>".format(head, items_str, hidden) elif area_items: tooltip = "{}<span>{}</span>".format(head, ", ".join(map(escape, area_items))) else: tooltip = head area.setToolTip(tooltip) area.setPen(QPen(QColor(10, 10, 10, 200), 1.5)) area.setFlag(QGraphicsPathItem.ItemIsSelectable, True) area.setSelected(i in oldselection) self._updating = False self._on_selectionChanged() def _updateInfo(self): # Clear all warnings self.warning() if not len(self.data): self.info.setText("No data on input\n") else: self.info.setText("{0} data sets on input\n".format(len(self.data))) if self.useidentifiers: no_idx = [ "#{}".format(i + 1) for i, key in enumerate(self.data) if not source_attributes(self.data[key].table.domain) ] if len(no_idx) == 1: self.warning("Data set {} has no suitable identifiers.".format(no_idx[0])) elif len(no_idx) > 1: self.warning( "Data sets {} and {} have no suitable identifiers.".format(", ".join(no_idx[:-1]), no_idx[-1]) ) def _on_selectionChanged(self): if self._updating: return areas = self.vennwidget.vennareas() indices = [i for i, area in enumerate(areas) if area.isSelected()] self.selection = indices self.invalidateOutput() def _on_useidentifiersChanged(self): self.inputsBox.setEnabled(self.useidentifiers == 1) # Invalidate all itemsets self._invalidate() self._updateItemsets() self._createDiagram() self._updateInfo() def _on_inputAttrActivated(self, attr_index): combo = self.sender() # Find the input index to which the combo box belongs # (they are reordered when removing inputs). index = None inputs = list(self.data.items()) for i in range(len(inputs)): _, c = self._controlAtIndex(i) if c is combo: index = i break assert index is not None key, _ = inputs[index] self._invalidate([key]) self._updateItemsets() self._createDiagram() def _on_itemTextEdited(self, index, text): text = str(text) key = list(self.itemsets.keys())[index] self.itemsets[key] = self.itemsets[key]._replace(title=text) def invalidateOutput(self): self.commit() def commit(self): selected_subsets = [] selected_items = reduce(set.union, [self.disjoint[index] for index in self.selection], set()) def match(val): if numpy.isnan(val): return False else: return str(val) in selected_items source_var = Orange.data.StringVariable("source") item_id_var = Orange.data.StringVariable("item_id") names = [itemset.title.strip() for itemset in self.itemsets.values()] names = uniquify(names) for i, (key, input) in enumerate(self.data.items()): if self.useidentifiers: attr = self.itemsetAttr(key) if attr is not None: mask = list(map(match, (inst[attr] for inst in input.table))) else: mask = [False] * len(input.table) def instance_key(inst): return str(inst[attr]) else: mask = [ComparableInstance(inst) in selected_items for inst in input.table] _map = {item: str(i) for i, item in enumerate(selected_items)} def instance_key(inst): return _map[ComparableInstance(inst)] mask = numpy.array(mask, dtype=bool) subset = input.table[mask] if len(subset) == 0: continue # add columns with source table id and set id if not self.output_duplicates: id_column = numpy.array([[instance_key(inst)] for inst in subset], dtype=object) source_names = numpy.array([[names[i]]] * len(subset), dtype=object) subset = append_column(subset, "M", source_var, source_names) subset = append_column(subset, "M", item_id_var, id_column) selected_subsets.append(subset) if selected_subsets and not self.output_duplicates: data = table_concat(selected_subsets) # Get all variables which are not constant between the same # item set varying = varying_between(data, [item_id_var]) if source_var in varying: varying.remove(source_var) data = reshape_wide(data, varying, [item_id_var], [source_var]) # remove the temporary item set id column data = drop_columns(data, [item_id_var]) elif selected_subsets: data = table_concat(selected_subsets) else: data = None self.send("Selected Data", data) def getSettings(self, *args, **kwargs): self._storeHints() return super().getSettings(self, *args, **kwargs) def send_report(self): self.report_plot()
def mouseReleaseEvent(self, event): if event.button() & Qt.LeftButton: self.__stopAutoScroll() return QGraphicsView.mouseReleaseEvent(self, event)
class OWQualityControl(widget.OWWidget): name = "Quality Control" description = "Experiment quality control" icon = "../widgets/icons/QualityControl.svg" priority = 5000 inputs = [("Experiment Data", Orange.data.Table, "set_data")] outputs = [] DISTANCE_FUNCTIONS = [("Distance from Pearson correlation", dist_pcorr), ("Euclidean distance", dist_eucl), ("Distance from Spearman correlation", dist_spearman)] settingsHandler = SetContextHandler() split_by_labels = settings.ContextSetting({}) sort_by_labels = settings.ContextSetting({}) selected_distance_index = settings.Setting(0) def __init__(self, parent=None): super().__init__(parent) ## Attributes self.data = None self.distances = None self.groups = None self.unique_pos = None self.base_group_index = 0 ## GUI box = gui.widgetBox(self.controlArea, "Info") self.info_box = gui.widgetLabel(box, "\n") ## Separate By box box = gui.widgetBox(self.controlArea, "Separate By") self.split_by_model = itemmodels.PyListModel(parent=self) self.split_by_view = QListView() self.split_by_view.setSelectionMode(QListView.ExtendedSelection) self.split_by_view.setModel(self.split_by_model) box.layout().addWidget(self.split_by_view) self.split_by_view.selectionModel().selectionChanged.connect( self.on_split_key_changed) ## Sort By box box = gui.widgetBox(self.controlArea, "Sort By") self.sort_by_model = itemmodels.PyListModel(parent=self) self.sort_by_view = QListView() self.sort_by_view.setSelectionMode(QListView.ExtendedSelection) self.sort_by_view.setModel(self.sort_by_model) box.layout().addWidget(self.sort_by_view) self.sort_by_view.selectionModel().selectionChanged.connect( self.on_sort_key_changed) ## Distance box box = gui.widgetBox(self.controlArea, "Distance Measure") gui.comboBox(box, self, "selected_distance_index", items=[name for name, _ in self.DISTANCE_FUNCTIONS], callback=self.on_distance_measure_changed) self.scene = QGraphicsScene() self.scene_view = QGraphicsView(self.scene) self.scene_view.setRenderHints(QPainter.Antialiasing) self.scene_view.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.mainArea.layout().addWidget(self.scene_view) self.scene_view.installEventFilter(self) self._disable_updates = False self._cached_distances = {} self._base_index_hints = {} self.main_widget = None self.resize(800, 600) def clear(self): """Clear the widget state.""" self.data = None self.distances = None self.groups = None self.unique_pos = None with disable_updates(self): self.split_by_model[:] = [] self.sort_by_model[:] = [] self.main_widget = None self.scene.clear() self.info_box.setText("\n") self._cached_distances = {} def set_data(self, data=None): """Set input experiment data.""" self.closeContext() self.clear() self.error(0) self.warning(0) if data is not None: keys = self.get_suitable_keys(data) if not keys: self.error(0, "Data has no suitable feature labels.") data = None self.data = data if data is not None: self.on_new_data() def update_label_candidates(self): """Update the label candidates selection GUI (Group/Sort By views). """ keys = self.get_suitable_keys(self.data) with disable_updates(self): self.split_by_model[:] = keys self.sort_by_model[:] = keys def get_suitable_keys(self, data): """ Return suitable attr label keys from the data where the key has at least two unique values in the data. """ attrs = [attr.attributes.items() for attr in data.domain.attributes] attrs = reduce(operator.iadd, attrs, []) # in case someone put non string values in attributes dict attrs = [(str(key), str(value)) for key, value in attrs] attrs = set(attrs) values = defaultdict(set) for key, value in attrs: values[key].add(value) keys = [key for key in values if len(values[key]) > 1] return keys def selected_split_by_labels(self): """Return the current selected split labels. """ sel_m = self.split_by_view.selectionModel() indices = [r.row() for r in sel_m.selectedRows()] return [self.sort_by_model[i] for i in indices] def selected_sort_by_labels(self): """Return the current selected sort labels """ sel_m = self.sort_by_view.selectionModel() indices = [r.row() for r in sel_m.selectedRows()] return [self.sort_by_model[i] for i in indices] def selected_distance(self): """Return the selected distance function. """ return self.DISTANCE_FUNCTIONS[self.selected_distance_index][1] def selected_base_group_index(self): """Return the selected base group index """ return self.base_group_index def selected_base_indices(self, base_group_index=None): indices = [] for g, ind in self.groups: if base_group_index is None: label = group_label(self.selected_split_by_labels(), g) ind = [i for i in ind if i is not None] i = self._base_index_hints.get(label, ind[0] if ind else None) else: i = ind[base_group_index] indices.append(i) return indices def on_new_data(self): """We have new data and need to recompute all. """ self.closeContext() self.update_label_candidates() self.info_box.setText( "%s genes \n%s experiments" % (len(self.data), len(self.data.domain.attributes)) ) self.base_group_index = 0 keys = self.get_suitable_keys(self.data) self.openContext(keys) ## Restore saved context settings (split/sort selection) split_by_labels = self.split_by_labels sort_by_labels = self.sort_by_labels def select(model, selection_model, selected_items): """Select items in a Qt item model view """ all_items = list(model) try: indices = [all_items.index(item) for item in selected_items] except: indices = [] for ind in indices: selection_model.select(model.index(ind), QItemSelectionModel.Select) with disable_updates(self): select(self.split_by_view.model(), self.split_by_view.selectionModel(), split_by_labels) select(self.sort_by_view.model(), self.sort_by_view.selectionModel(), sort_by_labels) with widget_disable(self): self.split_and_update() def on_split_key_changed(self, *args): """Split key has changed """ with widget_disable(self): if not self._disable_updates: self.base_group_index = 0 self.split_by_labels = self.selected_split_by_labels() self.split_and_update() def on_sort_key_changed(self, *args): """Sort key has changed """ with widget_disable(self): if not self._disable_updates: self.base_group_index = 0 self.sort_by_labels = self.selected_sort_by_labels() self.split_and_update() def on_distance_measure_changed(self): """Distance measure has changed """ if self.data is not None: with widget_disable(self): self.update_distances() self.replot_experiments() def on_view_resize(self, size): """The view with the quality plot has changed """ if self.main_widget: current = self.main_widget.size() self.main_widget.resize(size.width() - 6, current.height()) self.scene.setSceneRect(self.scene.itemsBoundingRect()) def on_rug_item_clicked(self, item): """An ``item`` in the quality plot has been clicked. """ update = False sort_by_labels = self.selected_sort_by_labels() if sort_by_labels and item.in_group: ## The item is part of the group if item.group_index != self.base_group_index: self.base_group_index = item.group_index update = True else: if sort_by_labels: # If the user clicked on an background item it # invalidates the sorted labels selection with disable_updates(self): self.sort_by_view.selectionModel().clear() update = True index = item.index group = item.group label = group_label(self.selected_split_by_labels(), group) if self._base_index_hints.get(label, 0) != index: self._base_index_hints[label] = index update = True if update: with widget_disable(self): self.split_and_update() def eventFilter(self, obj, event): if obj is self.scene_view and event.type() == QEvent.Resize: self.on_view_resize(event.size()) return super().eventFilter(obj, event) def split_and_update(self): """ Split the data based on the selected sort/split labels and update the quality plot. """ split_labels = self.selected_split_by_labels() sort_labels = self.selected_sort_by_labels() self.warning(0) if not split_labels: self.warning(0, "No separate by label selected.") self.groups, self.unique_pos = \ exp.separate_by(self.data, split_labels, consider=sort_labels, add_empty=True) self.groups = sorted(self.groups.items(), key=lambda t: list(map(float_if_posible, t[0]))) self.unique_pos = sorted(self.unique_pos.items(), key=lambda t: list(map(float_if_posible, t[0]))) if self.groups: if sort_labels: group_base = self.selected_base_group_index() base_indices = self.selected_base_indices(group_base) else: base_indices = self.selected_base_indices() self.update_distances(base_indices) self.replot_experiments() def get_cached_distances(self, measure): if measure not in self._cached_distances: attrs = self.data.domain.attributes mat = numpy.zeros((len(attrs), len(attrs))) self._cached_distances[measure] = \ (mat, set(zip(range(len(attrs)), range(len(attrs))))) return self._cached_distances[measure] def get_cached_distance(self, measure, i, j): matrix, computed = self.get_cached_distances(measure) key = (i, j) if i < j else (j, i) if key in computed: return matrix[i, j] else: return None def get_distance(self, measure, i, j): d = self.get_cached_distance(measure, i, j) if d is None: vec_i = take_columns(self.data, [i]) vec_j = take_columns(self.data, [j]) d = measure(vec_i, vec_j) mat, computed = self.get_cached_distances(measure) mat[i, j] = d key = key = (i, j) if i < j else (j, i) computed.add(key) return d def store_distance(self, measure, i, j, dist): matrix, computed = self.get_cached_distances(measure) key = (i, j) if i < j else (j, i) matrix[j, i] = matrix[i, j] = dist computed.add(key) def update_distances(self, base_indices=()): """Recompute the experiment distances. """ distance = self.selected_distance() if base_indices == (): base_group_index = self.selected_base_group_index() base_indices = [ind[base_group_index] \ for _, ind in self.groups] assert(len(base_indices) == len(self.groups)) base_distances = [] attributes = self.data.domain.attributes pb = gui.ProgressBar(self, len(self.groups) * len(attributes)) for (group, indices), base_index in zip(self.groups, base_indices): # Base column of the group if base_index is not None: base_vec = take_columns(self.data, [base_index]) distances = [] # Compute the distances between base column # and all the rest data columns. for i in range(len(attributes)): if i == base_index: distances.append(0.0) elif self.get_cached_distance(distance, i, base_index) is not None: distances.append(self.get_cached_distance(distance, i, base_index)) else: vec_i = take_columns(self.data, [i]) dist = distance(base_vec, vec_i) self.store_distance(distance, i, base_index, dist) distances.append(dist) pb.advance() base_distances.append(distances) else: base_distances.append(None) pb.finish() self.distances = base_distances 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 mousePressEvent(self, event): QGraphicsView.mousePressEvent(self, event)
def setScene(self, scene): QGraphicsView.setScene(self, scene) self._ensureSceneRect(scene)