def paint(self, painter, option, widget=None): brush = self.brush() if self.__hover: brush = QBrush(brush.color().darker(110)) painter.setBrush(brush) painter.setPen(self.pen()) painter.drawEllipse(self.rect())
def brush_darker(brush, factor): """Return a copy of the brush darkened by factor. """ grad = brush.gradient() if grad: return QBrush(gradient_darker(grad, factor)) else: brush = QBrush(brush) brush.setColor(brush.color().darker(factor)) return brush
def paint(self, painter, option, widget=None): painter.save() path = self.path() brush = QBrush(self.brush()) pen = QPen(self.pen()) if option.state & QStyle.State_Selected: pen.setColor(Qt.red) brush.setStyle(Qt.DiagCrossPattern) brush.setColor(QColor(40, 40, 40, 100)) elif option.state & QStyle.State_MouseOver: pen.setColor(Qt.blue) if option.state & QStyle.State_MouseOver: brush.setColor(QColor(100, 100, 100, 100)) if brush.style() == Qt.NoBrush: # Make sure the highlight is actually visible. brush.setStyle(Qt.SolidPattern) painter.setPen(pen) painter.setBrush(brush) painter.drawPath(path) painter.restore()
def color_for_label(self, ind, light=100): if self.label_colors is None: return Qt.lightGray return QBrush(self.label_colors[ind].lighter(light))
def setBrush(self, brush): brush = QBrush(brush) if self.__brush != brush: self.__brush = brush for item in self.__items: item.setBrush(brush)
def _update(self): def _isinvalid(x): return isnan(x) or isinf(x) # Update the displayed confusion matrix if self.results is not None and self.selected_learner: cmatrix = confusion_matrix(self.results, self.selected_learner[0]) colsum = cmatrix.sum(axis=0) rowsum = cmatrix.sum(axis=1) n = len(cmatrix) diag = numpy.diag_indices(n) colors = cmatrix.astype(numpy.double) colors[diag] = 0 if self.selected_quantity == 0: normalized = cmatrix.astype(numpy.int) formatstr = "{}" div = numpy.array([colors.max()]) else: if self.selected_quantity == 1: normalized = 100 * cmatrix / colsum div = colors.max(axis=0) else: normalized = 100 * cmatrix / rowsum[:, numpy.newaxis] div = colors.max(axis=1)[:, numpy.newaxis] formatstr = "{:2.1f} %" div[div == 0] = 1 colors /= div colors[diag] = normalized[diag] / normalized[diag].max() for i in range(n): for j in range(n): val = normalized[i, j] col_val = colors[i, j] item = self._item(i + 2, j + 2) item.setData( "NA" if _isinvalid(val) else formatstr.format(val), Qt.DisplayRole) bkcolor = QColor.fromHsl( [0, 240][i == j], 160, 255 if _isinvalid(col_val) else int(255 - 30 * col_val)) item.setData(QBrush(bkcolor), Qt.BackgroundRole) item.setData("trbl", BorderRole) item.setToolTip("actual: {}\npredicted: {}".format( self.headers[i], self.headers[j])) item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self._set_item(i + 2, j + 2, item) bold_font = self.tablemodel.invisibleRootItem().font() bold_font.setBold(True) def _sum_item(value, border=""): item = QStandardItem() item.setData(value, Qt.DisplayRole) item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) item.setFlags(Qt.ItemIsEnabled) item.setFont(bold_font) item.setData(border, BorderRole) item.setData(QColor(192, 192, 192), BorderColorRole) return item for i in range(n): self._set_item(n + 2, i + 2, _sum_item(int(colsum[i]), "t")) self._set_item(i + 2, n + 2, _sum_item(int(rowsum[i]), "l")) self._set_item(n + 2, n + 2, _sum_item(int(rowsum.sum())))
def _setup_plot(self): def merge_averaging(): for curve in curves: graphics = curve.merge() curve = graphics.curve self.plot.addItem(graphics.curve_item) if self.display_convex_curve: self.plot.addItem(graphics.hull_item) if self.display_def_threshold and curve.is_valid: points = curve.points ind = numpy.argmin(numpy.abs(points.thresholds - 0.5)) item = pg.TextItem(text="{:.3f}".format( points.thresholds[ind]), ) item.setPos(points.fpr[ind], points.tpr[ind]) self.plot.addItem(item) hull_curves = [curve.merged.hull for curve in selected] if hull_curves: self._rocch = convex_hull(hull_curves) iso_pen = QPen(QColor(Qt.black), 1) iso_pen.setCosmetic(True) self._perf_line = InfiniteLine(pen=iso_pen, antialias=True) self.plot.addItem(self._perf_line) return hull_curves def vertical_averaging(): for curve in curves: graphics = curve.avg_vertical() self.plot.addItem(graphics.curve_item) self.plot.addItem(graphics.confint_item) return [curve.avg_vertical.hull for curve in selected] def threshold_averaging(): for curve in curves: graphics = curve.avg_threshold() self.plot.addItem(graphics.curve_item) self.plot.addItem(graphics.confint_item) return [curve.avg_threshold.hull for curve in selected] def no_averaging(): for curve in curves: graphics = curve.folds() for fold in graphics: self.plot.addItem(fold.curve_item) if self.display_convex_curve: self.plot.addItem(fold.hull_item) return [fold.hull for curve in selected for fold in curve.folds] averagings = { OWROCAnalysis.Merge: merge_averaging, OWROCAnalysis.Vertical: vertical_averaging, OWROCAnalysis.Threshold: threshold_averaging, OWROCAnalysis.NoAveraging: no_averaging } target = self.target_index selected = self.selected_classifiers curves = [self.plot_curves(target, i) for i in selected] selected = [self.curve_data(target, i) for i in selected] hull_curves = averagings[self.roc_averaging]() if self.display_convex_hull and hull_curves: hull = convex_hull(hull_curves) hull_pen = QPen(QColor(200, 200, 200, 100), 2) hull_pen.setCosmetic(True) item = self.plot.plot(hull.fpr, hull.tpr, pen=hull_pen, brush=QBrush(QColor(200, 200, 200, 50)), fillLevel=0) item.setZValue(-10000) pen = QPen(QColor(100, 100, 100, 100), 1, Qt.DashLine) pen.setCosmetic(True) self.plot.plot([0, 1], [0, 1], pen=pen, antialias=True) if self.roc_averaging == OWROCAnalysis.Merge: self._update_perf_line() warning = "" if not all(c.is_valid for c in hull_curves): if any(c.is_valid for c in hull_curves): warning = "Some ROC curves are undefined" else: warning = "All ROC curves are undefined" self.warning(warning)
class OWSOM(OWWidget): name = "Self-Organizing Map" description = "Computation of self-organizing map." icon = "icons/SOM.svg" keywords = ["SOM"] class Inputs: data = Input("Data", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) settingsHandler = DomainContextHandler() auto_dimension = Setting(True) size_x = Setting(10) size_y = Setting(10) hexagonal = Setting(1) initialization = Setting(0) attr_color = ContextSetting(None) size_by_instances = Setting(True) pie_charts = Setting(False) selection = Setting(None, schema_only=True) graph_name = "view" _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2) _grid_pen.setCosmetic(True) OptControls = namedtuple( "OptControls", ("shape", "auto_dim", "spin_x", "spin_y", "initialization", "start")) class Warning(OWWidget.Warning): ignoring_disc_variables = Msg("SOM ignores categorical variables.") missing_colors = \ Msg("Some data instances have undefined value of '{}'.") missing_values = \ Msg("{} data instance{} with undefined value(s) {} not shown.") single_attribute = Msg("Data contains a single numeric column.") class Error(OWWidget.Error): no_numeric_variables = Msg("Data contains no numeric columns.") no_defined_rows = Msg("All rows contain at least one undefined value.") def __init__(self): super().__init__() self.__pending_selection = self.selection self._optimizer = None self._optimizer_thread = None self.stop_optimization = False self.data = self.cont_x = None self.cells = self.member_data = None self.selection = None self.colors = self.thresholds = self.bin_labels = None box = gui.vBox(self.controlArea, box="SOM") shape = gui.comboBox(box, self, "", items=("Hexagonal grid", "Square grid")) shape.setCurrentIndex(1 - self.hexagonal) box2 = gui.indentedBox(box, 10) auto_dim = gui.checkBox(box2, self, "auto_dimension", "Set dimensions automatically", callback=self.on_auto_dimension_changed) self.manual_box = box3 = gui.hBox(box2) spinargs = dict(value="", widget=box3, master=self, minv=5, maxv=100, step=5, alignment=Qt.AlignRight) spin_x = gui.spin(**spinargs) spin_x.setValue(self.size_x) gui.widgetLabel(box3, "×") spin_y = gui.spin(**spinargs) spin_y.setValue(self.size_y) gui.rubber(box3) self.manual_box.setEnabled(not self.auto_dimension) initialization = gui.comboBox(box, self, "initialization", items=("Initialize with PCA", "Random initialization", "Replicable random")) start = gui.button(box, self, "Restart", callback=self.restart_som_pressed, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)) self.opt_controls = self.OptControls(shape, auto_dim, spin_x, spin_y, initialization, start) box = gui.vBox(self.controlArea, "Color") gui.comboBox(box, self, "attr_color", searchable=True, callback=self.on_attr_color_change, model=DomainModel(placeholder="(Same color)", valid_types=DomainModel.PRIMITIVE)) gui.checkBox(box, self, "pie_charts", label="Show pie charts", callback=self.on_pie_chart_change) gui.checkBox(box, self, "size_by_instances", label="Size by number of instances", callback=self.on_attr_size_change) gui.rubber(self.controlArea) self.scene = QGraphicsScene(self) self.view = SomView(self.scene) self.view.setMinimumWidth(400) self.view.setMinimumHeight(400) self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setRenderHint(QPainter.Antialiasing) self.view.selection_changed.connect(self.on_selection_change) self.view.selection_moved.connect(self.on_selection_move) self.view.selection_mark_changed.connect(self.on_selection_mark_change) self.mainArea.layout().addWidget(self.view) self.elements = None self.grid = None self.grid_cells = None self.legend = None @Inputs.data def set_data(self, data): def prepare_data(): if len(cont_attrs) < len(attrs): self.Warning.ignoring_disc_variables() if len(cont_attrs) == 1: self.Warning.single_attribute() x = Table.from_table(Domain(cont_attrs), data).X if sp.issparse(x): self.data = data self.cont_x = x.tocsr() else: mask = np.all(np.isfinite(x), axis=1) if not np.any(mask): self.Error.no_defined_rows() else: if np.all(mask): self.data = data self.cont_x = x.copy() else: self.data = data[mask] self.cont_x = x[mask] self.cont_x -= np.min(self.cont_x, axis=0)[None, :] sums = np.sum(self.cont_x, axis=0)[None, :] sums[sums == 0] = 1 self.cont_x /= sums def set_warnings(): missing = len(data) - len(self.data) if missing == 1: self.Warning.missing_values(1, "", "is") elif missing > 1: self.Warning.missing_values(missing, "s", "are") self.stop_optimization_and_wait() self.closeContext() self.clear() self.Error.clear() self.Warning.clear() if data is not None: attrs = data.domain.attributes cont_attrs = [var for var in attrs if var.is_continuous] if not cont_attrs: self.Error.no_numeric_variables() else: prepare_data() if self.data is not None: self.controls.attr_color.model().set_domain(data.domain) self.attr_color = data.domain.class_var set_warnings() self.openContext(self.data) self.set_color_bins() self.create_legend() self.recompute_dimensions() self.start_som() def clear(self): self.data = self.cont_x = None self.cells = self.member_data = None self.attr_color = None self.colors = self.thresholds = self.bin_labels = None if self.elements is not None: self.scene.removeItem(self.elements) self.elements = None self.clear_selection() self.controls.attr_color.model().set_domain(None) self.Warning.clear() self.Error.clear() def recompute_dimensions(self): if not self.auto_dimension or self.cont_x is None: return dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0]))))) self.opt_controls.spin_x.setValue(dim) self.opt_controls.spin_y.setValue(dim) def on_auto_dimension_changed(self): self.manual_box.setEnabled(not self.auto_dimension) if self.auto_dimension: self.recompute_dimensions() else: spin_x = self.opt_controls.spin_x spin_y = self.opt_controls.spin_y dimx = int(5 * np.round(spin_x.value() / 5)) dimy = int(5 * np.round(spin_y.value() / 5)) spin_x.setValue(dimx) spin_y.setValue(dimy) def on_attr_color_change(self): self.controls.pie_charts.setEnabled(self.attr_color is not None) self.set_color_bins() self.create_legend() self.rescale() self._redraw() def on_attr_size_change(self): self._redraw() def on_pie_chart_change(self): self._redraw() def clear_selection(self): self.selection = None self.redraw_selection() def on_selection_change(self, selection, action=SomView.SelectionSet): if self.data is None: # clicks on empty canvas return if self.selection is None: self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16) if action == SomView.SelectionSet: self.selection[:] = 0 self.selection[selection] = 1 elif action == SomView.SelectionAddToGroup: self.selection[selection] = max(1, np.max(self.selection)) elif action == SomView.SelectionNewGroup: self.selection[selection] = 1 + np.max(self.selection) elif action & SomView.SelectionRemove: self.selection[selection] = 0 self.redraw_selection() self.update_output() def on_selection_move(self, event: QKeyEvent): if self.selection is None or not np.any(self.selection): if event.key() in (Qt.Key_Right, Qt.Key_Down): x = y = 0 else: x = self.size_x - 1 y = self.size_y - 1 else: x, y = np.nonzero(self.selection) if len(x) > 1: return if event.key() == Qt.Key_Up and y > 0: y -= 1 if event.key() == Qt.Key_Down and y < self.size_y - 1: y += 1 if event.key() == Qt.Key_Left and x: x -= 1 if event.key() == Qt.Key_Right and x < self.size_x - 1: x += 1 x -= self.hexagonal and x == self.size_x - 1 and y % 2 if self.selection is not None and self.selection[x, y]: return selection = np.zeros(self.grid_cells.shape, dtype=bool) selection[x, y] = True self.on_selection_change(selection) def on_selection_mark_change(self, marks): self.redraw_selection(marks=marks) def redraw_selection(self, marks=None): if self.grid_cells is None: return sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2) sel_pen.setCosmetic(True) mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4) mark_pen.setCosmetic(True) pens = [self._grid_pen, sel_pen] mark_brush = QBrush(QColor(224, 255, 255)) sels = self.selection is not None and np.max(self.selection) palette = LimitedDiscretePalette(number_of_colors=sels + 1) brushes = [QBrush(Qt.NoBrush)] + \ [QBrush(palette[i].lighter(165)) for i in range(sels)] for y in range(self.size_y): for x in range(self.size_x - (y % 2) * self.hexagonal): cell = self.grid_cells[y, x] marked = marks is not None and marks[x, y] sel_group = self.selection is not None and self.selection[x, y] if marked: cell.setBrush(mark_brush) cell.setPen(mark_pen) else: cell.setBrush(brushes[sel_group]) cell.setPen(pens[bool(sel_group)]) cell.setZValue(marked or sel_group) def restart_som_pressed(self): if self._optimizer_thread is not None: self.stop_optimization = True self._optimizer.stop_optimization = True else: self.start_som() def start_som(self): self.read_controls() self.update_layout() self.clear_selection() if self.cont_x is not None: self.enable_controls(False) self._recompute_som() else: self.update_output() def read_controls(self): c = self.opt_controls self.hexagonal = c.shape.currentIndex() == 0 self.size_x = c.spin_x.value() self.size_y = c.spin_y.value() def enable_controls(self, enable): c = self.opt_controls c.shape.setEnabled(enable) c.auto_dim.setEnabled(enable) c.start.setText("Start" if enable else "Stop") def update_layout(self): self.set_legend_pos() if self.elements: # Prevent having redrawn grid but with old elements self.scene.removeItem(self.elements) self.elements = None self.redraw_grid() self.rescale() def _redraw(self): self.Warning.missing_colors.clear() if self.elements: self.scene.removeItem(self.elements) self.elements = None self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal) if self.cells is None: return sizes = self.cells[:, :, 1] - self.cells[:, :, 0] sizes = sizes.astype(float) if not self.size_by_instances: sizes[sizes != 0] = 0.8 else: sizes *= 0.8 / np.max(sizes) self.elements = QGraphicsItemGroup() self.scene.addItem(self.elements) if self.attr_color is None: self._draw_same_color(sizes) elif self.pie_charts: self._draw_pie_charts(sizes) else: self._draw_colored_circles(sizes) @property def _grid_factors(self): return (0.5, sqrt3_2) if self.hexagonal else (0, 1) def _draw_same_color(self, sizes): fx, fy = self._grid_factors color = QColor(64, 64, 64) for y in range(self.size_y): for x in range(self.size_x - self.hexagonal * (y % 2)): r = sizes[x, y] n = len(self.get_member_indices(x, y)) if not r: continue ellipse = ColoredCircle(r / 2, color, 0) ellipse.setPos(x + (y % 2) * fx, y * fy) ellipse.setToolTip(f"{n} instances") self.elements.addToGroup(ellipse) def _get_color_column(self): color_column = \ self.data.get_column_view(self.attr_color)[0].astype(float, copy=False) if self.attr_color.is_discrete: with np.errstate(invalid="ignore"): int_col = color_column.astype(int) int_col[np.isnan(color_column)] = len(self.colors) else: int_col = np.zeros(len(color_column), dtype=int) # The following line is unnecessary because rows with missing # numeric data are excluded. Uncomment it if you change SOM to # tolerate missing values. # int_col[np.isnan(color_column)] = len(self.colors) for i, thresh in enumerate(self.thresholds, start=1): int_col[color_column >= thresh] = i return int_col def _tooltip(self, colors, distribution): if self.attr_color.is_discrete: values = self.attr_color.values else: values = self._bin_names() tot = np.sum(distribution) nbhp = "\N{NON-BREAKING HYPHEN}" return '<table style="white-space: nowrap">' + "".join(f""" <tr> <td> <font color={color.name()}>■</font> <b>{escape(val).replace("-", nbhp)}</b>: </td> <td> {n} ({n / tot * 100:.1f} %) </td> </tr> """ for color, val, n in zip(colors, values, distribution) if n) \ + "</table>" def _draw_pie_charts(self, sizes): fx, fy = self._grid_factors color_column = self._get_color_column() colors = self.colors.qcolors_w_nan for y in range(self.size_y): for x in range(self.size_x - self.hexagonal * (y % 2)): r = sizes[x, y] if not r: self.grid_cells[y, x].setToolTip("") continue members = self.get_member_indices(x, y) color_dist = np.bincount(color_column[members], minlength=len(colors)) rel_color_dist = color_dist.astype(float) / len(members) pie = PieChart(rel_color_dist, r / 2, colors) pie.setToolTip(self._tooltip(colors, color_dist)) self.elements.addToGroup(pie) pie.setPos(x + (y % 2) * fx, y * fy) def _draw_colored_circles(self, sizes): fx, fy = self._grid_factors color_column = self._get_color_column() qcolors = self.colors.qcolors_w_nan for y in range(self.size_y): for x in range(self.size_x - self.hexagonal * (y % 2)): r = sizes[x, y] if not r: continue members = self.get_member_indices(x, y) color_dist = color_column[members] color_dist = color_dist[color_dist < len(self.colors)] if len(color_dist) != len(members): self.Warning.missing_colors(self.attr_color.name) bc = np.bincount(color_dist, minlength=len(self.colors)) color = qcolors[np.argmax(bc)] ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members)) ellipse.setPos(x + (y % 2) * fx, y * fy) ellipse.setToolTip(self._tooltip(qcolors, bc)) self.elements.addToGroup(ellipse) def redraw_grid(self): if self.grid is not None: self.scene.removeItem(self.grid) self.grid = QGraphicsItemGroup() self.grid.setZValue(-200) self.grid_cells = np.full((self.size_y, self.size_x), None) for y in range(self.size_y): for x in range(self.size_x - (y % 2) * self.hexagonal): if self.hexagonal: cell = QGraphicsPathItem(_hexagon_path) cell.setPos(x + (y % 2) / 2, y * sqrt3_2) else: cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1) self.grid_cells[y, x] = cell cell.setPen(self._grid_pen) self.grid.addToGroup(cell) self.scene.addItem(self.grid) def get_member_indices(self, x, y): i, j = self.cells[x, y] return self.member_data[i:j] def _recompute_som(self): if self.cont_x is None: return som = SOM(self.size_x, self.size_y, hexagonal=self.hexagonal, pca_init=self.initialization == 0, random_seed=0 if self.initialization == 2 else None) class Optimizer(QObject): update = Signal(float, np.ndarray, np.ndarray) done = Signal(SOM) stopped = Signal() stop_optimization = False def __init__(self, data, som): super().__init__() self.som = som self.data = data def callback(self, progress): self.update.emit(progress, self.som.weights.copy(), self.som.ssum_weights.copy()) return not self.stop_optimization def run(self): try: self.som.fit(self.data, N_ITERATIONS, callback=self.callback) # Report an exception, but still remove the thread finally: self.done.emit(self.som) self.stopped.emit() def thread_finished(): self._optimizer = None self._optimizer_thread = None self.progressBarInit() self._optimizer = Optimizer(self.cont_x, som) self._optimizer_thread = QThread() self._optimizer_thread.setStackSize(5 * 2**20) self._optimizer.update.connect(self.__update) self._optimizer.done.connect(self.__done) self._optimizer.stopped.connect(self._optimizer_thread.quit) self._optimizer.moveToThread(self._optimizer_thread) self._optimizer_thread.started.connect(self._optimizer.run) self._optimizer_thread.finished.connect(thread_finished) self.stop_optimization = False self._optimizer_thread.start() @Slot(float, object, object) def __update(self, _progress, weights, ssum_weights): self.progressBarSet(_progress) self._assign_instances(weights, ssum_weights) self._redraw() @Slot(object) def __done(self, som): self.enable_controls(True) self.progressBarFinished() self._assign_instances(som.weights, som.ssum_weights) self._redraw() # This is the first time we know what was selected (assuming that # initialization is not set to random) if self.__pending_selection is not None: self.on_selection_change(self.__pending_selection) self.__pending_selection = None self.update_output() def stop_optimization_and_wait(self): if self._optimizer_thread is not None: self.stop_optimization = True self._optimizer.stop_optimization = True self._optimizer_thread.quit() self._optimizer_thread.wait() self._optimizer_thread = None def onDeleteWidget(self): self.stop_optimization_and_wait() self.clear() super().onDeleteWidget() def _assign_instances(self, weights, ssum_weights): if self.cont_x is None: return # the widget is shutting down while signals still processed assignments = SOM.winner_from_weights(self.cont_x, weights, ssum_weights, self.hexagonal) members = defaultdict(list) for i, (x, y) in enumerate(assignments): members[(x, y)].append(i) members.pop(None, None) self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int) self.member_data = np.empty(self.cont_x.shape[0], dtype=int) index = 0 for x in range(self.size_x): for y in range(self.size_y): nmembers = len(members[(x, y)]) self.member_data[index:index + nmembers] = members[(x, y)] self.cells[x, y] = [index, index + nmembers] index += nmembers def resizeEvent(self, event): super().resizeEvent(event) self.create_legend() # re-wrap lines if necessary self.rescale() def rescale(self): if self.legend: leg_height = self.legend.boundingRect().height() leg_extra = 1.5 else: leg_height = 0 leg_extra = 1 vw, vh = self.view.width(), self.view.height() - leg_height scale = min(vw / (self.size_x + 1), vh / ((self.size_y + leg_extra) * self._grid_factors[1])) self.view.setTransform(QTransform.fromScale(scale, scale)) if self.hexagonal: self.view.setSceneRect(0, -1, self.size_x - 1, (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale) else: self.view.setSceneRect(-0.25, -0.25, self.size_x - 0.5, self.size_y - 0.5 + leg_height / scale) def update_output(self): if self.data is None: self.Outputs.selected_data.send(None) self.Outputs.annotated_data.send(None) return indices = np.zeros(len(self.data), dtype=int) if self.selection is not None and np.any(self.selection): for y in range(self.size_y): for x in range(self.size_x): rows = self.get_member_indices(x, y) indices[rows] = self.selection[x, y] if np.any(indices): sel_data = create_groups_table(self.data, indices, False, "Group") self.Outputs.selected_data.send(sel_data) else: self.Outputs.selected_data.send(None) if np.max(indices) > 1: annotated = create_groups_table(self.data, indices) else: annotated = create_annotated_table(self.data, np.flatnonzero(indices)) self.Outputs.annotated_data.send(annotated) def set_color_bins(self): if self.attr_color is None: self.thresholds = self.bin_labels = self.colors = None elif self.attr_color.is_discrete: self.thresholds = self.bin_labels = None self.colors = self.attr_color.palette else: col = self.data.get_column_view(self.attr_color)[0].astype(float) if self.attr_color.is_time: binning = time_binnings(col, min_bins=4)[-1] else: binning = decimal_binnings(col, min_bins=4)[-1] self.thresholds = binning.thresholds[1:-1] self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1]) palette = BinnedContinuousPalette.from_palette( self.attr_color.palette, binning.thresholds) self.colors = palette def create_legend(self): if self.legend is not None: self.scene.removeItem(self.legend) self.legend = None if self.attr_color is None: return if self.attr_color.is_discrete: names = self.attr_color.values else: names = self._bin_names() items = [] size = 8 for name, color in zip(names, self.colors.qcolors): item = QGraphicsItemGroup() item.addToGroup( CanvasRectangle(None, -size / 2, -size / 2, size, size, Qt.gray, color)) item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter)) items.append(item) self.legend = wrap_legend_items(items, hspacing=20, vspacing=16 + size, max_width=self.view.width() - 25) self.legend.setFlags(self.legend.ItemIgnoresTransformations) self.legend.setTransform( QTransform.fromTranslate(-self.legend.boundingRect().width() / 2, 0)) self.scene.addItem(self.legend) self.set_legend_pos() def _bin_names(self): labels, short_labels = self.bin_labels return \ [f"< {labels[0]}"] \ + [f"{x} - {y}" for x, y in zip(labels, short_labels[1:])] \ + [f"≥ {labels[-1]}"] def set_legend_pos(self): if self.legend is None: return self.legend.setPos(self.size_x / 2, (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1]) def send_report(self): self.report_plot() if self.attr_color: self.report_caption( f"Self-organizing map colored by '{self.attr_color.name}'")
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 = "箱形图" description = "在方框图中可视化特征值的分布" icon = "icons/BoxPlot.svg" priority = 100 keywords = ["whisker"] class Inputs: data = Input("Data", Orange.data.Table) class Outputs: selected_data = Output("所选数据", 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 = "盒式布景" 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 self.attrs = VariableListModel() view = gui.listView(self.controlArea, self, "attribute", box="变量", 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", "按相关性排序", tooltip="由𝜒²或方差对子群排序", 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="子群", 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=["无比较", "中位数比较", "均值比较"], 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="显示", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox(box, self, 'stretched', "拉杆", callback=self.display_changed) gui.checkBox(box, self, 'show_labels', "显示框标签", callback=self.display_changed) self.sort_cb = gui.checkBox(box, self, 'sort_freqs', "按子组频率排序", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "选择发送", "自动发送") 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>没有测试结果。</center>") self.mainArea.setMinimumWidth(600) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box() def sizeHint(self): return QSize(100, 500) # Vertical size is regulated by mainArea 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 def stat_ttest(): d1, d2 = self.stats if d1.n == 0 or d2.n == 0: return np.nan, np.nan pooled_var = d1.var / d1.n + d2.var / d2.n df = pooled_var ** 2 / \ ((d1.var / d1.n) ** 2 / (d1.n - 1) + (d2.var / d2.n) ** 2 / (d2.n - 1)) if pooled_var == 0: return np.nan, np.nan 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) 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, self.p = tests.wilcoxon_rank_sum( # self.stats[0].dist, self.stats[1].dist) # t = "Mann-Whitney's z: %.1f (p=%.3f)" % (z, self.p) else: t, self.p = stat_ttest() t = "Student's t: %.3f (p=%.3f)" % (t, self.p) else: if self.compare == OWBoxPlot.CompareMedians: t = "" # U, self.p = -1, -1 # t = "Kruskal Wallis's U: %.1f (p=%.3f)" % (U, self.p) else: F, self.p = stat_ANOVA() t = "ANOVA: %.3f (p=%.3f)" % (F, self.p) 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 initStyleOptionForIndex(self, option: QStyleOptionHeader, logicalIndex: int) -> None: """ Similar to initStyleOptionForIndex in Qt 6.0 with the difference that `isSectionSelected` is not used, only `sectionIntersectsSelection` is used (isSectionSelected will scan the entire model column/row when the whole column/row is selected). """ hover = self.logicalIndexAt(self.mapFromGlobal(QCursor.pos())) pressed = self.__pressed if self.highlightSections(): is_selected = self.__sectionIntersectsSelection else: is_selected = lambda _: False state = QStyle.State_None if self.isEnabled(): state |= QStyle.State_Enabled if self.window().isActiveWindow(): state |= QStyle.State_Active if self.sectionsClickable(): if logicalIndex == hover: state |= QStyle.State_MouseOver if logicalIndex == pressed: state |= QStyle.State_Sunken if self.highlightSections(): if is_selected(logicalIndex): state |= QStyle.State_On if self.isSortIndicatorShown() and \ self.sortIndicatorSection() == logicalIndex: option.sortIndicator = ( QStyleOptionHeader.SortDown if self.sortIndicatorOrder() == Qt.AscendingOrder else QStyleOptionHeader.SortUp) style = self.style() model = self.model() orientation = self.orientation() textAlignment = model.headerData(logicalIndex, self.orientation(), Qt.TextAlignmentRole) defaultAlignment = self.defaultAlignment() textAlignment = (textAlignment if isinstance(textAlignment, int) else defaultAlignment) option.section = logicalIndex option.state = QStyle.State(int(option.state) | int(state)) option.textAlignment = Qt.Alignment(int(textAlignment)) option.iconAlignment = Qt.AlignVCenter text = model.headerData(logicalIndex, self.orientation(), Qt.DisplayRole) text = str(text) if text is not None else "" option.text = text icon = model.headerData(logicalIndex, self.orientation(), Qt.DecorationRole) try: option.icon = QIcon(icon) except (TypeError, ValueError): # pragma: no cover pass margin = 2 * style.pixelMetric(QStyle.PM_HeaderMargin, None, self) headerArrowAlignment = style.styleHint(QStyle.SH_Header_ArrowAlignment, None, self) isHeaderArrowOnTheSide = headerArrowAlignment & Qt.AlignVCenter if self.isSortIndicatorShown() and \ self.sortIndicatorSection() == logicalIndex \ and isHeaderArrowOnTheSide: margin += style.pixelMetric(QStyle.PM_HeaderMarkSize, None, self) if not option.icon.isNull(): margin += style.pixelMetric(QStyle.PM_SmallIconSize, None, self) margin += style.pixelMetric(QStyle.PM_HeaderMargin, None, self) if self.textElideMode() != Qt.ElideNone: elideMode = self.textElideMode() if hasattr(option, 'textElideMode'): # Qt 6.0 option.textElideMode = elideMode # pragma: no cover else: option.text = option.fontMetrics.elidedText( option.text, elideMode, option.rect.width() - margin) foregroundBrush = model.headerData(logicalIndex, orientation, Qt.ForegroundRole) try: foregroundBrush = QBrush(foregroundBrush) except (TypeError, ValueError): pass else: option.palette.setBrush(QPalette.ButtonText, foregroundBrush) backgroundBrush = model.headerData(logicalIndex, orientation, Qt.BackgroundRole) try: backgroundBrush = QBrush(backgroundBrush) except (TypeError, ValueError): pass else: option.palette.setBrush(QPalette.Button, backgroundBrush) option.palette.setBrush(QPalette.Window, backgroundBrush) # the section position visual = self.visualIndex(logicalIndex) assert visual != -1 first = self.__isFirstVisibleSection(visual) last = self.__isLastVisibleSection(visual) if first and last: option.position = QStyleOptionHeader.OnlyOneSection elif first: option.position = QStyleOptionHeader.Beginning elif last: option.position = QStyleOptionHeader.End else: option.position = QStyleOptionHeader.Middle option.orientation = orientation # the selected position (in QHeaderView this is always computed even if # highlightSections is False). if self.highlightSections(): previousSelected = is_selected(self.logicalIndex(visual - 1)) nextSelected = is_selected(self.logicalIndex(visual + 1)) else: previousSelected = nextSelected = False if previousSelected and nextSelected: option.selectedPosition = QStyleOptionHeader.NextAndPreviousAreSelected elif previousSelected: option.selectedPosition = QStyleOptionHeader.PreviousIsSelected elif nextSelected: option.selectedPosition = QStyleOptionHeader.NextIsSelected else: option.selectedPosition = QStyleOptionHeader.NotAdjacent
def mark_problematic_reader(): self.reader_combo.setItemData(self.reader_combo.currentIndex(), QBrush(Qt.red), Qt.ForegroundRole)
def _setup_plot(self): def get_minmax(points): minmax = [float('inf'), float('-inf'), float('inf'), float('-inf')] for pp in points: for p in pp: minmax[0] = min(p[0], minmax[0]) minmax[1] = max(p[0], minmax[1]) minmax[2] = min(p[1], minmax[2]) minmax[3] = max(p[1], minmax[3]) return minmax self.plot.clear() points = self.ca variables = self.selected_vars() colors = colorpalette.ColorPaletteGenerator(len(variables)) p_axes = self._p_axes() if points is None: return if len(variables) == 2: row_points = self.ca.row_factors[:, p_axes] col_points = self.ca.col_factors[:, p_axes] points = [row_points, col_points] else: points = self.ca.row_factors[:, p_axes] counts = [len(var.values) for var in variables] range_indices = np.cumsum([0] + counts) ranges = zip(range_indices, range_indices[1:]) points = [points[s:e] for s, e in ranges] minmax = get_minmax(points) margin = abs(minmax[0] - minmax[1]) margin = margin * 0.05 if margin > 1e-10 else 1 self.plot.setXRange(minmax[0] - margin, minmax[1] + margin) margin = abs(minmax[2] - minmax[3]) margin = margin * 0.05 if margin > 1e-10 else 1 self.plot.setYRange(minmax[2] - margin, minmax[3] + margin) for i, (v, points) in enumerate(zip(variables, points)): color_outline = colors[i] color_outline.setAlpha(200) color = QColor(color_outline) color.setAlpha(120) item = ScatterPlotItem( x=points[:, 0], y=points[:, 1], brush=QBrush(color), pen=pg.mkPen(color_outline.darker(120), width=1.5), size=np.full((points.shape[0], ), 10.1), ) self.plot.addItem(item) for name, point in zip(v.values, points): item = pg.TextItem(name, anchor=(0.5, 0)) self.plot.addItem(item) item.setPos(point[0], point[1]) inertia = self.ca.inertia_of_axis() if np.sum(inertia) == 0: inertia = 100 * inertia else: inertia = 100 * inertia / np.sum(inertia) ax = self.plot.getAxis("bottom") ax.setLabel("Component {} ({:.1f}%)".format(p_axes[0] + 1, inertia[p_axes[0]])) ax = self.plot.getAxis("left") ax.setLabel("Component {} ({:.1f}%)".format(p_axes[1] + 1, inertia[p_axes[1]]))
def paintEvent(self, event): painter = QStylePainter(self) rect = self._subControlRect(QStyle.SC_SliderGroove) is_horizontal = self.orientation() == Qt.Horizontal minpos, maxpos = self.minimumPosition(), self.maximumPosition() span = rect.width() if is_horizontal else rect.height() x1 = QStyle.sliderPositionFromValue( self.minimum(), self.maximum(), minpos, span, self.invertedAppearance()) x2 = QStyle.sliderPositionFromValue( self.minimum(), self.maximum(), maxpos, span, self.invertedAppearance()) # Background painter.fillRect(rect, Qt.white) # Highlight painter.setOpacity(.7) if is_horizontal: painter.fillRect(x1, rect.y(), x2 - x1, rect.height(), Qt.yellow) else: painter.fillRect(rect.x(), x1, rect.width(), x2 - x1, Qt.yellow) painter.setOpacity(1) # Histogram if self._pixmap: painter.drawPixmap(rect, self._pixmap, self._pixmap.rect()) # Frame painter.setPen(QPen(QBrush(Qt.darkGray), 2)) painter.drawRect(rect) # Handles painter.setPen(QPen(QBrush(self._HANDLE_COLOR), self._HANDLE_WIDTH)) painter.setOpacity(9) if is_horizontal: painter.drawLine(x1, rect.y(), x1, rect.y() + rect.height()) painter.drawLine(x2, rect.y(), x2, rect.y() + rect.height()) else: painter.drawLine(rect.x(), x1, rect.x() + rect.width(), x1) painter.drawLine(rect.x(), x2, rect.x() + rect.width(), x2) painter.setOpacity(1) if self._show_text: painter.setFont(QFont('Monospace', 7, QFont.Bold)) strMin, strMax = self.formatValues(minpos, maxpos) widthMin = painter.fontMetrics().width(strMin) widthMax = painter.fontMetrics().width(strMax) height = painter.fontMetrics().height() is_enough_space = x2 - x1 > 3 + (max(widthMax, widthMin) if is_horizontal else (2 * height + self._HANDLE_WIDTH)) if is_enough_space: if is_horizontal: painter.drawText(x1 + 3, rect.y() + height, strMin) painter.drawText(x2 - widthMax - 2, rect.y() + rect.height() - 3, strMax) else: painter.drawText(rect.x() + 1, x1 + height, strMin) painter.drawText(rect.x() + rect.width() - widthMax - 1, x2 - 2, strMax) # Tooltip if self._showControlTooltip\ and (not self._show_text or not is_enough_space): # (show control-drag tooltip) painter.setFont(QFont('Monospace', 10, QFont.Normal)) text = "Hold {} to move time interval" \ .format("Cmd" if sys.platform == "darwin" else "Ctrl") w = painter.fontMetrics().width(text) h = painter.fontMetrics().height() brush = QColor(224, 224, 224, 212) pen = QPen(Qt.NoPen) rect = QRect(4, 4, w + 8, h + 4) painter.setBrush(brush) painter.setPen(pen) painter.drawRect(rect) painter.setPen(Qt.black) painter.drawText(8, 4 + h, text)
def __init__(self, *args): QGraphicsEllipseItem.__init__(self, *args) self.setRect(-3.5, -3.5, 7., 7.) self.setPen(QPen(Qt.NoPen)) self.setBrush(QBrush(QColor("#9CACB4"))) self.__hover = False
def __init__(self, pen = QPen(Qt.black), brush = QBrush(Qt.white), xData = None, yData = None, tooltip = None): OWCurve.__init__(self, xData, yData, tooltip=tooltip) self.set_pen(pen) self.set_brush(brush) self._item = QGraphicsRectItem(self)
def __init__(self, pen = QPen(Qt.black), brush = QBrush(Qt.white), xData = [], yData = [], tooltip = None): OWCurve.__init__(self, xData, yData, tooltip=tooltip) self._data_polygon = self.polygon_from_data(xData, yData) self._polygon_item = QGraphicsPolygonItem(self) self.set_pen(pen) self.set_brush(brush)
from AnyQt.QtGui import (QBrush, QPen, QColor, QPainter, QPainterPath, QTransform) from AnyQt.QtWidgets import (QGraphicsItem, QGraphicsEllipseItem, QGraphicsRectItem, QGraphicsTextItem, QGraphicsLineItem, QGraphicsScene, QGraphicsView, QStyle, QSizePolicy, QFormLayout) from AnyQt.QtCore import (Qt, QRectF, QSize, QPointF, QLineF, QTimer, pyqtSignal, pyqtProperty) from Orange.widgets import gui from Orange.widgets.widget import OWWidget from Orange.widgets.settings import Setting DefDroppletBrush = QBrush(Qt.darkGray) class GraphNode: def __init__(self, *_, **kwargs): self.edges = kwargs.get("edges", set()) def graph_edges(self): return self.edges def graph_add_edge(self, edge): self.edges.add(edge) def __iter__(self): for edge in self.edges: yield edge.node2 def graph_nodes(self, atype=1):
def update_rect(self): if self.colors is None: self.rect.setBrush(QBrush(Qt.white)) else: self.rect.setBrush(QBrush(self.gradient))
def __init__(self, *args): super().__init__(*args) self.setAcceptHoverEvents(True) self.setAcceptedMouseButtons(Qt.LeftButton) self.setBrush(QBrush(Qt.gray)) self.setPen(Qt.white)
class OWBoxPlot(widget.OWWidget): 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) class Warning(widget.OWWidget.Warning): no_vars = widget.Msg( "Data contains no categorical or numeric variables") buttons_area_orientation = None #: Comparison types for continuous variables CompareNone, CompareMedians, CompareMeans = 0, 1, 2 settingsHandler = DomainContextHandler() # If this was a list, context handler would try to match its elements to # variable names! selection = ContextSetting((), schema_only=True) attribute = ContextSetting(None) order_by_importance = Setting(False) order_grouping_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) _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)) _attr_brush = QBrush(QColor(0x33, 0x00, 0xff)) graph_name = "box_scene" def __init__(self): super().__init__() self._axis_font = QFont() self._axis_font.setPixelSize(12) self._label_font = QFont() self._label_font.setPixelSize(11) self.dataset = None self.stats = [] self.dist = self.conts = 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 = 1 self.scene_min_x = self.scene_max_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() sorted_model = SortProxyModel(sortRole=Qt.UserRole) sorted_model.setSourceModel(self.attrs) sorted_model.sort(0) box = gui.vBox(self.controlArea, "Variable") view = self.attr_list = ListViewSearch() view.setModel(sorted_model) view.setSelectionMode(view.SingleSelection) view.selectionModel().selectionChanged.connect(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) box.layout().addWidget(view) gui.checkBox(box, self, "order_by_importance", "Order by relevance to subgroups", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_attr_sorting) self.group_vars = VariableListModel(placeholder="None") sorted_model = SortProxyModel(sortRole=Qt.UserRole) sorted_model.setSourceModel(self.group_vars) sorted_model.sort(0) box = gui.vBox(self.controlArea, "Subgroups") view = self.group_list = ListViewSearch() view.setModel(sorted_model) view.selectionModel().selectionChanged.connect(self.grouping_changed) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) box.layout().addWidget(view) gui.checkBox(box, self, "order_grouping_by_importance", "Order by relevance to variable", tooltip="Order by 𝜒² or ANOVA over the variable values", callback=self.apply_group_sorting) # 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)) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.update_graph) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.update_graph) # 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.update_graph, stateWhenDisabled=False) gui.checkBox(box, self, 'show_labels', "Show box labels", callback=self.update_graph) self.sort_cb = gui.checkBox(box, self, 'sort_freqs', "Sort by subgroup frequencies", callback=self.update_graph, stateWhenDisabled=False) gui.vBox(self.mainArea) self.box_scene = QGraphicsScene(self) self.box_scene.selectionChanged.connect(self.on_selection_changed) 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) self.stat_test = "" self.mainArea.setMinimumWidth(300) self.update_box_visibilities() 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.update_graph() return super().eventFilter(obj, event) @property def show_stretched(self): return self.stretched and self.group_var is not self.attribute def reset_attrs(self): domain = self.dataset.domain self.attrs[:] = [ var for var in chain(domain.class_vars, domain.metas, domain.attributes) if var.is_primitive() and not var.attributes.get("hidden", False) ] def reset_groups(self): domain = self.dataset.domain self.group_vars[:] = [None] + [ var for var in chain(domain.class_vars, domain.metas, domain.attributes) if var.is_discrete and not var.attributes.get("hidden", False) ] @Inputs.data def set_data(self, dataset): self.closeContext() self._reset_all_data() if dataset and not (len(dataset.domain.variables) or any(var.is_primitive() for var in dataset.domain.metas)): self.Warning.no_vars() dataset = None self.dataset = dataset if dataset: self.reset_attrs() self.reset_groups() self._select_default_variables() self.openContext(self.dataset) self._set_list_view_selections() self.compute_box_data() self.apply_attr_sorting() self.apply_group_sorting() self.update_graph() self.select_box_items() self.update_box_visibilities() self.commit() def _reset_all_data(self): self.clear_scene() self.Warning.no_vars.clear() self.stats = [] self.dist = self.conts = None self.group_var = None self.attribute = None self.stat_test = "" self.attrs[:] = [] self.group_vars[:] = [None] self.selection = () def _select_default_variables(self): # visualize first non-class variable, group by class (if present) domain = self.dataset.domain 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 def _set_list_view_selections(self): for view, var, callback in ((self.attr_list, self.attribute, self.attr_changed), (self.group_list, self.group_var, self.grouping_changed)): src_model = view.model().sourceModel() if var not in src_model: continue sel_model = view.selectionModel() sel_model.selectionChanged.disconnect(callback) row = src_model.indexOf(var) index = view.model().index(row, 0) sel_model.select(index, sel_model.ClearAndSelect) self._ensure_selection_visible(view) sel_model.selectionChanged.connect(callback) def apply_attr_sorting(self): def compute_score(attr): # This function and the one in apply_group_sorting are similar, but # different in too many details, so they are kept as separate # functions. # If you discover a bug in this function, check the other one, too. 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) > 1] p = f_oneway(*groups)[1] if len(groups) > 1 else 2 else: p = self._chi_square(group_var, attr)[1] if math.isnan(p): return 2 return p data = self.dataset if data is None: return domain = data.domain 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._sort_list(self.attrs, self.attr_list, compute_score) else: self._sort_list(self.attrs, self.attr_list, None) def apply_group_sorting(self): def compute_stat(group): # This function and the one in apply_attr_sorting are similar, but # different in too many details, so they are kept as separate # functions. # If you discover a bug in this function, check the other one, too. if group is attr: return 3 if group is None: return -1 if attr.is_continuous: groups = self._group_cols(data, group, attr_col) groups = [group for group in groups if len(group) > 1] p = f_oneway(*groups)[1] if len(groups) > 1 else 2 else: p = self._chi_square(group, attr)[1] if math.isnan(p): return 2 return p data = self.dataset if data is None: return attr = self.attribute if self.order_grouping_by_importance: if attr.is_continuous: attr_col = data.get_column_view(attr)[0].astype(float) self._sort_list(self.group_vars, self.group_list, compute_stat) else: self._sort_list(self.group_vars, self.group_list, None) def _sort_list(self, source_model, view, key=None): if key is None: c = count() def key(_): # pylint: disable=function-redefined return next(c) for i, attr in enumerate(source_model): source_model.setData(source_model.index(i), key(attr), Qt.UserRole) self._ensure_selection_visible(view) @staticmethod def _ensure_selection_visible(view): selection = view.selectedIndexes() if len(selection) == 1: view.scrollTo(selection[0]) def _chi_square(self, group_var, attr): # Chi-square with the given distribution into groups if not attr.values or not group_var.values: return 0, 2, 0 observed = np.array( contingency.get_contingency(self.dataset, group_var, attr)) observed = observed[observed.sum(axis=1) != 0, :] observed = observed[:, observed.sum(axis=0) != 0] if min(observed.shape) < 2: return 0, 2, 0 return chi2_contingency(observed)[:3] def grouping_changed(self, selected): if not selected: return # should never come here self.group_var = selected.indexes()[0].data(gui.TableVariable) self._variables_changed(self.apply_attr_sorting) def attr_changed(self, selected): if not selected: return # should never come here self.attribute = selected.indexes()[0].data(gui.TableVariable) self._variables_changed(self.apply_group_sorting) def _variables_changed(self, sorting): self.selection = () self.compute_box_data() sorting() self.update_graph() self.update_box_visibilities() self.commit() def update_graph(self): pending_selection = self.selection self.box_scene.selectionChanged.disconnect(self.on_selection_changed) try: # not for exceptions, just to reconnect after all possible paths self.clear_scene() if self.dataset is None or self.attribute is None: return if self.attribute.is_continuous: self._display_changed_cont() else: self._display_changed_disc() self.selection = pending_selection self.draw_stat() self.select_box_items() if self.attribute.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) finally: self.box_scene.selectionChanged.connect(self.on_selection_changed) def select_box_items(self): selection = set(self.selection) for box in self.box_scene.items(): if isinstance(box, FilterGraphicsRectItem): box.setSelected(box.data_range in selection) def _group_cols(self, data, group, attr): if isinstance(attr, np.ndarray): attr_col = attr else: attr_col = data.get_column_view(group)[0].astype(float) group_col = data.get_column_view(group)[0].astype(float) groups = [attr_col[group_col == i] for i in range(len(group.values))] groups = [col[~np.isnan(col)] for col in groups] return groups def compute_box_data(self): attr = self.attribute if not attr: return dataset = self.dataset if dataset is None \ or not attr.is_continuous and not attr.values \ or self.group_var and not self.group_var.values: self.stats = [] self.dist = self.conts = None return if self.group_var: self.dist = None missing_val_str = f"missing '{self.group_var.name}'" group_var_labels = self.group_var.values + ("", ) if self.attribute.is_continuous: stats, label_texts = [], [] attr_col = dataset.get_column_view(attr)[0].astype(float) for group, value in \ zip(self._group_cols(dataset, self.group_var, attr_col), group_var_labels): if group.size: stats.append(BoxData(group, value)) label_texts.append(value or missing_val_str) self.stats = stats self.label_txts_all = label_texts else: self.conts = contingency.get_contingency( dataset, attr, self.group_var) self.label_txts_all = [ v or missing_val_str for v, c in zip( group_var_labels, self.conts.array_with_unknowns) if np.sum(c) > 0 ] else: self.conts = None if self.attribute.is_continuous: attr_col = dataset.get_column_view(attr)[0].astype(float) self.stats = [BoxData(attr_col)] else: self.dist = distribution.get_distribution(dataset, attr) 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_box_visibilities(self): self.controls.stretched.setDisabled(self.group_var is self.attribute) if not self.attribute: self.stretching_box.hide() self.display_box.hide() elif self.attribute.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.box_scene.clear() self.box_view.viewport().update() self.attr_labels = [] self.labels = [] self.boxes = [] self.mean_labels = [] self.posthoc_lines = [] def _display_changed_cont(self): self.mean_labels = [ self.mean_label(stat, self.attribute, 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, self.attribute, 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.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_cont() self._show_posthoc() def _display_changed_disc(self): self.clear_scene() self.attr_labels = [ QGraphicsSimpleTextItem(lab) for lab in self.label_txts_all ] if not self.show_stretched: if self.group_var: self.labels = [ QGraphicsTextItem("{}".format(int(sum(cont)))) for cont in self.conts.array_with_unknowns 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: conts = self.conts.array_with_unknowns self.boxes = [ self.strudel(cont, val) for cont, val in zip(conts, self.group_var.values + ("", )) if np.sum(cont) > 0 ] sums_ = np.sum(conts, axis=1) sums_ = sums_[sums_ > 0] # only bars with sum > 0 are shown if self.sort_freqs: # pylint: disable=invalid-unary-operand-type self.order = sorted(self.order, key=(-sums_).__getitem__) else: conts = self.dist.array_with_unknowns self.boxes = [self.strudel(conts)] sums_ = [np.sum(conts)] 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.show_stretched: self.__draw_row_counts(y, self.labels[box_index], sums_[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._compute_tests_disc() 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, label, row_sum_): """Draw row counts Parameters ---------- y: int vertical offset of bars label: QGraphicsSimpleTextItem Label for group row_sum_: int Sum for the group """ assert not self.attribute.is_continuous b = label.boundingRect() right = self.scale_x * row_sum_ 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 """ 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_cont(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 n = len(self.dataset) 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}, N={n})" 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}, N={n})" self.stat_test = t def _compute_tests_disc(self): if self.group_var is None or self.attribute is None: self.stat_test = "" else: chi, p, dof = self._chi_square(self.group_var, self.attribute) if np.isnan(p): self.stat_test = "" else: self.stat_test = f"χ²: {chi:.2f} (p={p:.3f}, dof={dof})" def mean_label(self, stat, attr, val_name): label = QGraphicsItemGroup() t = QGraphicsSimpleTextItem(attr.str_val(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.]), 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_max_x = gtop * scale_x self.scene_width = self.scene_max_x - self.scene_min_x val = first_val last_text = self.scene_min_x while True: l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1, self._pen_axis_tick) l.setZValue(100) t = QGraphicsSimpleTextItem( self.attribute.str_val(val) if not misssing_stats else "?") t.setFont(self._axis_font) t.setFlag(QGraphicsItem.ItemIgnoresTransformations) r = t.boundingRect() x_start = val * scale_x - r.width() / 2 x_finish = x_start + r.width() if x_start > last_text + 10 and x_finish < self.scene_max_x: t.setPos(x_start, 8) self.box_scene.addItem(t) last_text = x_finish 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_stat(self): if self.stat_test: label = QGraphicsSimpleTextItem(self.stat_test) brect = self.box_scene.sceneRect() label.setPos(brect.center().x() - label.boundingRect().width() / 2, 8 + self._axis_font.pixelSize() * 2) label.setFlag(QGraphicsItem.ItemIgnoresTransformations) self.box_scene.addItem(label) def draw_axis_disc(self): """ Draw the horizontal axis and sets self.scale_x for discrete attributes """ assert not self.attribute.is_continuous if self.show_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.array_with_unknowns) else: max_box = float(np.sum(self.dist.array_with_unknowns)) 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.show_stretched and self.labels: if self.group_var: rows = list(zip(self.conts.array_with_unknowns, 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.show_stretched: self.scale_x *= 100 def label_group(self, stat, attr, mean_lab): def centered_text(val, pos): t = QGraphicsSimpleTextItem(attr.str_val(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 or stat.q75 is not None: # if any of them is None it means that its value is equal to median box_from = stat.median if stat.q25 is None else stat.q25 box_to = stat.median if stat.q75 is None else stat.q75 mbox = FilterGraphicsRectItem(stat.data_range, box_from * scale_x, -height / 2, (box_to - box_from) * 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=None): attr = self.attribute ss = np.sum(dist) box = [] if ss < 1e-6: cond = DiscDataRange(None, group_val) box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10)) cum = 0 missing_val_str = f"missing '{attr.name}'" values = attr.values + ("", ) colors = attr.palette.qcolors_w_nan total = sum(dist) for freq, value, color in zip(dist, values, colors): if freq < 1e-6: continue v = freq if self.show_stretched: v /= ss v *= self.scale_x cond = DiscDataRange(value, group_val) rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12) rect.setBrush(QBrush(color)) rect.setPen(QPen(Qt.NoPen)) value = value or missing_val_str if self.show_stretched: tooltip = f"{value}: {100 * freq / total:.2f}%" else: tooltip = f"{value}: ({int(freq)})" rect.setToolTip(tooltip) text = QGraphicsTextItem(value) box.append(rect) box.append(text) cum += v return box def on_selection_changed(self): self.selection = tuple(item.data_range for item in self.box_scene.selectedItems() if item.data_range) self.commit() def commit(self): conditions = self._gather_conditions() if conditions: selected = Values(conditions, conjunction=False)(self.dataset) selection = np.in1d(self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0] else: selected, selection = None, [] self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send( create_annotated_table(self.dataset, selection)) def _gather_conditions(self): conditions = [] attr = self.attribute group_attr = self.group_var for data_range in self.selection: if attr.is_discrete: # If some value was removed from the data (in case settings are # loaded from a scheme), do not include the corresponding # filter; this is appropriate since data with such value does # not exist anyway if not data_range.value: condition = IsDefined([attr], negate=True) elif data_range.value not in attr.values: continue else: condition = FilterDiscrete(attr, [data_range.value]) else: condition = FilterContinuous(attr, FilterContinuous.Between, data_range.low, data_range.high) if data_range.group_value: if not data_range.group_value: grp_filter = IsDefined([group_attr], negate=True) elif data_range.group_value not in group_attr.values: continue else: grp_filter = FilterDiscrete(group_attr, [data_range.group_value]) condition = Values([condition, grp_filter], conjunction=True) conditions.append(condition) return conditions 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, 0) 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, int(width)) painter.drawText( int(option.rect.x()), int(option.rect.y() + self.boundingRect().height() - self.PADDING), text)
def hoverEnterEvent(self, event): super().hoverEnterEvent(event) self.setBrush(QBrush(QColor(100, 100, 100))) self.update()
def text_format(foreground=Qt.black, weight=QFont.Normal): fmt = QTextCharFormat() fmt.setForeground(QBrush(foreground)) fmt.setFontWeight(weight) return fmt
def hoverLeaveEvent(self, event): super().hoverLeaveEvent(event) self.setBrush(QBrush(QColor(200, 200, 200))) self.update()
def __setup(self): # Setup the subwidgets/groups/layout smax = max((np.nanmax(g.scores) for g in self.__groups if g.scores.size), default=1) smax = 1 if np.isnan(smax) else smax smin = min((np.nanmin(g.scores) for g in self.__groups if g.scores.size), default=-1) smin = -1 if np.isnan(smin) else smin smin = min(smin, 0) font = self.font() font.setPixelSize(self.__barHeight) axispen = QPen(Qt.black) ax = pg.AxisItem(parent=self, orientation="top", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.__topScale = ax layout = self.__layout assert layout is self.layout() layout.addItem(ax, 0, 2) for i, group in enumerate(self.__groups): silhouettegroup = BarPlotItem(parent=self) silhouettegroup.setBrush(QBrush(QColor(*group.color))) silhouettegroup.setPen(self.__pen) silhouettegroup.setDataRange(smin, smax) silhouettegroup.setPlotData(group.scores) silhouettegroup.setPreferredBarSize(self.__barHeight) silhouettegroup.setData(0, group.indices) layout.addItem(silhouettegroup, i + 1, 2) if group.label: layout.addItem(Line(orientation=Qt.Vertical), i + 1, 1) label = QGraphicsSimpleTextItem( "{} ({})".format(group.label, len(group.scores)), self ) label.setRotation(-90) item = SimpleLayoutItem( label, anchor=(0., 1.0), anchorItem=(0., 0.), ) item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) layout.addItem(item, i + 1, 0, Qt.AlignCenter) textlist = _SilhouettePlotTextListWidget( self, font=font, elideMode=Qt.ElideRight, alignment=Qt.AlignLeft | Qt.AlignVCenter ) textlist.setMaximumWidth(750) textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False) sp = textlist.sizePolicy() sp.setVerticalPolicy(QSizePolicy.Ignored) textlist.setSizePolicy(sp) if group.rownames is not None: textlist.setItems(group.items) textlist.setVisible(self.__rowNamesVisible) else: textlist.setVisible(False) layout.addItem(textlist, i + 1, 3) ax = pg.AxisItem(parent=self, orientation="bottom", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.__bottomScale = ax layout.addItem(ax, len(self.__groups) + 1, 2)
def backgroundBrush(self): brush = getattr(self, "_background_brush") if brush is None: brush = getattr(self.scene(), "defaultItemBrush", Qt.NoBrush) return QBrush(brush)
def brush(self): """Return the items brush. """ return QBrush(self.__brush)
class OWDataSets(OWWidget): name = "Datasets" description = "Load a dataset from an online repository" icon = "icons/DataSets.svg" priority = 20 replaces = ["orangecontrib.prototypes.widgets.owdatasets.OWDataSets"] keywords = ["online", "data sets"] want_control_area = False # The following constants can be overridden in a subclass # to reuse this widget for a different repository # Take care when refactoring! (used in e.g. single-cell) INDEX_URL = "https://datasets.biolab.si/" DATASET_DIR = "datasets" # override HEADER_SCHEMA to define new columns # if schema is changed override methods: self.assign_delegates and # self.create_model HEADER_SCHEMA = [['islocal', { 'label': '' }], ['title', { 'label': 'Title' }], ['size', { 'label': 'Size' }], ['instances', { 'label': 'Instances' }], ['variables', { 'label': 'Variables' }], ['target', { 'label': 'Target' }], ['tags', { 'label': 'Tags' }]] # type: List[str, dict] IndicatorBrushes = (QBrush(Qt.darkGray), QBrush(QColor(0, 192, 0))) class Error(OWWidget.Error): no_remote_datasets = Msg("Could not fetch dataset list") class Warning(OWWidget.Warning): only_local_datasets = Msg("Could not fetch datasets list, only local " "cached datasets are shown") class Outputs: data = Output("Data", Orange.data.Table) #: Selected dataset id selected_id = settings.Setting(None) # type: Optional[str] #: main area splitter state splitter_state = settings.Setting(b'') # type: bytes header_state = settings.Setting(b'') # type: bytes def __init__(self): super().__init__() self.allinfo_local = {} self.allinfo_remote = {} self.local_cache_path = os.path.join(data_dir(), self.DATASET_DIR) # current_output does not equal selected_id when, for instance, the # data is still downloading self.current_output = None self._header_labels = [ header['label'] for _, header in self.HEADER_SCHEMA ] self._header_index = namedtuple( '_header_index', [info_tag for info_tag, _ in self.HEADER_SCHEMA]) self.Header = self._header_index( *[index for index, _ in enumerate(self._header_labels)]) self.__awaiting_state = None # type: Optional[_FetchState] self.filterLineEdit = QLineEdit( textChanged=self.filter, placeholderText="Search for data set ...") self.mainArea.layout().addWidget(self.filterLineEdit) self.splitter = QSplitter(orientation=Qt.Vertical) self.view = TreeViewWithReturn( sortingEnabled=True, selectionMode=QTreeView.SingleSelection, alternatingRowColors=True, rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, uniformRowHeights=True, toolTip="Press Return or double-click to send") # the method doesn't exists yet, pylint: disable=unnecessary-lambda self.view.doubleClicked.connect(self.commit) self.view.returnPressed.connect(self.commit) box = gui.widgetBox(self.splitter, "Description", addToLayout=False) self.descriptionlabel = QLabel( wordWrap=True, textFormat=Qt.RichText, ) self.descriptionlabel = QTextBrowser( openExternalLinks=True, textInteractionFlags=(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)) self.descriptionlabel.setFrameStyle(QTextBrowser.NoFrame) # no (white) text background self.descriptionlabel.viewport().setAutoFillBackground(False) box.layout().addWidget(self.descriptionlabel) self.splitter.addWidget(self.view) self.splitter.addWidget(box) self.splitter.setSizes([300, 200]) self.splitter.splitterMoved.connect(lambda: setattr( self, "splitter_state", bytes(self.splitter.saveState()))) self.mainArea.layout().addWidget(self.splitter) proxy = QSortFilterProxyModel() proxy.setFilterKeyColumn(-1) proxy.setFilterCaseSensitivity(False) self.view.setModel(proxy) if self.splitter_state: self.splitter.restoreState(self.splitter_state) self.assign_delegates() self.setBlocking(True) self.setStatusMessage("Initializing") self._executor = ThreadPoolExecutor(max_workers=1) f = self._executor.submit(list_remote, self.INDEX_URL) w = FutureWatcher(f, parent=self) w.done.connect(self.__set_index) def assign_delegates(self): # NOTE: All columns must have size hinting delegates. # QTreeView queries only the columns displayed in the viewport so # the layout would be different depending in the horizontal scroll # position self.view.setItemDelegate(UniformHeightDelegate(self)) self.view.setItemDelegateForColumn( self.Header.islocal, UniformHeightIndicatorDelegate(self, role=Qt.DisplayRole, indicatorSize=4)) self.view.setItemDelegateForColumn(self.Header.size, SizeDelegate(self)) self.view.setItemDelegateForColumn(self.Header.instances, NumericalDelegate(self)) self.view.setItemDelegateForColumn(self.Header.variables, NumericalDelegate(self)) self.view.resizeColumnToContents(self.Header.islocal) def _parse_info(self, file_path): if file_path in self.allinfo_remote: info = self.allinfo_remote[file_path] else: info = self.allinfo_local[file_path] islocal = file_path in self.allinfo_local isremote = file_path in self.allinfo_remote outdated = islocal and isremote and ( self.allinfo_remote[file_path].get('version', '') != self.allinfo_local[file_path].get('version', '')) islocal &= not outdated prefix = os.path.join('', *file_path[:-1]) filename = file_path[-1] return Namespace(file_path=file_path, prefix=prefix, filename=filename, islocal=islocal, outdated=outdated, **info) def create_model(self): allkeys = set(self.allinfo_local) | set(self.allinfo_remote) allkeys = sorted(allkeys) model = QStandardItemModel(self) model.setHorizontalHeaderLabels(self._header_labels) current_index = -1 for i, file_path in enumerate(allkeys): datainfo = self._parse_info(file_path) item1 = QStandardItem() item1.setData(" " if datainfo.islocal else "", Qt.DisplayRole) item1.setData(self.IndicatorBrushes[0], Qt.ForegroundRole) item1.setData(datainfo, Qt.UserRole) item2 = QStandardItem(datainfo.title) item3 = QStandardItem() item3.setData(datainfo.size, Qt.DisplayRole) item4 = QStandardItem() item4.setData(datainfo.instances, Qt.DisplayRole) item5 = QStandardItem() item5.setData(datainfo.variables, Qt.DisplayRole) item6 = QStandardItem() item6.setData(datainfo.target, Qt.DisplayRole) if datainfo.target: item6.setIcon(variable_icon(datainfo.target)) item7 = QStandardItem() item7.setData(", ".join(datainfo.tags) if datainfo.tags else "", Qt.DisplayRole) row = [item1, item2, item3, item4, item5, item6, item7] model.appendRow(row) if os.path.join(*file_path) == self.selected_id: current_index = i return model, current_index @Slot(object) def __set_index(self, f): # type: (Future) -> None # set results from `list_remote` query. assert QThread.currentThread() is self.thread() assert f.done() self.setBlocking(False) self.setStatusMessage("") self.allinfo_local = list_local(self.local_cache_path) try: self.allinfo_remote = f.result() except Exception: # anytying can happen, pylint: disable=broad-except log.exception("Error while fetching updated index") if not self.allinfo_local: self.Error.no_remote_datasets() else: self.Warning.only_local_datasets() self.allinfo_remote = {} model, current_index = self.create_model() self.view.model().setSourceModel(model) self.view.selectionModel().selectionChanged.connect( self.__on_selection) scw = self.view.setColumnWidth width = self.view.fontMetrics().width self.view.resizeColumnToContents(0) scw(self.Header.title, width("X" * 37)) scw(self.Header.size, 20 + max(width("888 bytes "), width("9999.9 MB "))) scw(self.Header.instances, 20 + width("100000000")) scw(self.Header.variables, 20 + width("1000000")) header = self.view.header() header.restoreState(self.header_state) if current_index != -1: selmodel = self.view.selectionModel() selmodel.select( self.view.model().mapFromSource(model.index(current_index, 0)), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) self.commit() def __update_cached_state(self): model = self.view.model().sourceModel() localinfo = list_local(self.local_cache_path) assert isinstance(model, QStandardItemModel) allinfo = [] for i in range(model.rowCount()): item = model.item(i, 0) info = item.data(Qt.UserRole) is_local = info.file_path in localinfo is_current = (is_local and os.path.join( self.local_cache_path, *info.file_path) == self.current_output) item.setData(" " * (is_local + is_current), Qt.DisplayRole) item.setData(self.IndicatorBrushes[is_current], Qt.ForegroundRole) allinfo.append(info) def selected_dataset(self): """ Return the current selected dataset info or None if not selected Returns ------- info : Optional[Namespace] """ rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: info = current.data(Qt.UserRole) assert isinstance(info, Namespace) else: info = None return info def filter(self): filter_string = self.filterLineEdit.text().strip() proxyModel = self.view.model() if proxyModel: proxyModel.setFilterFixedString(filter_string) def __on_selection(self): # Main datasets view selection has changed rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: current = self.view.model().mapToSource(current) di = current.data(Qt.UserRole) text = description_html(di) self.descriptionlabel.setText(text) self.selected_id = os.path.join(di.prefix, di.filename) else: self.descriptionlabel.setText("") self.selected_id = None def commit(self): """ Commit a dataset to the output immediately (if available locally) or schedule download background and an eventual send. During the download the widget is in blocking state (OWWidget.isBlocking) """ di = self.selected_dataset() if di is not None: self.Error.clear() if self.__awaiting_state is not None: # disconnect from the __commit_complete self.__awaiting_state.watcher.done.disconnect( self.__commit_complete) # .. and connect to update_cached_state # self.__awaiting_state.watcher.done.connect( # self.__update_cached_state) # TODO: There are possible pending __progress_advance queued self.__awaiting_state.pb.advance.disconnect( self.__progress_advance) self.progressBarFinished() self.__awaiting_state = None if not di.islocal: pr = progress() callback = lambda pr=pr: pr.advance.emit() pr.advance.connect(self.__progress_advance, Qt.QueuedConnection) self.progressBarInit() self.setStatusMessage("Fetching...") self.setBlocking(True) f = self._executor.submit(ensure_local, self.INDEX_URL, di.file_path, self.local_cache_path, force=di.outdated, progress_advance=callback) w = FutureWatcher(f, parent=self) w.done.connect(self.__commit_complete) self.__awaiting_state = _FetchState(f, w, pr) else: self.setStatusMessage("") self.setBlocking(False) self.commit_cached(di.file_path) else: self.load_and_output(None) @Slot(object) def __commit_complete(self, f): # complete the commit operation after the required file has been # downloaded assert QThread.currentThread() is self.thread() assert self.__awaiting_state is not None assert self.__awaiting_state.future is f if self.isBlocking(): self.progressBarFinished() self.setBlocking(False) self.setStatusMessage("") self.__awaiting_state = None try: path = f.result() # anything can happen here, pylint: disable=broad-except except Exception as ex: log.exception("Error:") self.error(format_exception(ex)) path = None self.load_and_output(path) def commit_cached(self, file_path): path = LocalFiles(self.local_cache_path).localpath(*file_path) self.load_and_output(path) @Slot() def __progress_advance(self): assert QThread.currentThread() is self.thread() self.progressBarAdvance(1) def onDeleteWidget(self): super().onDeleteWidget() if self.__awaiting_state is not None: self.__awaiting_state.watcher.done.disconnect( self.__commit_complete) self.__awaiting_state.pb.advance.disconnect( self.__progress_advance) self.__awaiting_state = None @staticmethod def sizeHint(): return QSize(1100, 500) def closeEvent(self, event): self.splitter_state = bytes(self.splitter.saveState()) self.header_state = bytes(self.view.header().saveState()) super().closeEvent(event) def load_and_output(self, path): if path is None: self.Outputs.data.send(None) else: data = self.load_data(path) self.Outputs.data.send(data) self.current_output = path self.__update_cached_state() @staticmethod def load_data(path): return Orange.data.Table(path)
def brush(self): return QBrush(self.__brush)
def add_points(): nonlocal cur, image_token if image_token != self._image_token: return batch = visible[cur:cur + self.N_POINTS_PER_ITER] batch_lat = lat[batch] batch_lon = lon[batch] x, y = self.Projection.latlon_to_easting_northing( batch_lat, batch_lon) x, y = self.Projection.easting_northing_to_pixel( x, y, zoom, origin, map_pane_pos) if self._jittering: dx, dy = self._jittering_offsets[batch].T x, y = x + dx, y + dy colors = (self._colorgen.getRGB( self._scaled_color_values[batch]).tolist() if self._color_attr else repeat((0xff, 0, 0))) sizes = self._size_coef * \ (self._sizes[batch] if self._size_attr else np.tile(10, len(batch))) opacity_subset, opacity_rest = self._opacity, int(.8 * self._opacity) for x, y, is_selected, size, color, _in_subset in \ zip(x, y, selected[batch], sizes, colors, in_subset[batch]): pensize2, selpensize2 = (.35, 1.5) if size >= 5 else (.15, .7) pensize2 *= self._size_coef selpensize2 *= self._size_coef size2 = size / 2 if is_selected: painter.setPen(QPen(QBrush(Qt.green), 2 * selpensize2)) painter.drawEllipse(x - size2 - selpensize2, y - size2 - selpensize2, size + selpensize2, size + selpensize2) color = QColor(*color) if _in_subset: color.setAlpha(opacity_subset) painter.setBrush(QBrush(color)) painter.setPen( QPen(QBrush(color.darker(180)), 2 * pensize2)) else: color.setAlpha(opacity_rest) painter.setBrush(Qt.NoBrush) painter.setPen( QPen(QBrush(color.lighter(120)), 2 * pensize2)) painter.drawEllipse(x - size2 - pensize2, y - size2 - pensize2, size + pensize2, size + pensize2) im.save(self._overlay_image_path, 'PNG') self.evalJS('markersImageLayer.setUrl("{}#{}"); 0;'.format( self.toFileURL(self._overlay_image_path), np.random.random())) cur += self.N_POINTS_PER_ITER if cur < len(visible): QTimer.singleShot(10, add_points) self._owwidget.progressBarAdvance(100 / n_iters, None) else: self._owwidget.progressBarFinished(None) self._image_token = None
def color_for_cell(self, row, col): return QBrush(QColor.fromHsv(120, self.colors[row, col], 255))
def __setup(self): # Setup the subwidgets/groups/layout smax = max( (np.nanmax(g.scores) for g in self.__groups if g.scores.size), default=1) smax = 1 if np.isnan(smax) else smax smin = min( (np.nanmin(g.scores) for g in self.__groups if g.scores.size), default=-1) smin = -1 if np.isnan(smin) else smin smin = min(smin, 0) font = self.font() font.setPixelSize(self.__barHeight) axispen = QPen(Qt.black) ax = pg.AxisItem(parent=self, orientation="top", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.layout().addItem(ax, 0, 2) for i, group in enumerate(self.__groups): silhouettegroup = BarPlotItem(parent=self) silhouettegroup.setBrush(QBrush(QColor(*group.color))) silhouettegroup.setPen(self.__pen) silhouettegroup.setDataRange(smin, smax) silhouettegroup.setPlotData(group.scores) silhouettegroup.setPreferredBarSize(self.__barHeight) silhouettegroup.setData(0, group.indices) self.layout().addItem(silhouettegroup, i + 1, 2) if group.label: self.layout().addItem(Line(orientation=Qt.Vertical), i + 1, 1) label = QGraphicsSimpleTextItem(self) label.setText("{} ({})".format(escape(group.label), len(group.scores))) item = WrapperLayoutItem(label, Qt.Vertical, parent=self) self.layout().addItem(item, i + 1, 0, Qt.AlignCenter) textlist = TextListWidget(self, font=font) sp = textlist.sizePolicy() sp.setVerticalPolicy(QSizePolicy.Ignored) textlist.setSizePolicy(sp) textlist.setParent(self) if group.rownames is not None: textlist.setItems(group.items) textlist.setVisible(self.__rowNamesVisible) else: textlist.setVisible(False) self.layout().addItem(textlist, i + 1, 3) ax = pg.AxisItem(parent=self, orientation="bottom", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.layout().addItem(ax, len(self.__groups) + 1, 2)
def display_contingency(self): """ Set the contingency to display. """ cont = self.contingencies var, cvar = self.var, self.cvar if cont is None or not len(cont): return self.plot.clear() self.plot_prob.clear() self._legend.clear() self.tooltip_items = [] if self.show_prob: self.ploti.showAxis('right') else: self.ploti.hideAxis('right') bottomaxis = self.ploti.getAxis("bottom") bottomaxis.setLabel(var.name) bottomaxis.resizeEvent() cvar_values = cvar.values colors = [QColor(*col) for col in cvar.colors] if var and var.is_continuous: bottomaxis.setTicks(None) weights, cols, cvar_values, curves = [], [], [], [] for i, dist in enumerate(cont): v, W = dist if len(v): weights.append(numpy.sum(W)) cols.append(colors[i]) cvar_values.append(cvar.values[i]) curves.append( ash_curve(dist, cont, m=OWDistributions.ASH_HIST, smoothing_factor=self.smoothing_factor)) weights = numpy.array(weights) sumw = numpy.sum(weights) weights /= sumw colors = cols curves = [(X, Y * w) for (X, Y), w in zip(curves, weights)] curvesline = [] #from histograms to lines for X, Y in curves: X = X + (X[1] - X[0]) / 2 X = X[:-1] X = numpy.array(X) Y = numpy.array(Y) curvesline.append((X, Y)) for t in ["fill", "line"]: curve_data = list(zip(curvesline, colors, weights, cvar_values)) for (X, Y), color, w, cval in reversed(curve_data): item = pg.PlotCurveItem() pen = QPen(QBrush(color), 3) pen.setCosmetic(True) color = QColor(color) color.setAlphaF(0.2) item.setData(X, Y / (w if self.relative_freq else 1), antialias=True, stepMode=False, fillLevel=0 if t == "fill" else None, brush=QBrush(color), pen=pen) self.plot.addItem(item) if t == "line": item.tooltip = "{}\n{}={}".format( "Normalized density " if self.relative_freq else "Density ", cvar.name, cval) self.tooltip_items.append((self.plot, item)) if self.show_prob: all_X = numpy.array( numpy.unique(numpy.hstack([X for X, _ in curvesline]))) inter_X = numpy.array( numpy.linspace(all_X[0], all_X[-1], len(all_X) * 2)) curvesinterp = [ numpy.interp(inter_X, X, Y) for (X, Y) in curvesline ] sumprob = numpy.sum(curvesinterp, axis=0) legal = sumprob > 0.05 * numpy.max(sumprob) i = len(curvesinterp) + 1 show_all = self.show_prob == i for Y, color, cval in reversed( list(zip(curvesinterp, colors, cvar_values))): i -= 1 if show_all or self.show_prob == i: item = pg.PlotCurveItem() pen = QPen(QBrush(color), 3, style=Qt.DotLine) pen.setCosmetic(True) prob = Y[legal] / sumprob[legal] item.setData(inter_X[legal], prob, antialias=True, stepMode=False, fillLevel=None, brush=None, pen=pen) self.plot_prob.addItem(item) item.tooltip = "Probability that \n" + cvar.name + "=" + cval self.tooltip_items.append((self.plot_prob, item)) elif var and var.is_discrete: bottomaxis.setTicks([list(enumerate(var.values))]) cont = numpy.array(cont) maxh = 0 #maximal column height maxrh = 0 #maximal relative column height scvar = cont.sum(axis=1) #a cvar with sum=0 with allways have distribution counts 0, #therefore we can divide it by anything scvar[scvar == 0] = 1 for i, (value, dist) in enumerate(zip(var.values, cont.T)): maxh = max(maxh, max(dist)) maxrh = max(maxrh, max(dist / scvar)) for i, (value, dist) in enumerate(zip(var.values, cont.T)): dsum = sum(dist) geom = QRectF(i - 0.333, 0, 0.666, maxrh if self.relative_freq else maxh) if self.show_prob: prob = dist / dsum ci = 1.96 * numpy.sqrt(prob * (1 - prob) / dsum) else: ci = None item = DistributionBarItem( geom, dist / scvar / maxrh if self.relative_freq else dist / maxh, colors) self.plot.addItem(item) tooltip = "\n".join("%s: %.*f" % (n, 3 if self.relative_freq else 1, v) for n, v in zip( cvar_values, dist / scvar if self.relative_freq else dist)) item.tooltip = "{} ({}={}):\n{}".format( "Normalized frequency " if self.relative_freq else "Frequency ", cvar.name, value, tooltip) self.tooltip_items.append((self.plot, item)) if self.show_prob: item.tooltip += "\n\nProbabilities:" for ic, a in enumerate(dist): if self.show_prob - 1 != ic and \ self.show_prob - 1 != len(dist): continue position = -0.333 + ((ic + 0.5) * 0.666 / len(dist)) if dsum < 1e-6: continue prob = a / dsum if not 1e-6 < prob < 1 - 1e-6: continue ci = 1.96 * sqrt(prob * (1 - prob) / dsum) item.tooltip += "\n%s: %.3f ± %.3f" % (cvar_values[ic], prob, ci) mark = pg.ScatterPlotItem() errorbar = pg.ErrorBarItem() pen = QPen(QBrush(QColor(0)), 1) pen.setCosmetic(True) errorbar.setData(x=[i + position], y=[prob], bottom=min(numpy.array([ci]), prob), top=min(numpy.array([ci]), 1 - prob), beam=numpy.array([0.05]), brush=QColor(1), pen=pen) mark.setData([i + position], [prob], antialias=True, symbol="o", fillLevel=None, pxMode=True, size=10, brush=QColor(colors[ic]), pen=pen) self.plot_prob.addItem(errorbar) self.plot_prob.addItem(mark) for color, name in zip(colors, cvar_values): self._legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, shape="s"), escape(name)) self._legend.show()
from orangecontrib.imageanalytics.widgets.owimageviewer import (ImageLoader, Preview) _log = logging.getLogger(__name__) _ImageItem = namedtuple( "_ImageItem", [ "index", # Index in the input data table "widget", # GraphicsThumbnailWidget displaying the image. "url", # Composed final image url. "future" ] # Future instance yielding an QImage ) DEFAULT_SELECTION_BRUSH = QBrush(QColor(217, 232, 252, 192)) DEFAULT_SELECTION_PEN = QPen(QColor(125, 162, 206, 192)) class OWImageGrid(widget.OWWidget): name = "Image Grid" description = "Visualize images in a similarity grid" icon = "icons/ImageGrid.svg" priority = 160 keywords = ["image", "grid", "similarity"] graph_name = "scene" class Inputs: data = Input("Embeddings", Orange.data.Table, default=True) data_subset = Input("Data Subset", Orange.data.Table)