def __init__(self, i, mu1, mu2, sigma1, sigma2, phi, color): OWPlotItem.__init__(self) self.outer_box = QGraphicsPolygonItem(self) self.inner_box = QGraphicsPolygonItem(self) self.i = i self.mu1 = mu1 self.mu2 = mu2 self.sigma1 = sigma1 self.sigma2 = sigma2 self.phi = phi self.twosigmapolygon = QPolygonF([ QPointF(i, mu1 - sigma1), QPointF(i, mu1 + sigma1), QPointF(i + 1, mu2 + sigma2), QPointF(i + 1, mu2 - sigma2), QPointF(i, mu1 - sigma1) ]) self.sigmapolygon = QPolygonF([ QPointF(i, mu1 - .5 * sigma1), QPointF(i, mu1 + .5 * sigma1), QPointF(i + 1, mu2 + .5 * sigma2), QPointF(i + 1, mu2 - .5 * sigma2), QPointF(i, mu1 - .5 * sigma1) ]) if isinstance(color, tuple): color = QColor(*color) color.setAlphaF(.3) self.outer_box.setBrush(color) self.outer_box.setPen(QColor(0, 0, 0, 0)) self.inner_box.setBrush(color) self.inner_box.setPen(color)
def __updatePen(self): self.prepareGeometryChange() self.__boundingRect = None if self.__dynamic: if self.__dynamicEnabled: color = QColor(0, 150, 0, 150) else: color = QColor(150, 0, 0, 150) normal = QPen(QBrush(color), 2.0) hover = QPen(QBrush(color.darker(120)), 2.1) else: normal = QPen(QBrush(QColor("#9CACB4")), 2.0) hover = QPen(QBrush(QColor("#7D7D7D")), 2.1) if self.__state & LinkItem.Empty: pen_style = Qt.DashLine else: pen_style = Qt.SolidLine normal.setStyle(pen_style) hover.setStyle(pen_style) if self.hover: pen = hover else: pen = normal self.curveItem.setPen(pen)
def toggle_node_color_reg(self): """Update the node color for regression trees""" def_color = QColor(192, 192, 255) if self.regression_colors == self.COL_DEFAULT: brush = QBrush(def_color.lighter(100)) for node in self.scene.nodes(): node.backgroundBrush = brush elif self.regression_colors == self.COL_INSTANCE: max_insts = len(self.model.instances) for node in self.scene.nodes(): node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * len(node.node_inst.subset) / max_insts)) elif self.regression_colors == self.COL_MEAN: minv = np.nanmin(self.dataset.Y) maxv = np.nanmax(self.dataset.Y) fact = 1 / (maxv - minv) if minv != maxv else 1 colors = self.scene.colors for node in self.scene.nodes(): node.backgroundBrush = QBrush( colors[fact * (node.node_inst.value[0] - minv)]) else: nodes = list(self.scene.nodes()) variances = [node.node_inst.value[1] for node in nodes] max_var = max(variances) for node, var in zip(nodes, variances): node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * var / max_var)) self.scene.update()
def update_profiles_color(self, selection): color = QColor(self.color) alpha = LinePlotStyle.UNSELECTED_LINE_ALPHA if not selection \ else LinePlotStyle.UNSELECTED_LINE_ALPHA_SEL color.setAlpha(alpha) x, y = self.profiles.getData() self.profiles.setData(x=x, y=y, pen=self.make_pen(color))
def make_color_legend(self): color_index = self.get_color_index() if color_index == -1: return color_var = self.domain[color_index] use_shape = self.get_shape_index() == color_index if color_var.is_discrete: if not self.legend: self.create_legend() palette = self.discrete_palette for i, value in enumerate(color_var.values): color = QColor(*palette.getRGB(i)) brush = color.lighter(self.DarkerValue) self.legend.addItem( ScatterPlotItem( pen=color, brush=brush, size=10, symbol=self.CurveSymbols[i] if use_shape else "o"), escape(value)) else: legend = self.color_legend = LegendItem() legend.setParentItem(self.plot_widget.getViewBox()) legend.restoreAnchor(self.__color_legend_anchor) label = PaletteItemSample(self.continuous_palette, self.scale) legend.addItem(label, "") legend.setGeometry(label.boundingRect())
def make_color_legend(self): if self.attr_color is None: return use_shape = self.attr_shape == self.get_color() if self.attr_color.is_discrete: if not self.legend: self.create_legend() palette = self.discrete_palette for i, value in enumerate(self._get_values(self.attr_color)): color = QColor(*palette.getRGB(i)) pen = _make_pen(color.darker(self.DarkerValue), 1.5) color.setAlpha(self.alpha_value if self.subset_indices is None else 255) brush = QBrush(color) self.legend.addItem( ScatterPlotItem( pen=pen, brush=brush, size=10, symbol=self.CurveSymbols[i] if use_shape else "o"), escape(value)) else: legend = self.color_legend = LegendItem() legend.setParentItem(self.plot_widget.getViewBox()) legend.restoreAnchor(self.__color_legend_anchor) label = PaletteItemSample(self.continuous_palette, self.scale) legend.addItem(label, "") legend.setGeometry(label.boundingRect())
def toggle_node_color_reg(self): """Update the node color for regression trees""" def_color = QColor(192, 192, 255) if self.regression_colors == self.COL_DEFAULT: brush = QBrush(def_color.lighter(100)) for node in self.scene.nodes(): node.backgroundBrush = brush elif self.regression_colors == self.COL_INSTANCE: max_insts = len(self.tree_adapter.get_instances_in_nodes( [self.tree_adapter.root])) for node in self.scene.nodes(): node_insts = len(self.tree_adapter.get_instances_in_nodes( [node.node_inst])) node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * node_insts / max_insts)) elif self.regression_colors == self.COL_MEAN: minv = np.nanmin(self.dataset.Y) maxv = np.nanmax(self.dataset.Y) fact = 1 / (maxv - minv) if minv != maxv else 1 colors = self.scene.colors for node in self.scene.nodes(): node_mean = self.tree_adapter.get_distribution(node.node_inst)[0][0] node.backgroundBrush = QBrush(colors[fact * (node_mean - minv)]) else: nodes = list(self.scene.nodes()) variances = [self.tree_adapter.get_distribution(node.node_inst)[0][1] for node in nodes] max_var = max(variances) for node, var in zip(nodes, variances): node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * var / max_var)) self.scene.update()
def _get_same_colors(self, subset): """ Return the same pen for all points while the brush color depends upon whether the point is in the subset or not Args: subset (np.ndarray): a bool array indicating whether a data point is in the subset or not (e.g. in the 'Data Subset' signal in the Scatter plot and similar widgets); Returns: (tuple): a list of pens and list of brushes """ color = self.plot_widget.palette().color(OWPalette.Data) pen = [_make_pen(color, 1.5) for _ in range(self.n_shown)] if subset is not None: brush = np.where( subset, *(QBrush(QColor(*col)) for col in (self.COLOR_SUBSET, self.COLOR_NOT_SUBSET))) else: color = QColor(*self.COLOR_DEFAULT) color.setAlpha(self.alpha_value) brush = [QBrush(color) for _ in range(self.n_shown)] return pen, brush
def _get_range_curve(self): color = QColor(self.color) color.setAlpha(LinePlotStyle.RANGE_ALPHA) bottom, top = nanmin(self.y_data, axis=0), nanmax(self.y_data, axis=0) return pg.FillBetweenItem( pg.PlotDataItem(x=self.x_data, y=bottom), pg.PlotDataItem(x=self.x_data, y=top), brush=color )
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))) 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) color.setAlpha(self._opacity) painter.setBrush(QBrush(color) if _in_subset else Qt.NoBrush) painter.setPen(QPen(QBrush(color.darker(180)), 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)
def _update_shape_legend(self, labels): self.shape_legend.clear() if labels is None or self.scatterplot_item is None: return color = QColor(0, 0, 0) color.setAlpha(self.alpha_value) for label, symbol in zip(labels, self.CurveSymbols): self.shape_legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, symbol=symbol), escape(label))
def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color)
def make_shape_legend(self): if self.attr_shape is None or self.attr_shape == self.get_color(): return if not self.legend: self.create_legend() color = QColor(0, 0, 0) color.setAlpha(self.alpha_value) for i, value in enumerate(self._get_values(self.attr_shape)): self.legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, symbol=self.CurveSymbols[i]), escape(value))
def refresh_integral_markings(dis, markings_list, curveplot): for m in markings_list: if m in curveplot.markings: curveplot.remove_marking(m) markings_list.clear() def add_marking(a): markings_list.append(a) curveplot.add_marking(a) for di in dis: if di is None: continue # nothing to draw color = QColor(di.get("color", "red")) for el in di["draw"]: if el[0] == "curve": bs_x, bs_ys, penargs = el[1] curve = pg.PlotCurveItem() curve.setPen(pg.mkPen(color=QColor(color), **penargs)) curve.setZValue(10) curve.setData(x=bs_x, y=bs_ys[0]) add_marking(curve) elif el[0] == "fill": (x1, ys1), (x2, ys2) = el[1] phigh = pg.PlotCurveItem(x1, ys1[0], pen=None) plow = pg.PlotCurveItem(x2, ys2[0], pen=None) color = QColor(color) color.setAlphaF(0.5) cc = pg.mkBrush(color) pfill = pg.FillBetweenItem(plow, phigh, brush=cc) pfill.setZValue(9) add_marking(pfill) elif el[0] == "line": (x1, y1), (x2, y2) = el[1] line = pg.PlotCurveItem() line.setPen(pg.mkPen(color=QColor(color), width=4)) line.setZValue(10) line.setData(x=[x1[0], x2[0]], y=[y1[0], y2[0]]) add_marking(line) elif el[0] == "dot": (x, ys) = el[1] dot = pg.ScatterPlotItem(x=x, y=ys[0]) dot.setPen(pg.mkPen(color=QColor(color), width=5)) dot.setZValue(10) add_marking(dot)
def draw_distributions(self): """Draw distributions with discrete attributes""" if not (self.show_distributions and self.data is not None and self.domain.has_discrete_class): return class_count = len(self.domain.class_var.values) class_ = self.domain.class_var # we create a hash table of possible class values (happens only if we have a discrete class) if self.domain_contingencies is None: self.domain_contingencies = dict( zip([attr for attr in self.domain if attr.is_discrete], get_contingencies(self.data, skipContinuous=True))) self.domain_contingencies[class_] = get_contingency(self.data, class_, class_) max_count = max([contingency.max() for contingency in self.domain_contingencies.values()] or [1]) sorted_class_values = get_variable_values_sorted(self.domain.class_var) for axis_idx, attr_idx in enumerate(self.attribute_indices): attr = self.domain[attr_idx] if attr.is_discrete: continue contingency = self.domain_contingencies[attr] attr_len = len(attr.values) # we create a hash table of variable values and their indices sorted_variable_values = get_variable_values_sorted(attr) # create bar curve for j in range(attr_len): attribute_value = sorted_variable_values[j] value_count = contingency[:, attribute_value] for i in range(class_count): class_value = sorted_class_values[i] color = QColor(*self.colors[i]) color.setAlpha(self.alpha_value) width = float(value_count[class_value] * 0.5) / float(max_count) y_off = float(1.0 + 2.0 * j) / float(2 * attr_len) height = 0.7 / float(class_count * attr_len) y_low_bottom = y_off + float(class_count * height) / 2.0 - i * height curve = PolygonCurve(QPen(color), QBrush(color), xData=[axis_idx, axis_idx + width, axis_idx + width, axis_idx], yData=[y_low_bottom, y_low_bottom, y_low_bottom - height, y_low_bottom - height], tooltip=attr.name) curve.attach(self)
def test_colors_continuous_nan(self): self.master.is_continuous_color = lambda: True graph = self.graph d = np.arange(10, dtype=float) % 2 d[4] = np.nan self.master.get_color_data = lambda: d graph.reset_graph() pens = graph.scatterplot_item.data["pen"] brushes = graph.scatterplot_item.data["brush"] nan_color = QColor(*NAN_GREY) self.assertEqual(pens[4].color().hue(), nan_color.hue()) self.assertEqual(brushes[4].color().hue(), nan_color.hue())
def _update_colored_legend(self, legend, labels, symbols): if self.scatterplot_item is None or not self.palette: return if isinstance(symbols, str): symbols = itertools.repeat(symbols, times=len(labels)) for i, (label, symbol) in enumerate(zip(labels, symbols)): color = QColor(*self.palette.getRGB(i)) pen = _make_pen(color.darker(self.DarkerValue), 1.5) color.setAlpha(255 if self.subset_is_shown else self.alpha_value) brush = QBrush(color) legend.addItem( ScatterPlotItem(pen=pen, brush=brush, size=10, symbol=symbol), escape(label))
def __init__(self, parent=None, line=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) if line is None: line = QLineF(0, 0, 20, 0) self.__line = line self.__color = QColor(Qt.red) self.__arrowItem = ArrowItem(self) self.__arrowItem.setLine(line) self.__arrowItem.setBrush(self.__color) self.__arrowItem.setPen(QPen(Qt.NoPen)) self.__arrowItem.setArrowStyle(ArrowItem.Concave) self.__arrowItem.setLineWidth(5) self.__shadow = QGraphicsDropShadowEffect(blurRadius=5, offset=QPointF(1.0, 2.0)) self.__arrowItem.setGraphicsEffect(self.__shadow) self.__shadow.setEnabled(True) self.__autoAdjustGeometry = True
def setColor(self, color): """ Set arrow brush color. """ if self.__color != color: self.__color = QColor(color) self.__updateStyleState()
def _setup_plot(self): self.plot.clear() points = self.ca variables = self.selected_vars() colors = colorpalette.ColorPaletteGenerator(len(variables)) p_axes = self._p_axes() 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 = numpy.cumsum([0] + counts) ranges = zip(range_indices, range_indices[1:]) points = [points[s:e] for s, e in ranges] 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=numpy.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() inertia = 100 * inertia / numpy.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 saturated(color, factor=150): """Return a saturated color. """ h = color.hsvHueF() s = color.hsvSaturationF() v = color.valueF() a = color.alphaF() s = factor * s / 100.0 s = max(min(1.0, s), 0.0) return QColor.fromHsvF(h, s, v, a).convertTo(color.spec())
def setColor(self, color): """ Set the color of the shadow. """ if not isinstance(color, QColor): color = QColor(color) if self.__color != color: self.__color = QColor(color) self.__updatePixmap()
def __init__(self, parent=None, color=QColor(), radius=5, **kwargs): QWidget.__init__(self, parent, **kwargs) self.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.setAttribute(Qt.WA_NoChildEventsForParent, True) self.setFocusPolicy(Qt.NoFocus) self.__color = QColor(color) self.__radius = radius self.__widget = None self.__widgetParent = None self.__updatePixmap()
def set_number_of_colors(self, number_of_colors=0): """Change the palette if there are palettes for different number of colors. Else, just copy colors as numpy array""" self.number_of_colors = number_of_colors if isinstance(self.rgb_colors, dict): number_of_colors = max(3, number_of_colors) if number_of_colors not in self.rgb_colors: try: number_of_colors = min([n for n in self.rgb_colors if n >= number_of_colors]) except ValueError: raise ValueError("Not enough colors") rgb_colors = self.rgb_colors[number_of_colors] elif number_of_colors <= len(self.rgb_colors): rgb_colors = self.rgb_colors else: rgb_colors = [] for i in range(self.number_of_colors): col = QColor() col.setHsv(360 / number_of_colors * i, 255, 255) rgb_colors.append(col.getRgb()[:3]) self.rgb_array = np.vstack((rgb_colors, [NAN_GREY]))
def __init__(self, parent=None, line=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) if line is None: line = QLineF(0, 0, 20, 0) self.__line = QLineF(line) self.__color = QColor(Qt.red) # An item with the same shape as this arrow, stacked behind this # item as a source for QGraphicsDropShadowEffect. Cannot attach # the effect to this item directly as QGraphicsEffect makes the item # non devicePixelRatio aware. self.__arrowShadowBase = ArrowItem(self, line=line) self.__arrowShadowBase.setPen(Qt.NoPen) # no pen -> slightly thinner self.__arrowShadowBase.setBrush(QBrush(self.__color)) self.__arrowShadowBase.setArrowStyle(ArrowItem.Concave) self.__arrowShadowBase.setLineWidth(5) self.__shadow = QGraphicsDropShadowEffect( blurRadius=5, offset=QPointF(1.0, 2.0), ) self.__arrowShadowBase.setGraphicsEffect(self.__shadow) self.__shadow.setEnabled(True) # The 'real' shape item self.__arrowItem = ArrowItem(self, line=line) self.__arrowItem.setBrush(self.__color) self.__arrowItem.setPen(QPen(self.__color)) self.__arrowItem.setArrowStyle(ArrowItem.Concave) self.__arrowItem.setLineWidth(5) self.__autoAdjustGeometry = True
class TableModel(AbstractSortTableModel): """ An adapter for using Orange.data.Table within Qt's Item View Framework. :param Orange.data.Table sourcedata: Source data table. :param QObject parent: """ #: Orange.data.Value for the index. ValueRole = gui.TableValueRole # next(gui.OrangeUserRole) #: Orange.data.Value of the row's class. ClassValueRole = gui.TableClassValueRole # next(gui.OrangeUserRole) #: Orange.data.Variable of the column. VariableRole = gui.TableVariable # next(gui.OrangeUserRole) #: Basic statistics of the column VariableStatsRole = next(gui.OrangeUserRole) #: The column's role (position) in the domain. #: One of Attribute, ClassVar or Meta DomainRole = next(gui.OrangeUserRole) #: Column domain roles ClassVar, Meta, Attribute = range(3) #: Default background color for domain roles ColorForRole = { ClassVar: QColor(160, 160, 160), Meta: QColor(220, 220, 200), Attribute: None, } #: Standard column descriptor Column = namedtuple( "Column", ["var", "role", "background", "format"]) #: Basket column descriptor (i.e. sparse X/Y/metas/ compressed into #: a single column). Basket = namedtuple( "Basket", ["vars", "role", "background", "density", "format"]) # The class uses the same names (X_density etc) as Table # pylint: disable=invalid-name def __init__(self, sourcedata, parent=None): super().__init__(parent) self.source = sourcedata self.domain = domain = sourcedata.domain self.X_density = sourcedata.X_density() self.Y_density = sourcedata.Y_density() self.M_density = sourcedata.metas_density() def format_sparse(vars, datagetter, instance): data = datagetter(instance) return ", ".join("{}={}".format(vars[i].name, vars[i].repr_val(v)) for i, v in zip(data.indices, data.data)) def format_sparse_bool(vars, datagetter, instance): data = datagetter(instance) return ", ".join(vars[i].name for i in data.indices) def format_dense(var, instance): return str(instance[var]) def make_basket_formater(vars, density, role): formater = (format_sparse if density == Storage.SPARSE else format_sparse_bool) if role == TableModel.Attribute: getter = operator.attrgetter("sparse_x") elif role == TableModel.ClassVar: getter = operator.attrgetter("sparse_y") elif role == TableModel.Meta: getter = operator.attrgetter("sparse_metas") return partial(formater, vars, getter) def make_basket(vars, density, role): return TableModel.Basket( vars, TableModel.Attribute, self.ColorForRole[role], density, make_basket_formater(vars, density, role) ) def make_column(var, role): return TableModel.Column( var, role, self.ColorForRole[role], partial(format_dense, var) ) columns = [] if self.Y_density != Storage.DENSE and domain.class_vars: coldesc = make_basket(domain.class_vars, self.Y_density, TableModel.ClassVar) columns.append(coldesc) else: columns += [make_column(var, TableModel.ClassVar) for var in domain.class_vars] if self.M_density != Storage.DENSE and domain.metas: coldesc = make_basket(domain.metas, self.M_density, TableModel.Meta) columns.append(coldesc) else: columns += [make_column(var, TableModel.Meta) for var in domain.metas] if self.X_density != Storage.DENSE and domain.attributes: coldesc = make_basket(domain.attributes, self.X_density, TableModel.Attribute) columns.append(coldesc) else: columns += [make_column(var, TableModel.Attribute) for var in domain.attributes] #: list of all domain variables (class_vars + metas + attrs) self.vars = domain.class_vars + domain.metas + domain.attributes self.columns = columns #: A list of all unique attribute labels (in all variables) self._labels = sorted( reduce(operator.ior, [set(var.attributes) for var in self.vars], set())) @lru_cache(maxsize=1000) def row_instance(index): return self.source[int(index)] self._row_instance = row_instance # column basic statistics (VariableStatsRole), computed when # first needed. self.__stats = None self.__rowCount = sourcedata.approx_len() self.__columnCount = len(self.columns) if self.__rowCount > (2 ** 31 - 1): raise ValueError("len(sourcedata) > 2 ** 31 - 1") def sortColumnData(self, column): return self._columnSortKeyData(column, TableModel.ValueRole) @deprecated('Orange.widgets.utils.itemmodels.TableModel.sortColumnData') def columnSortKeyData(self, column, role): return self._columnSortKeyData(column, role) def _columnSortKeyData(self, column, role): """ Return a sequence of source table objects which can be used as `keys` for sorting. :param int column: Sort column. :param Qt.ItemRole role: Sort item role. """ coldesc = self.columns[column] if isinstance(coldesc, TableModel.Column) \ and role == TableModel.ValueRole: col_data = numpy.asarray(self.source.get_column_view(coldesc.var)[0]) if coldesc.var.is_continuous: # continuous from metas have dtype object; cast it to float col_data = col_data.astype(float) return col_data else: return numpy.asarray([self.index(i, column).data(role) for i in range(self.rowCount())]) def data(self, index, role, # For optimizing out LOAD_GLOBAL byte code instructions in # the item role tests. _str=str, _Qt_DisplayRole=Qt.DisplayRole, _Qt_EditRole=Qt.EditRole, _Qt_BackgroundRole=Qt.BackgroundRole, _ValueRole=ValueRole, _ClassValueRole=ClassValueRole, _VariableRole=VariableRole, _DomainRole=DomainRole, _VariableStatsRole=VariableStatsRole, # Some cached local precomputed values. # All of the above roles we respond to _recognizedRoles=frozenset([Qt.DisplayRole, Qt.EditRole, Qt.BackgroundRole, ValueRole, ClassValueRole, VariableRole, DomainRole, VariableStatsRole])): """ Reimplemented from `QAbstractItemModel.data` """ if role not in _recognizedRoles: return None row, col = index.row(), index.column() if not 0 <= row <= self.__rowCount: return None row = self.mapToSourceRows(row) try: instance = self._row_instance(row) except IndexError: self.layoutAboutToBeChanged.emit() self.beginRemoveRows(self.parent(), row, max(self.rowCount(), row)) self.__rowCount = min(row, self.__rowCount) self.endRemoveRows() self.layoutChanged.emit() return None coldesc = self.columns[col] if role == _Qt_DisplayRole: return coldesc.format(instance) elif role == _Qt_EditRole and isinstance(coldesc, TableModel.Column): return instance[coldesc.var] elif role == _Qt_BackgroundRole: return coldesc.background elif role == _ValueRole and isinstance(coldesc, TableModel.Column): return instance[coldesc.var] elif role == _ClassValueRole: try: return instance.get_class() except TypeError: return None elif role == _VariableRole and isinstance(coldesc, TableModel.Column): return coldesc.var elif role == _DomainRole: return coldesc.role elif role == _VariableStatsRole: return self._stats_for_column(col) else: return None def setData(self, index, value, role): row = self.mapFromSourceRows(index.row()) if role == Qt.EditRole: try: self.source[row, index.column()] = value except (TypeError, IndexError): return False else: self.dataChanged.emit(index, index) return True else: return False def parent(self, index=QModelIndex()): """Reimplemented from `QAbstractTableModel.parent`.""" return QModelIndex() def rowCount(self, parent=QModelIndex()): """Reimplemented from `QAbstractTableModel.rowCount`.""" return 0 if parent.isValid() else self.__rowCount def columnCount(self, parent=QModelIndex()): """Reimplemented from `QAbstractTableModel.columnCount`.""" return 0 if parent.isValid() else self.__columnCount def headerData(self, section, orientation, role): """Reimplemented from `QAbstractTableModel.headerData`.""" if orientation == Qt.Vertical: if role == Qt.DisplayRole: return int(self.mapToSourceRows(section) + 1) return None coldesc = self.columns[section] if role == Qt.DisplayRole: if isinstance(coldesc, TableModel.Basket): return "{...}" else: return coldesc.var.name elif role == Qt.ToolTipRole: return self._tooltip(coldesc) elif role == TableModel.VariableRole \ and isinstance(coldesc, TableModel.Column): return coldesc.var elif role == TableModel.VariableStatsRole: return self._stats_for_column(section) elif role == TableModel.DomainRole: return coldesc.role else: return None def _tooltip(self, coldesc): """ Return an header tool tip text for an `column` descriptor. """ if isinstance(coldesc, TableModel.Basket): return None labels = self._labels variable = coldesc.var pairs = [(escape(key), escape(str(variable.attributes[key]))) for key in labels if key in variable.attributes] tip = "<b>%s</b>" % escape(variable.name) tip = "<br/>".join([tip] + ["%s = %s" % pair for pair in pairs]) return tip def _stats_for_column(self, column): """ Return BasicStats for `column` index. """ coldesc = self.columns[column] if isinstance(coldesc, TableModel.Basket): return None if self.__stats is None: self.__stats = datacaching.getCached( self.source, basic_stats.DomainBasicStats, (self.source, True) ) return self.__stats[coldesc.var]
class DropShadowFrame(QWidget): """ A widget drawing a drop shadow effect around the geometry of another widget (works similar to :class:`QFocusFrame`). Parameters ---------- parent : :class:`QObject` Parent object. color : :class:`QColor` The color of the drop shadow. radius : float Shadow radius. """ def __init__(self, parent=None, color=QColor(), radius=5, **kwargs): QWidget.__init__(self, parent, **kwargs) self.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.setAttribute(Qt.WA_NoChildEventsForParent, True) self.setFocusPolicy(Qt.NoFocus) self.__color = QColor(color) self.__radius = radius self.__widget = None self.__widgetParent = None self.__updatePixmap() def setColor(self, color): """ Set the color of the shadow. """ if not isinstance(color, QColor): color = QColor(color) if self.__color != color: self.__color = QColor(color) self.__updatePixmap() def color(self): """ Return the color of the drop shadow. By default this is a color from the `palette` (for `self.foregroundRole()`) """ if self.__color.isValid(): return QColor(self.__color) else: return self.palette().color(self.foregroundRole()) color_ = Property(QColor, fget=color, fset=setColor, designable=True, doc="Drop shadow color") def setRadius(self, radius): """ Set the drop shadow's blur radius. """ if self.__radius != radius: self.__radius = radius self.__updateGeometry() self.__updatePixmap() def radius(self): """ Return the shadow blur radius. """ return self.__radius radius_ = Property(int, fget=radius, fset=setRadius, designable=True, doc="Drop shadow blur radius.") def setWidget(self, widget): """ Set the widget around which to show the shadow. """ if self.__widget: self.__widget.removeEventFilter(self) self.__widget = widget if self.__widget: self.__widget.installEventFilter(self) # Find the parent for the frame # This is the top level window a toolbar or a viewport # of a scroll area parent = widget.parentWidget() while not (isinstance(parent, (QAbstractScrollArea, QToolBar)) or \ parent.isWindow()): parent = parent.parentWidget() if isinstance(parent, QAbstractScrollArea): parent = parent.viewport() self.__widgetParent = parent self.setParent(parent) self.stackUnder(widget) self.__updateGeometry() self.setVisible(widget.isVisible()) def widget(self): """ Return the widget that was set by `setWidget`. """ return self.__widget def paintEvent(self, event): # TODO: Use QPainter.drawPixmapFragments on Qt 4.7 opt = QStyleOption() opt.initFrom(self) pixmap = self.__shadowPixmap shadow_rect = QRectF(opt.rect) widget_rect = QRectF(self.widget().geometry()) widget_rect.moveTo(self.radius_, self.radius_) left = top = right = bottom = self.radius_ pixmap_rect = QRectF(QPointF(0, 0), QSizeF(pixmap.size())) # Shadow casting rectangle in the source pixmap. pixmap_shadow_rect = pixmap_rect.adjusted(left, top, -right, -bottom) source_rects = self.__shadowPixmapFragments(pixmap_rect, pixmap_shadow_rect) target_rects = self.__shadowPixmapFragments(shadow_rect, widget_rect) painter = QPainter(self) for source, target in zip(source_rects, target_rects): painter.drawPixmap(target, pixmap, source) painter.end() def eventFilter(self, obj, event): etype = event.type() if etype == QEvent.Move or etype == QEvent.Resize: self.__updateGeometry() elif etype == QEvent.Show: self.__updateGeometry() self.show() elif etype == QEvent.Hide: self.hide() return QWidget.eventFilter(self, obj, event) def __updateGeometry(self): """ Update the shadow geometry to fit the widget's changed geometry. """ widget = self.__widget parent = self.__widgetParent radius = self.radius_ pos = widget.pos() if parent != widget.parentWidget(): pos = widget.parentWidget().mapTo(parent, pos) geom = QRect(pos, widget.size()) geom.adjust(-radius, -radius, radius, radius) if geom != self.geometry(): self.setGeometry(geom) # Set the widget mask (punch a hole through to the `widget` instance. rect = self.rect() mask = QRegion(rect) transparent = QRegion(rect.adjusted(radius, radius, -radius, -radius)) mask = mask.subtracted(transparent) self.setMask(mask) def __updatePixmap(self): """ Update the cached shadow pixmap. """ rect_size = QSize(50, 50) left = top = right = bottom = self.radius_ # Size of the pixmap. pixmap_size = QSize(rect_size.width() + left + right, rect_size.height() + top + bottom) shadow_rect = QRect(QPoint(left, top), rect_size) pixmap = QPixmap(pixmap_size) pixmap.fill(QColor(0, 0, 0, 0)) rect_fill_color = self.palette().color(QPalette.Window) pixmap = render_drop_shadow_frame( pixmap, QRectF(shadow_rect), shadow_color=self.color_, offset=QPointF(0, 0), radius=self.radius_, rect_fill_color=rect_fill_color ) self.__shadowPixmap = pixmap self.update() def __shadowPixmapFragments(self, pixmap_rect, shadow_rect): """ Return a list of 8 QRectF fragments for drawing a shadow. """ s_left, s_top, s_right, s_bottom = \ shadow_rect.left(), shadow_rect.top(), \ shadow_rect.right(), shadow_rect.bottom() s_width, s_height = shadow_rect.width(), shadow_rect.height() p_width, p_height = pixmap_rect.width(), pixmap_rect.height() top_left = QRectF(0.0, 0.0, s_left, s_top) top = QRectF(s_left, 0.0, s_width, s_top) top_right = QRectF(s_right, 0.0, p_width - s_width, s_top) right = QRectF(s_right, s_top, p_width - s_right, s_height) right_bottom = QRectF(shadow_rect.bottomRight(), pixmap_rect.bottomRight()) bottom = QRectF(shadow_rect.bottomLeft(), pixmap_rect.bottomRight() - \ QPointF(p_width - s_right, 0.0)) bottom_left = QRectF(shadow_rect.bottomLeft() - QPointF(s_left, 0.0), pixmap_rect.bottomLeft() + QPointF(s_left, 0.0)) left = QRectF(pixmap_rect.topLeft() + QPointF(0.0, s_top), shadow_rect.bottomLeft()) return [top_left, top, top_right, right, right_bottom, bottom, bottom_left, left]
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 changeDiscreteColor(self, item): r, g, b = item.rgbColor color = QColorDialog.getColor(QColor(r, g, b), self) if color.isValid(): item.setIcon(ColorPixmap(color, 25)) item.rgbColor = (color.red(), color.green(), color.blue())
def to_rgb_tuple(self, color): try: color = QColor(*color) except TypeError: color = QColor(color) return color.red(), color.green(), color.blue()
def __getitem__(self, values): if isinstance(values, Number): return QColor(*self.getRGB(values)) return [QColor(*c) for c in self.getRGB(values)]
def __getitem__(self, value): if isinstance(value, Iterable): return [QColor(*c) for c in self.getRGB(value)] return QColor(*self.getRGB(value))
def hoverEnterEvent(self, event): super().hoverEnterEvent(event) self.setBrush(QBrush(QColor(100, 100, 100))) self.update()
def paintEvent(self, event): super().paintEvent(event) painter = QPainter(self.viewport()) painter.setBrush(QColor(100, 100, 100, 100)) painter.setRenderHints(self.renderHints()) painter.drawPolygon(self.viewPolygon())
def _setup_table_view(self, view, data): """Setup the `view` (QTableView) with `data` (Orange.data.Table) """ if data is None: view.setModel(None) return datamodel = RichTableModel(data) rowcount = data.approx_len() if self.color_by_class and data.domain.has_discrete_class: color_schema = [QColor(*c) for c in data.domain.class_var.colors] else: color_schema = None if self.show_distributions: view.setItemDelegate( gui.TableBarItem(self, color=self.dist_color, color_schema=color_schema)) else: view.setItemDelegate(QStyledItemDelegate(self)) # Enable/disable view sorting based on data's type view.setSortingEnabled(is_sortable(data)) header = view.horizontalHeader() header.setSectionsClickable(is_sortable(data)) header.setSortIndicatorShown(is_sortable(data)) header.sortIndicatorChanged.connect(self.update_selection) view.setModel(datamodel) vheader = view.verticalHeader() option = view.viewOptions() size = view.style().sizeFromContents(QStyle.CT_ItemViewItem, option, QSize(20, 20), view) vheader.setDefaultSectionSize(size.height() + 2) vheader.setMinimumSectionSize(5) vheader.setSectionResizeMode(QHeaderView.Fixed) # Limit the number of rows displayed in the QTableView # (workaround for QTBUG-18490 / QTBUG-28631) maxrows = (2**31 - 1) // (vheader.defaultSectionSize() + 2) if rowcount > maxrows: sliceproxy = TableSliceProxy(parent=view, rowSlice=slice(0, maxrows)) sliceproxy.setSourceModel(datamodel) # First reset the view (without this the header view retains # it's state - at this point invalid/broken) view.setModel(None) view.setModel(sliceproxy) assert view.model().rowCount() <= maxrows assert vheader.sectionSize(0) > 1 or datamodel.rowCount() == 0 # update the header (attribute names) self._update_variable_labels(view) selmodel = BlockSelectionModel(view.model(), parent=view, selectBlocks=not self.select_rows) view.setSelectionModel(selmodel) view.selectionFinished.connect(self.update_selection)
class ArrowAnnotation(Annotation): def __init__(self, parent=None, line=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) if line is None: line = QLineF(0, 0, 20, 0) self.__line = QLineF(line) self.__color = QColor(Qt.red) # An item with the same shape as this arrow, stacked behind this # item as a source for QGraphicsDropShadowEffect. Cannot attach # the effect to this item directly as QGraphicsEffect makes the item # non devicePixelRatio aware. self.__arrowShadowBase = ArrowItem(self, line=line) self.__arrowShadowBase.setPen(Qt.NoPen) # no pen -> slightly thinner self.__arrowShadowBase.setBrush(QBrush(self.__color)) self.__arrowShadowBase.setArrowStyle(ArrowItem.Concave) self.__arrowShadowBase.setLineWidth(5) self.__shadow = QGraphicsDropShadowEffect( blurRadius=5, offset=QPointF(1.0, 2.0), ) self.__arrowShadowBase.setGraphicsEffect(self.__shadow) self.__shadow.setEnabled(True) # The 'real' shape item self.__arrowItem = ArrowItem(self, line=line) self.__arrowItem.setBrush(self.__color) self.__arrowItem.setPen(QPen(self.__color)) self.__arrowItem.setArrowStyle(ArrowItem.Concave) self.__arrowItem.setLineWidth(5) self.__autoAdjustGeometry = True def setAutoAdjustGeometry(self, autoAdjust): """ If set to `True` then the geometry will be adjusted whenever the arrow is changed with `setLine`. Otherwise the geometry of the item is only updated so the `line` lies within the `geometry()` rect (i.e. it only grows). True by default """ self.__autoAdjustGeometry = autoAdjust if autoAdjust: self.adjustGeometry() def autoAdjustGeometry(self): """ Should the geometry of the item be adjusted automatically when `setLine` is called. """ return self.__autoAdjustGeometry def setLine(self, line): """ Set the arrow base line (a `QLineF` in object coordinates). """ if self.__line != line: self.__line = QLineF(line) # local item coordinate system geom = self.geometry().translated(-self.pos()) if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) arrow_shape = arrow_path_concave(line, self.lineWidth()) arrow_rect = arrow_shape.boundingRect() if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) if self.__autoAdjustGeometry: # Shrink the geometry if required. geom = geom.intersected(arrow_rect) # topLeft can move changing the local coordinates. diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) self.__arrowItem.setLine(line) self.__arrowShadowBase.setLine(line) self.__line = line # parent item coordinate system geom.translate(self.pos()) self.setGeometry(geom) def line(self): """ Return the arrow base line (`QLineF` in object coordinates). """ return QLineF(self.__line) def setColor(self, color): """ Set arrow brush color. """ if self.__color != color: self.__color = QColor(color) self.__updateStyleState() def color(self): """ Return the arrow brush color. """ return QColor(self.__color) def setLineWidth(self, lineWidth): """ Set the arrow line width. """ self.__arrowItem.setLineWidth(lineWidth) self.__arrowShadowBase.setLineWidth(lineWidth) def lineWidth(self): """ Return the arrow line width. """ return self.__arrowItem.lineWidth() def adjustGeometry(self): """ Adjust the widget geometry to exactly fit the arrow inside while preserving the arrow path scene geometry. """ # local system coordinate geom = self.geometry().translated(-self.pos()) line = self.__line arrow_rect = self.__arrowItem.shape().boundingRect() if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) geom = geom.intersected(arrow_rect) diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) geom.translate(self.pos()) self.setGeometry(geom) self.setLine(line) def shape(self): arrow_shape = self.__arrowItem.shape() return self.mapFromItem(self.__arrowItem, arrow_shape) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: self.__updateStyleState() return Annotation.itemChange(self, change, value) def __updateStyleState(self): """ Update the arrows' brush, pen, ... based on it's state """ if self.isSelected(): color = self.__color.darker(150) pen = QPen(QColor(96, 158, 215), Qt.DashDotLine) pen.setWidthF(1.25) pen.setCosmetic(True) shadow = pen.color().darker(150) else: color = self.__color pen = QPen(color) shadow = QColor(63, 63, 63, 180) self.__arrowShadowBase.setBrush(color) self.__shadow.setColor(shadow) self.__arrowItem.setBrush(color) self.__arrowItem.setPen(pen)
class OWBoxPlot(widget.OWWidget): """ Here's how the widget's functions call each other: - `set_data` is a signal handler fills the list boxes and calls `grouping_changed`. - `grouping_changed` handles changes of grouping attribute: it enables or disables the box for ordering, orders attributes and calls `attr_changed`. - `attr_changed` handles changes of attribute. It recomputes box data by calling `compute_box_data`, shows the appropriate display box (discrete/continuous) and then calls`layout_changed` - `layout_changed` constructs all the elements for the scene (as lists of QGraphicsItemGroup) and calls `display_changed`. It is called when the attribute or grouping is changed (by attr_changed) and on resize event. - `display_changed` puts the elements corresponding to the current display settings on the scene. It is called when the elements are reconstructed (layout is changed due to selection of attributes or resize event), or when the user changes display settings or colors. For discrete attributes, the flow is a bit simpler: the elements are not constructed in advance (by layout_changed). Instead, layout_changed and display_changed call display_changed_disc that draws everything. """ name = "Box Plot" description = "Visualize the distribution of feature values in a box plot." icon = "icons/BoxPlot.svg" priority = 100 keywords = ["whisker"] class Inputs: data = Input("Data", Orange.data.Table) class Outputs: selected_data = Output("Selected Data", Orange.data.Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table) #: Comparison types for continuous variables CompareNone, CompareMedians, CompareMeans = 0, 1, 2 settingsHandler = DomainContextHandler() conditions = ContextSetting([]) attribute = ContextSetting(None) order_by_importance = Setting(False) group_var = ContextSetting(None) show_annotations = Setting(True) compare = Setting(CompareMeans) stattest = Setting(0) sig_threshold = Setting(0.05) stretched = Setting(True) show_labels = Setting(True) sort_freqs = Setting(False) auto_commit = Setting(True) _sorting_criteria_attrs = { CompareNone: "", CompareMedians: "median", CompareMeans: "mean" } _pen_axis_tick = QPen(Qt.white, 5) _pen_axis = QPen(Qt.darkGray, 3) _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2) _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2) _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1) _pen_dotted.setStyle(Qt.DotLine) _post_line_pen = QPen(Qt.lightGray, 2) _post_grp_pen = QPen(Qt.lightGray, 4) for pen in (_pen_paramet, _pen_median, _pen_dotted, _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen): pen.setCosmetic(True) pen.setCapStyle(Qt.RoundCap) pen.setJoinStyle(Qt.RoundJoin) _pen_axis_tick.setCapStyle(Qt.FlatCap) _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0)) _axis_font = QFont() _axis_font.setPixelSize(12) _label_font = QFont() _label_font.setPixelSize(11) _attr_brush = QBrush(QColor(0x33, 0x00, 0xff)) graph_name = "box_scene" def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.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="Variable", model=self.attrs, callback=self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) gui.separator(view.box, 6, 6) self.cb_order = gui.checkBox( view.box, self, "order_by_importance", "Order by relevance", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_sorting) self.group_vars = DomainModel(placeholder="None", separators=False, valid_types=Orange.data.DiscreteVariable) self.group_view = view = gui.listView(self.controlArea, self, "group_var", box="Subgroups", model=self.group_vars, callback=self.grouping_changed) view.setEnabled(False) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox(self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum), addSpace=False) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.layout_changed) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox(self.controlArea, box="Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox(box, self, 'stretched', "Stretch bars", callback=self.display_changed) gui.checkBox(box, self, 'show_labels', "Show box labels", callback=self.display_changed) self.sort_cb = gui.checkBox(box, self, 'sort_freqs', "Sort by subgroup frequencies", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_scene.selectionChanged.connect(self.commit) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>No test results.</center>") self.mainArea.setMinimumWidth(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)): 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: return self.display_changed_disc() 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: return self.display_changed_disc() 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: 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 last_to == to or frm == to: 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): if self.attribute: return self.attribute.name 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)
class FeatureStatisticsTableModel(AbstractSortTableModel): CLASS_VAR, META, ATTRIBUTE = range(3) COLOR_FOR_ROLE = { CLASS_VAR: QColor(160, 160, 160), META: QColor(220, 220, 200), ATTRIBUTE: QColor(255, 255, 255), } HIDDEN_VAR_TYPES = (StringVariable, ) class Columns(IntEnum): ICON, NAME, DISTRIBUTION, CENTER, MEDIAN, DISPERSION, MIN, MAX, \ MISSING = range(9) @property def name(self): return { self.ICON: '', self.NAME: 'Name', self.DISTRIBUTION: 'Distribution', self.CENTER: 'Mean', self.MEDIAN: 'Median', self.DISPERSION: 'Dispersion', self.MIN: 'Min.', self.MAX: 'Max.', self.MISSING: 'Missing', }[self.value] @property def index(self): return self.value @classmethod def from_index(cls, index): return cls(index) def __init__(self, data=None, parent=None): """ Parameters ---------- data : Optional[Table] parent : Optional[QWidget] """ super().__init__(parent) self.table = None # type: Optional[Table] self.domain = None # type: Optional[Domain] self.target_var = None # type: Optional[Variable] self.n_attributes = self.n_instances = 0 self.__attributes = self.__class_vars = self.__metas = None # sets of variables for fast membership tests self.__attributes_set = set() self.__class_vars_set = set() self.__metas_set = set() self.__distributions_cache = {} no_data = np.array([]) self._variable_types = self._variable_names = no_data self._min = self._max = self._center = self._median = no_data self._dispersion = no_data self._missing = no_data # Clear model initially to set default values self.clear() self.set_data(data) def set_data(self, data): if data is None: self.clear() return self.beginResetModel() self.table = data self.domain = domain = data.domain self.target_var = None self.__attributes = self.__filter_attributes(domain.attributes, self.table.X) # We disable pylint warning because the `Y` property squeezes vectors, # while we need a 2d array, which `_Y` provides self.__class_vars = self.__filter_attributes(domain.class_vars, self.table._Y) # pylint: disable=protected-access self.__metas = self.__filter_attributes(domain.metas, self.table.metas) self.__attributes_set = set(self.__metas[0]) self.__class_vars_set = set(self.__class_vars[0]) self.__metas_set = set(self.__metas[0]) self.n_attributes = len(self.variables) self.n_instances = len(data) self.__distributions_cache = {} self.__compute_statistics() self.endResetModel() def clear(self): self.beginResetModel() self.table = self.domain = self.target_var = None self.n_attributes = self.n_instances = 0 self.__attributes = (np.array([]), np.array([])) self.__class_vars = (np.array([]), np.array([])) self.__metas = (np.array([]), np.array([])) self.__attributes_set = set() self.__class_vars_set = set() self.__metas_set = set() self.__distributions_cache.clear() self.endResetModel() @property def variables(self): matrices = [ self.__attributes[0], self.__class_vars[0], self.__metas[0] ] if not any(m.size for m in matrices): return [] return np.hstack(matrices) @staticmethod def _attr_indices(attrs): # type: (List) -> Tuple[List[int], List[int], List[int], List[int]] """Get the indices of different attribute types eg. discrete.""" disc_var_idx = [ i for i, attr in enumerate(attrs) if isinstance(attr, DiscreteVariable) ] cont_var_idx = [ i for i, attr in enumerate(attrs) if isinstance(attr, ContinuousVariable) and not isinstance(attr, TimeVariable) ] time_var_idx = [ i for i, attr in enumerate(attrs) if isinstance(attr, TimeVariable) ] string_var_idx = [ i for i, attr in enumerate(attrs) if isinstance(attr, StringVariable) ] return disc_var_idx, cont_var_idx, time_var_idx, string_var_idx def __filter_attributes(self, attributes, matrix): """Filter out variables which shouldn't be visualized.""" attributes, matrix = np.asarray(attributes), matrix mask = [ idx for idx, attr in enumerate(attributes) if not isinstance(attr, self.HIDDEN_VAR_TYPES) ] return attributes[mask], matrix[:, mask] def __compute_statistics(self): # Since data matrices can of mixed sparsity, we need to compute # attributes separately for each of them. matrices = [self.__attributes, self.__class_vars, self.__metas] # Filter out any matrices with size 0 matrices = list(filter(lambda tup: tup[1].size, matrices)) self._variable_types = np.array([type(var) for var in self.variables]) self._variable_names = np.array( [var.name.lower() for var in self.variables]) self._min = self.__compute_stat( matrices, discrete_f=lambda x: ut.nanmin(x, axis=0), continuous_f=lambda x: ut.nanmin(x, axis=0), time_f=lambda x: ut.nanmin(x, axis=0), ) self._dispersion = self.__compute_stat( matrices, discrete_f=_categorical_entropy, continuous_f=coefficient_of_variation, ) self._missing = self.__compute_stat( matrices, discrete_f=lambda x: ut.countnans(x, axis=0), continuous_f=lambda x: ut.countnans(x, axis=0), string_f=lambda x: (x == StringVariable.Unknown).sum(axis=0), time_f=lambda x: ut.countnans(x, axis=0), default_val=len(matrices[0]) if matrices else 0) self._max = self.__compute_stat( matrices, discrete_f=lambda x: ut.nanmax(x, axis=0), continuous_f=lambda x: ut.nanmax(x, axis=0), time_f=lambda x: ut.nanmax(x, axis=0), ) # Since scipy apparently can't do mode on sparse matrices, cast it to # dense. This can be very inefficient for large matrices, and should # be changed def __mode(x, *args, **kwargs): if sp.issparse(x): x = x.todense(order="C") # return ss.mode(x, *args, **kwargs)[0] return ut.nanmode(x, *args, **kwargs)[0] # Temporary replacement for scipy self._center = self.__compute_stat( matrices, discrete_f=None, continuous_f=lambda x: ut.nanmean(x, axis=0), time_f=lambda x: ut.nanmean(x, axis=0), ) self._median = self.__compute_stat( matrices, discrete_f=lambda x: __mode(x, axis=0), continuous_f=lambda x: ut.nanmedian(x, axis=0), time_f=lambda x: ut.nanmedian(x, axis=0), ) def get_statistics_matrix(self, variables=None, return_labels=False): """Get the numeric computed statistics in a single matrix. Optionally, we can specify for which variables we want the stats. Also, we can get the string column names as labels if desired. Parameters ---------- variables : Iterable[Union[Variable, int, str]] Return statistics for only the variables specified. Accepts all formats supported by `domain.index` return_labels : bool In addition to the statistics matrix, also return string labels for the columns of the matrix e.g. 'Mean' or 'Dispersion', as specified in `Columns`. Returns ------- Union[Tuple[List[str], np.ndarray], np.ndarray] """ if self.table is None: return np.atleast_2d([]) # If a list of variables is given, select only corresponding stats # variables can be a list or array, pylint: disable=len-as-condition if variables is not None and len(variables) != 0: indices = [self.domain.index(var) for var in variables] else: indices = ... matrix = np.vstack(( self._center[indices], self._median[indices], self._dispersion[indices], self._min[indices], self._max[indices], self._missing[indices], )).T # Return string labels for the returned matrix columns e.g. 'Mean', # 'Dispersion' if requested if return_labels: labels = [ self.Columns.CENTER.name, self.Columns.MEDIAN.name, self.Columns.DISPERSION.name, self.Columns.MIN.name, self.Columns.MAX.name, self.Columns.MISSING.name ] return labels, matrix return matrix def __compute_stat(self, matrices, discrete_f=None, continuous_f=None, time_f=None, string_f=None, default_val=np.nan): """Apply functions to appropriate variable types. The default value is returned if there is no function defined for specific variable types. """ if not matrices: return np.array([]) results = [] for variables, x in matrices: result = np.full(len(variables), default_val) # While the following caching and checks are messy, the indexing # turns out to be a bottleneck for large datasets, so a single # indexing operation improves performance *idxs, str_idx = self._attr_indices(variables) for func, idx in zip((discrete_f, continuous_f, time_f), idxs): idx = np.array(idx) if func and idx.size: x_ = x[:, idx] if x_.size: if not np.issubdtype(x_.dtype, np.number): x_ = x_.astype(np.float64) try: finites = np.isfinite(x_) except TypeError: result[idx] = func(x_) else: mask = np.any(finites, axis=0) if np.any(mask): result[idx[mask]] = func(x_[:, mask]) if string_f: x_ = x[:, str_idx] if x_.size: if x_.dtype is not np.object: x_ = x_.astype(np.object) result[str_idx] = string_f(x_) results.append(result) return np.hstack(results) def sortColumnData(self, column): """Prepare the arrays with which we will sort the rows. If we want to sort based on a single value e.g. the name, return a 1d array. Sometimes we may want to sort by multiple criteria, comparing continuous variances with discrete entropies makes no sense, so we want to group those variable types together. """ # Prepare indices for variable types so we can group them together order = [ ContinuousVariable, TimeVariable, DiscreteVariable, StringVariable ] mapping = {var: idx for idx, var in enumerate(order)} vmapping = np.vectorize(mapping.__getitem__) var_types_indices = vmapping(self._variable_types) # Store the variable name sorted indices so we can pass a default # order when sorting by multiple keys # Double argsort is "inverse" argsort: # data will be *sorted* by these indices var_name_indices = np.argsort(np.argsort(self._variable_names)) # Prepare vartype indices so ready when needed disc_idx, _, time_idx, str_idx = self._attr_indices(self.variables) # Sort by: (type) if column == self.Columns.ICON: return var_types_indices # Sort by: (name) elif column == self.Columns.NAME: # We use `_variable_names` here and not the indices because the # last (or single) row is actually sorted and we don't want to sort # the indices return self._variable_names # Sort by: (None) elif column == self.Columns.DISTRIBUTION: return np.ones_like(var_types_indices) # Sort by: (type, center) elif column == self.Columns.CENTER: # Sorting discrete or string values by mean makes no sense vals = np.array(self._center) vals[disc_idx] = var_name_indices[disc_idx] vals[str_idx] = var_name_indices[str_idx] return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T # Sort by: (type, median) elif column == self.Columns.MEDIAN: # Sorting discrete or string values by median makes no sense vals = np.array(self._median) vals[disc_idx] = var_name_indices[disc_idx] vals[str_idx] = var_name_indices[str_idx] return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T # Sort by: (type, dispersion) elif column == self.Columns.DISPERSION: # Sort time variables by their dispersion, which is not stored in # the dispersion array vals = np.array(self._dispersion) vals[time_idx] = self._max[time_idx] - self._min[time_idx] return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T # Sort by: (type, min) elif column == self.Columns.MIN: # Sorting discrete or string values by min makes no sense vals = np.array(self._min) vals[disc_idx] = var_name_indices[disc_idx] vals[str_idx] = var_name_indices[str_idx] return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T # Sort by: (type, max) elif column == self.Columns.MAX: # Sorting discrete or string values by min makes no sense vals = np.array(self._max) vals[disc_idx] = var_name_indices[disc_idx] vals[str_idx] = var_name_indices[str_idx] return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T # Sort by: (missing) elif column == self.Columns.MISSING: return self._missing return None def _sortColumnData(self, column): """Allow sorting with 2d arrays.""" data = np.asarray(self.sortColumnData(column)) data = data[self.mapToSourceRows(Ellipsis)] assert data.ndim <= 2, 'Data should be at most 2-dimensional' return data def _argsortData(self, data, order): if data.ndim == 1: if np.issubdtype(data.dtype, np.number): if order == Qt.DescendingOrder: data = -data indices = np.argsort(data, kind='stable') # Always sort NaNs last if np.issubdtype(data.dtype, np.number): indices = np.roll(indices, -np.isnan(data).sum()) else: # When not sorting by numbers, we can't do data = -data, but # use indices = indices[::-1] instead. This is not stable, but # doesn't matter because we use this only for variable names # which are guaranteed to be unique indices = np.argsort(data) if order == Qt.DescendingOrder: indices = indices[::-1] else: assert np.issubdtype(data.dtype, np.number), \ 'We do not deal with non numeric values in sorting by ' \ 'multiple values' if order == Qt.DescendingOrder: data[:, -1] = -data[:, -1] # In order to make sure NaNs always appear at the end, insert a # indicator whether NaN or not. Note that the data array must # contain an empty column of zeros at index -2 since inserting an # extra column after the fact can result in a MemoryError for data # with a large amount of variables assert np.all(data[:, -2] == 0), \ 'Add an empty column of zeros at index -2 to accomodate NaNs' np.isnan(data[:, -1], out=data[:, -2]) indices = np.lexsort(np.flip(data.T, axis=0)) return indices def headerData(self, section, orientation, role): # type: (int, Qt.Orientation, Qt.ItemDataRole) -> Any if orientation == Qt.Horizontal: if role == Qt.DisplayRole: return self.Columns.from_index(section).name return None def data(self, index, role): # type: (QModelIndex, Qt.ItemDataRole) -> Any def background(): if attribute in self.__attributes_set: return self.COLOR_FOR_ROLE[self.ATTRIBUTE] if attribute in self.__metas_set: return self.COLOR_FOR_ROLE[self.META] if attribute in self.__class_vars_set: return self.COLOR_FOR_ROLE[self.CLASS_VAR] return None def text_alignment(): if column == self.Columns.NAME: return Qt.AlignLeft | Qt.AlignVCenter return Qt.AlignRight | Qt.AlignVCenter def decoration(): if column == self.Columns.ICON: return gui.attributeIconDict[attribute] return None def display(): # pylint: disable=too-many-branches def format_zeros(str_val): """Zeros should be handled separately as they cannot be negative.""" if float(str_val) == 0: num_decimals = min(self.variables[row].number_of_decimals, 2) str_val = f"{0:.{num_decimals}f}" return str_val def render_value(value): if np.isnan(value): return "" if np.isinf(value): return "∞" str_val = attribute.str_val(value) if attribute.is_continuous and not attribute.is_time: str_val = format_zeros(str_val) return str_val if column == self.Columns.NAME: return attribute.name elif column == self.Columns.DISTRIBUTION: if isinstance(attribute, (DiscreteVariable, ContinuousVariable)): if row not in self.__distributions_cache: scene = QGraphicsScene(parent=self) histogram = Histogram( data=self.table, variable=attribute, color_attribute=self.target_var, border=(0, 0, 2, 0), border_color='#ccc', ) scene.addItem(histogram) self.__distributions_cache[row] = scene return self.__distributions_cache[row] elif column == self.Columns.CENTER: return render_value(self._center[row]) elif column == self.Columns.MEDIAN: return render_value(self._median[row]) elif column == self.Columns.DISPERSION: if isinstance(attribute, TimeVariable): return format_time_diff(self._min[row], self._max[row]) elif isinstance(attribute, DiscreteVariable): return "%.3g" % self._dispersion[row] else: return render_value(self._dispersion[row]) elif column == self.Columns.MIN: if not isinstance(attribute, DiscreteVariable): return render_value(self._min[row]) elif column == self.Columns.MAX: if not isinstance(attribute, DiscreteVariable): return render_value(self._max[row]) elif column == self.Columns.MISSING: return '%d (%d%%)' % (self._missing[row], 100 * self._missing[row] / self.n_instances) return None roles = { Qt.BackgroundRole: background, Qt.TextAlignmentRole: text_alignment, Qt.DecorationRole: decoration, Qt.DisplayRole: display } if not index.isValid() or role not in roles: return None row, column = self.mapToSourceRows(index.row()), index.column() # Make sure we're not out of range if not 0 <= row <= self.n_attributes: return None attribute = self.variables[row] return roles[role]() def rowCount(self, parent=QModelIndex()): return 0 if parent.isValid() else self.n_attributes def columnCount(self, parent=QModelIndex()): return 0 if parent.isValid() else len(self.Columns) def set_target_var(self, variable): self.target_var = variable self.__distributions_cache.clear() start_idx = self.index(0, self.Columns.DISTRIBUTION) end_idx = self.index(self.rowCount(), self.Columns.DISTRIBUTION) self.dataChanged.emit(start_idx, end_idx)
def main(argv=None): # Allow termination with CTRL + C signal.signal(signal.SIGINT, signal.SIG_DFL) # Disable pyqtgraph's atexit and QApplication.aboutToQuit cleanup handlers. pyqtgraph.setConfigOption("exitCleanup", False) if argv is None: argv = sys.argv usage = "usage: %prog [options] [workflow_file]" parser = optparse.OptionParser(usage=usage) parser.add_option("--no-discovery", action="store_true", help="Don't run widget discovery " "(use full cache instead)") parser.add_option("--force-discovery", action="store_true", help="Force full widget discovery " "(invalidate cache)") parser.add_option("--clear-widget-settings", action="store_true", help="Remove stored widget setting") parser.add_option("--no-welcome", action="store_true", help="Don't show welcome dialog.") parser.add_option("--no-splash", action="store_true", help="Don't show splash screen.") parser.add_option("-l", "--log-level", help="Logging level (0, 1, 2, 3, 4)", type="int", default=1) parser.add_option("--style", help="QStyle to use", type="str", default=None) parser.add_option("--stylesheet", help="Application level CSS style sheet to use", type="str", default=None) parser.add_option("--qt", help="Additional arguments for QApplication", type="str", default=None) (options, args) = parser.parse_args(argv[1:]) levels = [ logging.CRITICAL, logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG ] # Fix streams before configuring logging (otherwise it will store # and write to the old file descriptors) fix_win_pythonw_std_stream() # Try to fix macOS automatic window tabbing (Sierra and later) fix_macos_nswindow_tabbing() logging.basicConfig( level=levels[options.log_level], handlers=[make_stdout_handler(levels[options.log_level])]) # set default application configuration config_ = config.Config() canvasconfig.set_default(config_) log.info("Starting 'Orange Canvas' application.") qt_argv = argv[:1] style = options.style defaultstylesheet = "orange.qss" fusiontheme = None if style is not None: if style.startswith("fusion:"): qt_argv += ["-style", "fusion"] _, _, fusiontheme = style.partition(":") else: qt_argv += ["-style", style] if options.qt is not None: qt_argv += shlex.split(options.qt) qt_argv += args if QT_VERSION >= 0x50600: CanvasApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) log.debug("Starting CanvasApplicaiton with argv = %r.", qt_argv) app = CanvasApplication(qt_argv) config_.init() if app.style().metaObject().className() == "QFusionStyle": if fusiontheme == "breeze-dark": app.setPalette(breeze_dark()) defaultstylesheet = "darkorange.qss" palette = app.palette() if style is None and palette.color(QPalette.Window).value() < 127: log.info("Switching default stylesheet to darkorange") defaultstylesheet = "darkorange.qss" # Initialize SQL query and execution time logger (in SqlTable) sql_level = min(levels[options.log_level], logging.INFO) make_sql_logger(sql_level) clear_settings_flag = os.path.join(widget_settings_dir(), "DELETE_ON_START") if options.clear_widget_settings or \ os.path.isfile(clear_settings_flag): log.info("Clearing widget settings") shutil.rmtree(widget_settings_dir(), ignore_errors=True) # Set http_proxy environment variables, after (potentially) clearing settings fix_set_proxy_env() # Setup file log handler for the select logger list - this is always # at least INFO level = min(levels[options.log_level], logging.INFO) file_handler = logging.FileHandler(filename=os.path.join( config.log_dir(), "canvas.log"), mode="w") formatter = logging.Formatter( "%(asctime)s:%(levelname)s:%(name)s: %(message)s") file_handler.setFormatter(formatter) file_handler.setLevel(level) stream = TextStream() stream_handler = logging.StreamHandler(stream) stream_handler.setFormatter(formatter) stream_handler.setLevel(level) for namespace in ["orangecanvas", "orangewidget", "Orange"]: logger = logging.getLogger(namespace) logger.setLevel(level) logger.addHandler(file_handler) logger.addHandler(stream_handler) # intercept any QFileOpenEvent requests until the main window is # fully initialized. # NOTE: The QApplication must have the executable ($0) and filename # arguments passed in argv otherwise the FileOpen events are # triggered for them (this is done by Cocoa, but QApplicaiton filters # them out if passed in argv) open_requests = [] def onrequest(url): log.info("Received an file open request %s", url) open_requests.append(url) app.fileOpenRequest.connect(onrequest) settings = QSettings() settings.setValue('startup/launch-count', settings.value('startup/launch-count', 0, int) + 1) if settings.value("reporting/send-statistics", False, type=bool) \ and is_release: UsageStatistics.set_enabled(True) stylesheet = options.stylesheet or defaultstylesheet stylesheet_string = None if stylesheet != "none": if os.path.isfile(stylesheet): with open(stylesheet, "r") as f: stylesheet_string = f.read() else: if not os.path.splitext(stylesheet)[1]: # no extension stylesheet = os.path.extsep.join([stylesheet, "qss"]) pkg_name = orangecanvas.__name__ resource = "styles/" + stylesheet if pkg_resources.resource_exists(pkg_name, resource): stylesheet_string = \ pkg_resources.resource_string(pkg_name, resource).decode() base = pkg_resources.resource_filename(pkg_name, "styles") pattern = re.compile( r"^\s@([a-zA-Z0-9_]+?)\s*:\s*([a-zA-Z0-9_/]+?);\s*$", flags=re.MULTILINE) matches = pattern.findall(stylesheet_string) for prefix, search_path in matches: QDir.addSearchPath(prefix, os.path.join(base, search_path)) log.info("Adding search path %r for prefix, %r", search_path, prefix) stylesheet_string = pattern.sub("", stylesheet_string) else: log.info("%r style sheet not found.", stylesheet) # Add the default canvas_icons search path dirpath = os.path.abspath(os.path.dirname(orangecanvas.__file__)) QDir.addSearchPath("canvas_icons", os.path.join(dirpath, "icons")) canvas_window = MainWindow() canvas_window.setAttribute(Qt.WA_DeleteOnClose) canvas_window.setWindowIcon(config.application_icon()) canvas_window.connect_output_stream(stream) # initialize notification server, set to initial canvas notif_server = NotificationServer() canvas.notification_server_instance = notif_server canvas_window.set_notification_server(notif_server) if stylesheet_string is not None: canvas_window.setStyleSheet(stylesheet_string) if not options.force_discovery: reg_cache = cache.registry_cache() else: reg_cache = None widget_registry = qt.QtWidgetRegistry() widget_discovery = config_.widget_discovery(widget_registry, cached_descriptions=reg_cache) want_splash = \ settings.value("startup/show-splash-screen", True, type=bool) and \ not options.no_splash if want_splash: pm, rect = config.splash_screen() splash_screen = SplashScreen(pixmap=pm, textRect=rect) splash_screen.setFont(QFont("Helvetica", 12)) color = QColor("#FFD39F") def show_message(message): splash_screen.showMessage(message, color=color) widget_registry.category_added.connect(show_message) log.info("Running widget discovery process.") cache_filename = os.path.join(config.cache_dir(), "widget-registry.pck") if options.no_discovery: with open(cache_filename, "rb") as f: widget_registry = pickle.load(f) widget_registry = qt.QtWidgetRegistry(widget_registry) else: if want_splash: splash_screen.show() widget_discovery.run(config.widgets_entry_points()) if want_splash: splash_screen.hide() splash_screen.deleteLater() # Store cached descriptions cache.save_registry_cache(widget_discovery.cached_descriptions) with open(cache_filename, "wb") as f: pickle.dump(WidgetRegistry(widget_registry), f) set_global_registry(widget_registry) canvas_window.set_widget_registry(widget_registry) canvas_window.show() canvas_window.raise_() want_welcome = \ settings.value("startup/show-welcome-screen", True, type=bool) \ and not options.no_welcome # Process events to make sure the canvas_window layout has # a chance to activate (the welcome dialog is modal and will # block the event queue, plus we need a chance to receive open file # signals when running without a splash screen) app.processEvents() app.fileOpenRequest.connect(canvas_window.open_scheme_file) if args: log.info("Loading a scheme from the command line argument %r", args[0]) canvas_window.load_scheme(args[0]) elif open_requests: log.info("Loading a scheme from an `QFileOpenEvent` for %r", open_requests[-1]) canvas_window.load_scheme(open_requests[-1].toLocalFile()) else: canvas_window.ask_load_swp_if_exists() if want_welcome: canvas_window.welcome_dialog() # local references prevent destruction update_check = check_for_updates() send_stat = send_usage_statistics() pull_notifs = pull_notifications() # Tee stdout and stderr into Output dock log_view = canvas_window.output_view() stdout = TextStream() stdout.stream.connect(log_view.write) if sys.stdout: stdout.stream.connect(sys.stdout.write) stdout.flushed.connect(sys.stdout.flush) stderr = TextStream() error_writer = log_view.formatted(color=Qt.red) stderr.stream.connect(error_writer.write) if sys.stderr: stderr.stream.connect(sys.stderr.write) stderr.flushed.connect(sys.stderr.flush) log.info("Entering main event loop.") excepthook = ExceptHook(stream=stderr) excepthook.handledException.connect(handle_exception) try: with closing(stdout),\ closing(stderr),\ closing(stream), \ patch('sys.excepthook', excepthook),\ patch('sys.stderr', stderr),\ patch('sys.stdout', stdout): status = app.exec_() except BaseException: log.error("Error in main event loop.", exc_info=True) status = 42 del canvas_window del update_check del send_stat del pull_notifs app.processEvents() app.flush() # Collect any cycles before deleting the QApplication instance gc.collect() del app return status
class Node(QGraphicsNode): """ This class provides an interface for all the bells & whistles of the Network Explorer. """ BRUSH_DEFAULT = QBrush(QColor('#669')) class Pen: DEFAULT = QPen(Qt.black, 0) SELECTED = QPen(QColor('#dd0000'), 3) HIGHLIGHTED = QPen(QColor('#ffaa22'), 3) _TOOLTIP = lambda: '' def __init__(self, id, view): super().__init__(view=view) self.id = id self.setBrush(Node.BRUSH_DEFAULT) self.setPen(Node.Pen.DEFAULT) self._is_highlighted = False self._tooltip = Node._TOOLTIP def setSize(self, size): self._radius = radius = size / 2 self.setRect(-radius, -radius, size, size) def setText(self, text): if text: self.label.setText(text) self.label.setVisible(bool(text)) def setColor(self, color): self.setBrush(QBrush(QColor(color)) if color else Node.BRUSH_DEFAULT) def isHighlighted(self): return self._is_highlighted def setHighlighted(self, highlight): self._is_highlighted = highlight if not self.isSelected(): self.itemChange(self.ItemSelectedChange, False) def itemChange(self, change, value): if change == self.ItemSelectedChange: self.setPen(Node.Pen.SELECTED if value else Node.Pen.HIGHLIGHTED if self._is_highlighted else Node.Pen.DEFAULT) return super().itemChange(change, value) def paint(self, painter, option, widget): option.state &= ~QStyle.State_Selected # We use a custom selection pen super().paint(painter, option, widget) def setTooltip(self, callback): assert not callback or callable(callback) self._tooltip = callback or Node._TOOLTIP def hoverEnterEvent(self, event): self.setToolTip(self._tooltip()) def hoverLeaveEvent(self, event): self.setToolTip('')
class OWMosaicDisplay(OWWidget): name = "Mosaic Display" description = "Display data in a mosaic plot." icon = "icons/MosaicDisplay.svg" priority = 220 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) PEARSON, CLASS_DISTRIBUTION = 0, 1 settingsHandler = DomainContextHandler() use_boxes = Setting(True) interior_coloring = Setting(CLASS_DISTRIBUTION) variable1 = ContextSetting("", exclude_metas=False) variable2 = ContextSetting("", exclude_metas=False) variable3 = ContextSetting("", exclude_metas=False) variable4 = ContextSetting("", exclude_metas=False) variable_color = ContextSetting("", exclude_metas=False) selection = ContextSetting(set()) BAR_WIDTH = 5 SPACING = 4 ATTR_NAME_OFFSET = 20 ATTR_VAL_OFFSET = 3 BLUE_COLORS = [ QColor(255, 255, 255), QColor(210, 210, 255), QColor(110, 110, 255), QColor(0, 0, 255) ] RED_COLORS = [ QColor(255, 255, 255), QColor(255, 200, 200), QColor(255, 100, 100), QColor(255, 0, 0) ] vizrank = SettingProvider(MosaicVizRank) graph_name = "canvas" class Warning(OWWidget.Warning): incompatible_subset = Msg("Data subset is incompatible with Data") no_valid_data = Msg("No valid data") no_cont_selection_sql = \ Msg("Selection of numeric features on SQL is not supported") def __init__(self): super().__init__() self.data = None self.discrete_data = None self.unprocessed_subset_data = None self.subset_data = None self.color_data = None self.areas = [] self.canvas = QGraphicsScene() self.canvas_view = ViewWithPress(self.canvas, handler=self.clear_selection) self.mainArea.layout().addWidget(self.canvas_view) self.canvas_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvas_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvas_view.setRenderHint(QPainter.Antialiasing) box = gui.vBox(self.controlArea, box=True) self.attr_combos = [ gui.comboBox(box, self, value="variable{}".format(i), orientation=Qt.Horizontal, contentsLength=12, callback=self.reset_graph, sendSelectedValue=True, valueType=str, emptyString="(None)") for i in range(1, 5) ] self.vizrank, self.vizrank_button = MosaicVizRank.add_vizrank( box, self, "Find Informative Mosaics", self.set_attr) box2 = gui.vBox(self.controlArea, box="Interior Coloring") dmod = DomainModel self.color_model = DomainModel(order=dmod.MIXED, valid_types=dmod.PRIMITIVE, placeholder="(Pearson residuals)") self.cb_attr_color = gui.comboBox(box2, self, value="variable_color", orientation=Qt.Horizontal, contentsLength=12, labelWidth=50, callback=self.set_color_data, sendSelectedValue=True, model=self.color_model, valueType=str) self.bar_button = gui.checkBox(box2, self, 'use_boxes', label='Compare with total', callback=self._compare_with_total) gui.rubber(self.controlArea) def sizeHint(self): return QSize(720, 530) def _compare_with_total(self): if self.data is not None and \ self.data.domain.class_var is not None and \ self.interior_coloring != self.CLASS_DISTRIBUTION: self.interior_coloring = self.CLASS_DISTRIBUTION self.coloring_changed() # This also calls self.update_graph else: self.update_graph() def _get_discrete_data(self, data): """ Discretizes continuous attributes. Returns None when there is no data, no rows, or no discrete or continuous attributes. """ if (data is None or not len(data) or not any(attr.is_discrete or attr.is_continuous for attr in chain(data.domain, data.domain.metas))): return None elif any(attr.is_continuous for attr in data.domain): return Discretize(method=EqualFreq(n=4), remove_const=False, discretize_classes=True, discretize_metas=True)(data) else: return data def init_combos(self, data): for combo in self.attr_combos: combo.clear() if data is None: self.color_model.set_domain(None) return for combo in self.attr_combos[1:]: combo.addItem("(None)") icons = gui.attributeIconDict for attr in chain(data.domain, data.domain.metas): if attr.is_primitive: for combo in self.attr_combos: combo.addItem(icons[attr], attr.name) if self.attr_combos[0].count() > 0: self.variable1 = self.attr_combos[0].itemText(0) self.variable2 = self.attr_combos[1].itemText( 2 * (self.attr_combos[1].count() > 2)) self.variable3 = self.attr_combos[2].itemText(0) self.variable4 = self.attr_combos[3].itemText(0) if self.data.domain.class_var: self.variable_color = self.data.domain.class_var.name idx = self.cb_attr_color.findText(self.variable_color) else: idx = 0 self.cb_attr_color.setCurrentIndex(idx) def get_attr_list(self): return [ a for a in [self.variable1, self.variable2, self.variable3, self.variable4] if a and a != "(None)" ] def set_attr(self, *attrs): self.variable1, self.variable2, self.variable3, self.variable4 = \ [a.name if a else "" for a in attrs] self.reset_graph() def resizeEvent(self, e): OWWidget.resizeEvent(self, e) self.update_graph() def showEvent(self, ev): OWWidget.showEvent(self, ev) self.update_graph() @Inputs.data def set_data(self, data): if type(data) == SqlTable and data.approx_len() > LARGE_TABLE: data = data.sample_time(DEFAULT_SAMPLE_TIME) self.closeContext() self.data = data self.vizrank.stop_and_reset() self.vizrank_button.setEnabled( self.data is not None and len(self.data) > 1 \ and len(self.data.domain.attributes) >= 1) if self.data is None: self.discrete_data = None self.init_combos(None) return self.color_model.set_domain(self.data.domain) self.init_combos(self.data) self.openContext(self.data) # if we first received subset we now call setSubsetData to process it if self.unprocessed_subset_data: self.set_subset_data(self.unprocessed_subset_data) self.unprocessed_subset_data = None self.set_color_data() @Inputs.data_subset def set_subset_data(self, data): self.Warning.incompatible_subset.clear() if self.data is None: self.unprocessed_subset_data = data return try: self.subset_data = data.transform(self.data.domain) except: self.subset_data = None self.Warning.incompatible_subset(shown=data is not None) # this is called by widget after setData and setSubsetData are called. # this way the graph is updated only once def handleNewSignals(self): self.reset_graph() def clear_selection(self): self.selection = set() self.update_selection_rects() self.send_selection() def coloring_changed(self): self.vizrank.coloring_changed() self.update_graph() def reset_graph(self): self.clear_selection() self.update_graph() def set_color_data(self): if self.data is None or len(self.data) < 2 or len( self.data.domain.attributes) < 1: return if self.cb_attr_color.currentIndex() <= 0: color_var = None self.interior_coloring = self.PEARSON self.bar_button.setEnabled(False) else: color_var = self.data.domain[self.cb_attr_color.currentText()] self.interior_coloring = self.CLASS_DISTRIBUTION self.bar_button.setEnabled(True) attributes = [ v for v in self.data.domain.attributes + self.data.domain.class_vars + self.data.domain.metas if v != color_var and v.is_primitive() ] domain = Domain(attributes, color_var, None) self.color_data = color_data = self.data.from_table(domain, self.data) self.discrete_data = self._get_discrete_data(color_data) self.vizrank.stop_and_reset() self.vizrank_button.setEnabled(True) self.coloring_changed() def update_selection_rects(self): for i, (_, _, area) in enumerate(self.areas): if i in self.selection: area.setPen(QPen(Qt.black, 3, Qt.DotLine)) else: area.setPen(QPen()) def select_area(self, index, ev): if ev.button() != Qt.LeftButton: return if ev.modifiers() & Qt.ControlModifier: self.selection ^= {index} else: self.selection = {index} self.update_selection_rects() self.send_selection() def send_selection(self): if not self.selection or self.data is None: self.Outputs.selected_data.send(None) self.Outputs.annotated_data.send( create_annotated_table(self.data, [])) return filters = [] self.Warning.no_cont_selection_sql.clear() if self.discrete_data is not self.data: if isinstance(self.data, SqlTable): self.Warning.no_cont_selection_sql() for i in self.selection: cols, vals, _ = self.areas[i] filters.append( filter.Values( filter.FilterDiscrete(col, [val]) for col, val in zip(cols, vals))) if len(filters) > 1: filters = filter.Values(filters, conjunction=False) else: filters = filters[0] selection = filters(self.discrete_data) idset = set(selection.ids) sel_idx = [i for i, id in enumerate(self.data.ids) if id in idset] if self.discrete_data is not self.data: selection = self.data[sel_idx] self.Outputs.selected_data.send(selection) self.Outputs.annotated_data.send( create_annotated_table(self.data, sel_idx)) def send_report(self): self.report_plot(self.canvas) def update_graph(self): spacing = self.SPACING bar_width = self.BAR_WIDTH def get_counts(attr_vals, values): """This function calculates rectangles' widths. If all widths are zero then all widths are set to 1.""" if attr_vals == "": counts = [conditionaldict[val] for val in values] else: counts = [ conditionaldict[attr_vals + "-" + val] for val in values ] total = sum(counts) if total == 0: counts = [1] * len(values) total = sum(counts) return total, counts def draw_data(attr_list, x0_x1, y0_y1, side, condition, total_attrs, used_attrs, used_vals, attr_vals=""): x0, x1 = x0_x1 y0, y1 = y0_y1 if conditionaldict[attr_vals] == 0: add_rect(x0, x1, y0, y1, "", used_attrs, used_vals, attr_vals=attr_vals) # store coordinates for later drawing of labels draw_text(side, attr_list[0], (x0, x1), (y0, y1), total_attrs, used_attrs, used_vals, attr_vals) return attr = attr_list[0] # how much smaller rectangles do we draw edge = len(attr_list) * spacing values = get_variable_values_sorted(data.domain[attr]) if side % 2: values = values[::-1] # reverse names if necessary if side % 2 == 0: # we are drawing on the x axis # remove the space needed for separating different attr. values whole = max(0, (x1 - x0) - edge * (len(values) - 1)) if whole == 0: edge = (x1 - x0) / float(len(values) - 1) else: # we are drawing on the y axis whole = max(0, (y1 - y0) - edge * (len(values) - 1)) if whole == 0: edge = (y1 - y0) / float(len(values) - 1) total, counts = get_counts(attr_vals, values) # if we are visualizing the third attribute and the first attribute # has the last value, we have to reverse the order in which the # boxes will be drawn otherwise, if the last cell, nearest to the # labels of the fourth attribute, is empty, we wouldn't be able to # position the labels valrange = list(range(len(values))) if len(attr_list + used_attrs) == 4 and len(used_attrs) == 2: attr1values = get_variable_values_sorted( data.domain[used_attrs[0]]) if used_vals[0] == attr1values[-1]: valrange = valrange[::-1] for i in valrange: start = i * edge + whole * float(sum(counts[:i]) / total) end = i * edge + whole * float(sum(counts[:i + 1]) / total) val = values[i] htmlval = to_html(val) if attr_vals != "": newattrvals = attr_vals + "-" + val else: newattrvals = val tooltip = condition + 4 * " " + attr + \ ": <b>" + htmlval + "</b><br>" attrs = used_attrs + [attr] vals = used_vals + [val] common_args = attrs, vals, newattrvals if side % 2 == 0: # if we are moving horizontally if len(attr_list) == 1: add_rect(x0 + start, x0 + end, y0, y1, tooltip, *common_args) else: draw_data(attr_list[1:], (x0 + start, x0 + end), (y0, y1), side + 1, tooltip, total_attrs, *common_args) else: if len(attr_list) == 1: add_rect(x0, x1, y0 + start, y0 + end, tooltip, *common_args) else: draw_data(attr_list[1:], (x0, x1), (y0 + start, y0 + end), side + 1, tooltip, total_attrs, *common_args) draw_text(side, attr_list[0], (x0, x1), (y0, y1), total_attrs, used_attrs, used_vals, attr_vals) def draw_text(side, attr, x0_x1, y0_y1, total_attrs, used_attrs, used_vals, attr_vals): x0, x1 = x0_x1 y0, y1 = y0_y1 if side in drawn_sides: return # the text on the right will be drawn when we are processing # visualization of the last value of the first attribute if side == 3: attr1values = \ get_variable_values_sorted(data.domain[used_attrs[0]]) if used_vals[0] != attr1values[-1]: return if not conditionaldict[attr_vals]: if side not in draw_positions: draw_positions[side] = (x0, x1, y0, y1) return else: if side in draw_positions: # restore the positions of attribute values and name (x0, x1, y0, y1) = draw_positions[side] drawn_sides.add(side) values = get_variable_values_sorted(data.domain[attr]) if side % 2: values = values[::-1] spaces = spacing * (total_attrs - side) * (len(values) - 1) width = x1 - x0 - spaces * (side % 2 == 0) height = y1 - y0 - spaces * (side % 2 == 1) # calculate position of first attribute currpos = 0 total, counts = get_counts(attr_vals, values) aligns = [ Qt.AlignTop | Qt.AlignHCenter, Qt.AlignRight | Qt.AlignVCenter, Qt.AlignBottom | Qt.AlignHCenter, Qt.AlignLeft | Qt.AlignVCenter ] align = aligns[side] for i, val in enumerate(values): perc = counts[i] / float(total) if distributiondict[val] != 0: if side == 0: CanvasText(self.canvas, str(val), x0 + currpos + width * 0.5 * perc, y1 + self.ATTR_VAL_OFFSET, align) elif side == 1: CanvasText(self.canvas, str(val), x0 - self.ATTR_VAL_OFFSET, y0 + currpos + height * 0.5 * perc, align) elif side == 2: CanvasText(self.canvas, str(val), x0 + currpos + width * perc * 0.5, y0 - self.ATTR_VAL_OFFSET, align) else: CanvasText(self.canvas, str(val), x1 + self.ATTR_VAL_OFFSET, y0 + currpos + height * 0.5 * perc, align) if side % 2 == 0: currpos += perc * width + spacing * (total_attrs - side) else: currpos += perc * height + spacing * (total_attrs - side) if side == 0: CanvasText(self.canvas, attr, x0 + (x1 - x0) / 2, y1 + self.ATTR_VAL_OFFSET + self.ATTR_NAME_OFFSET, align, bold=1) elif side == 1: CanvasText(self.canvas, attr, x0 - max_ylabel_w1 - self.ATTR_VAL_OFFSET, y0 + (y1 - y0) / 2, align, bold=1, vertical=True) elif side == 2: CanvasText(self.canvas, attr, x0 + (x1 - x0) / 2, y0 - self.ATTR_VAL_OFFSET - self.ATTR_NAME_OFFSET, align, bold=1) else: CanvasText(self.canvas, attr, x1 + max_ylabel_w2 + self.ATTR_VAL_OFFSET, y0 + (y1 - y0) / 2, align, bold=1, vertical=True) def add_rect(x0, x1, y0, y1, condition, used_attrs, used_vals, attr_vals=""): area_index = len(self.areas) if x0 == x1: x1 += 1 if y0 == y1: y1 += 1 # rectangles of width and height 1 are not shown - increase if x1 - x0 + y1 - y0 == 2: y1 += 1 if class_var: colors = [QColor(*col) for col in class_var.colors] else: colors = None def select_area(_, ev): self.select_area(area_index, ev) def rect(x, y, w, h, z, pen_color=None, brush_color=None, **args): if pen_color is None: return CanvasRectangle(self.canvas, x, y, w, h, z=z, onclick=select_area, **args) if brush_color is None: brush_color = pen_color return CanvasRectangle(self.canvas, x, y, w, h, pen_color, brush_color, z=z, onclick=select_area, **args) def line(x1, y1, x2, y2): r = QGraphicsLineItem(x1, y1, x2, y2, None) self.canvas.addItem(r) r.setPen(QPen(Qt.white, 2)) r.setZValue(30) outer_rect = rect(x0, y0, x1 - x0, y1 - y0, 30) self.areas.append((used_attrs, used_vals, outer_rect)) if not conditionaldict[attr_vals]: return if self.interior_coloring == self.PEARSON: s = sum(apriori_dists[0]) expected = s * reduce( mul, (apriori_dists[i][used_vals[i]] / float(s) for i in range(len(used_vals)))) actual = conditionaldict[attr_vals] pearson = (actual - expected) / sqrt(expected) if pearson == 0: ind = 0 else: ind = max(0, min(int(log(abs(pearson), 2)), 3)) color = [self.RED_COLORS, self.BLUE_COLORS][pearson > 0][ind] rect(x0, y0, x1 - x0, y1 - y0, -20, color) outer_rect.setToolTip( condition + "<hr/>" + "Expected instances: %.1f<br>" "Actual instances: %d<br>" "Standardized (Pearson) residual: %.1f" % (expected, conditionaldict[attr_vals], pearson)) else: cls_values = get_variable_values_sorted(class_var) prior = get_distribution(data, class_var.name) total = 0 for i, value in enumerate(cls_values): val = conditionaldict[attr_vals + "-" + value] if val == 0: continue if i == len(cls_values) - 1: v = y1 - y0 - total else: v = ((y1 - y0) * val) / conditionaldict[attr_vals] rect(x0, y0 + total, x1 - x0, v, -20, colors[i]) total += v if self.use_boxes and \ abs(x1 - x0) > bar_width and \ abs(y1 - y0) > bar_width: total = 0 line(x0 + bar_width, y0, x0 + bar_width, y1) n = sum(prior) for i, (val, color) in enumerate(zip(prior, colors)): if i == len(prior) - 1: h = y1 - y0 - total else: h = (y1 - y0) * val / n rect(x0, y0 + total, bar_width, h, 20, color) total += h if conditionalsubsetdict: if conditionalsubsetdict[attr_vals]: counts = [ conditionalsubsetdict[attr_vals + "-" + val] for val in cls_values ] if sum(counts) == 1: rect(x0 - 2, y0 - 2, x1 - x0 + 5, y1 - y0 + 5, -550, colors[counts.index(1)], Qt.white, penWidth=2, penStyle=Qt.DashLine) if self.subset_data is not None: line(x1 - bar_width, y0, x1 - bar_width, y1) total = 0 n = conditionalsubsetdict[attr_vals] if n: for i, (cls, color) in \ enumerate(zip(cls_values, colors)): val = conditionalsubsetdict[attr_vals + "-" + cls] if val == 0: continue if i == len(prior) - 1: v = y1 - y0 - total else: v = ((y1 - y0) * val) / n rect(x1 - bar_width, y0 + total, bar_width, v, 15, color) total += v actual = [ conditionaldict[attr_vals + "-" + cls_values[i]] for i in range(len(prior)) ] n_actual = sum(actual) if n_actual > 0: apriori = [prior[key] for key in cls_values] n_apriori = sum(apriori) text = "<br/>".join( "<b>%s</b>: %d / %.1f%% (Expected %.1f / %.1f%%)" % (cls, act, 100.0 * act / n_actual, apr / n_apriori * n_actual, 100.0 * apr / n_apriori) for cls, act, apr in zip(cls_values, actual, apriori)) else: text = "" outer_rect.setToolTip("{}<hr>Instances: {}<br><br>{}".format( condition, n_actual, text[:-4])) def draw_legend(x0_x1, y0_y1): x0, x1 = x0_x1 _, y1 = y0_y1 if self.interior_coloring == self.PEARSON: names = [ "<-8", "-8:-4", "-4:-2", "-2:2", "2:4", "4:8", ">8", "Residuals:" ] colors = self.RED_COLORS[::-1] + self.BLUE_COLORS[1:] else: names = get_variable_values_sorted(class_var) + \ [class_var.name + ":"] colors = [QColor(*col) for col in class_var.colors] names = [ CanvasText(self.canvas, name, alignment=Qt.AlignVCenter) for name in names ] totalwidth = sum(text.boundingRect().width() for text in names) # compute the x position of the center of the legend y = y1 + self.ATTR_NAME_OFFSET + self.ATTR_VAL_OFFSET + 35 distance = 30 startx = (x0 + x1) / 2 - (totalwidth + (len(names)) * distance) / 2 names[-1].setPos(startx + 15, y) names[-1].show() xoffset = names[-1].boundingRect().width() + distance size = 8 for i in range(len(names) - 1): if self.interior_coloring == self.PEARSON: edgecolor = Qt.black else: edgecolor = colors[i] CanvasRectangle(self.canvas, startx + xoffset, y - size / 2, size, size, edgecolor, colors[i]) names[i].setPos(startx + xoffset + 10, y) xoffset += distance + names[i].boundingRect().width() self.canvas.clear() self.areas = [] data = self.discrete_data if data is None: return subset = self.subset_data attr_list = self.get_attr_list() class_var = data.domain.class_var if class_var: sql = type(data) == SqlTable name = not sql and data.name # save class_var because it is removed in the next line data = data[:, attr_list + [class_var]] data.domain.class_var = class_var if not sql: data.name = name else: data = data[:, attr_list] # TODO: check this # data = Preprocessor_dropMissing(data) if len(data) == 0: self.Warning.no_valid_data() return else: self.Warning.no_valid_data.clear() attrs = [attr for attr in attr_list if not data.domain[attr].values] if attrs: CanvasText(self.canvas, "Feature {} has no values".format(attrs[0]), (self.canvas_view.width() - 120) / 2, self.canvas_view.height() / 2) return if self.interior_coloring == self.PEARSON: apriori_dists = [ get_distribution(data, attr) for attr in attr_list ] else: apriori_dists = [] def get_max_label_width(attr): values = get_variable_values_sorted(data.domain[attr]) maxw = 0 for val in values: t = CanvasText(self.canvas, val, 0, 0, bold=0, show=False) maxw = max(int(t.boundingRect().width()), maxw) return maxw # get the maximum width of rectangle xoff = 20 width = 20 if len(attr_list) > 1: text = CanvasText(self.canvas, attr_list[1], bold=1, show=0) max_ylabel_w1 = min(get_max_label_width(attr_list[1]), 150) width = 5 + text.boundingRect().height() + \ self.ATTR_VAL_OFFSET + max_ylabel_w1 xoff = width if len(attr_list) == 4: text = CanvasText(self.canvas, attr_list[3], bold=1, show=0) max_ylabel_w2 = min(get_max_label_width(attr_list[3]), 150) width += text.boundingRect().height() + \ self.ATTR_VAL_OFFSET + max_ylabel_w2 - 10 # get the maximum height of rectangle height = 100 yoff = 45 square_size = min(self.canvas_view.width() - width - 20, self.canvas_view.height() - height - 20) if square_size < 0: return # canvas is too small to draw rectangles self.canvas_view.setSceneRect(0, 0, self.canvas_view.width(), self.canvas_view.height()) drawn_sides = set() draw_positions = {} conditionaldict, distributiondict = \ get_conditional_distribution(data, attr_list) conditionalsubsetdict = None if subset: conditionalsubsetdict, _ = \ get_conditional_distribution(subset, attr_list) # draw rectangles draw_data(attr_list, (xoff, xoff + square_size), (yoff, yoff + square_size), 0, "", len(attr_list), [], []) draw_legend((xoff, xoff + square_size), (yoff, yoff + square_size)) self.update_selection_rects()
def hoverLeaveEvent(self, event): super().hoverLeaveEvent(event) self.setBrush(QBrush(QColor(200, 200, 200))) self.update()
def __getitem__(self, val): return QColor(*self.getRGB(val))
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 __init__(self): super().__init__() self._inputs = OrderedDict() self.__pending_selected_rows = self.selected_rows self.selected_rows = None self.__pending_selected_cols = self.selected_cols self.selected_cols = None self.dist_color = QColor(*self.dist_color_RGB) info_box = gui.vBox(self.controlArea, "Info") self.info_ex = gui.widgetLabel( info_box, 'No data on input.', ) self.info_ex.setWordWrap(True) self.info_attr = gui.widgetLabel(info_box, ' ') self.info_attr.setWordWrap(True) self.info_class = gui.widgetLabel(info_box, ' ') self.info_class.setWordWrap(True) self.info_meta = gui.widgetLabel(info_box, ' ') self.info_meta.setWordWrap(True) info_box.setMinimumWidth(200) gui.separator(self.controlArea) box = gui.vBox(self.controlArea, "Variables") self.c_show_attribute_labels = gui.checkBox( box, self, "show_attribute_labels", "Show variable labels (if present)", callback=self._on_show_variable_labels_changed) gui.checkBox(box, self, "show_distributions", 'Visualize numeric values', callback=self._on_distribution_color_changed) gui.checkBox(box, self, "color_by_class", 'Color by instance classes', callback=self._on_distribution_color_changed) box = gui.vBox(self.controlArea, "Selection") gui.checkBox(box, self, "select_rows", "Select full rows", callback=self._on_select_rows_changed) gui.rubber(self.controlArea) reset = gui.button(None, self, "Restore Original Order", callback=self.restore_order, tooltip="Show rows in the original order", autoDefault=False) self.buttonsArea.layout().insertWidget(0, reset) gui.auto_send(self.buttonsArea, self, "auto_commit") # GUI with tabs self.tabs = gui.tabWidget(self.mainArea) self.tabs.currentChanged.connect(self._on_current_tab_changed)
def __init__(self): super().__init__() self._inputs = OrderedDict() self.__pending_selected_rows = self.selected_rows self.selected_rows = None self.__pending_selected_cols = self.selected_cols self.selected_cols = None self.dist_color = QColor(*self.dist_color_RGB) info_box = gui.vBox(self.controlArea, "信息") self.info_ex = gui.widgetLabel( info_box, '没有输入数据。', ) self.info_ex.setWordWrap(True) self.info_attr = gui.widgetLabel(info_box, ' ') self.info_attr.setWordWrap(True) self.info_class = gui.widgetLabel(info_box, ' ') self.info_class.setWordWrap(True) self.info_meta = gui.widgetLabel(info_box, ' ') self.info_meta.setWordWrap(True) info_box.setMinimumWidth(200) gui.separator(self.controlArea) box = gui.vBox(self.controlArea, "变量") self.c_show_attribute_labels = gui.checkBox( box, self, "show_attribute_labels", "显示变量标签(如果存在)", callback=self._on_show_variable_labels_changed) gui.checkBox(box, self, "show_distributions", '可视化数值', callback=self._on_distribution_color_changed) gui.checkBox(box, self, "color_by_class", '按实例类着色', callback=self._on_distribution_color_changed) box = gui.vBox(self.controlArea, "选择") gui.checkBox(box, self, "select_rows", "选择整行", callback=self._on_select_rows_changed) gui.rubber(self.controlArea) reset = gui.button(None, self, "恢复原始顺序", callback=self.restore_order, tooltip="按原始顺序显示行", autoDefault=False) self.buttonsArea.layout().insertWidget(0, reset) gui.auto_commit(self.buttonsArea, self, "auto_commit", "发送所选行", "自动发送") # GUI with tabs self.tabs = gui.tabWidget(self.mainArea) self.tabs.currentChanged.connect(self._on_current_tab_changed)
def draw_statistics(self): """Draw lines that represent standard deviation or quartiles""" return # TODO: Implement using BasicStats if self.show_statistics and self.data is not None: data = [] domain = self.data.domain for attr_idx in self.attribute_indices: if not self.domain[attr_idx].is_continuous: data.append([()]) continue # only for continuous attributes if not domain.class_var or domain.has_continuous_class: if self.show_statistics == MEANS: m = self.domain_data_stat[attr_idx].mean dev = self.domain_data_stat[attr_idx].var data.append([(m - dev, m, m + dev)]) elif self.show_statistics == MEDIAN: data.append([(0, 0, 0)]); continue sorted_array = np.sort(attr_values) if len(sorted_array) > 0: data.append([(sorted_array[int(len(sorted_array) / 4.0)], sorted_array[int(len(sorted_array) / 2.0)], sorted_array[int(len(sorted_array) * 0.75)])]) else: data.append([(0, 0, 0)]) else: curr = [] class_values = get_variable_values_sorted(self.domain.class_var) class_index = self.domain.index(self.domain.class_var) for c in range(len(class_values)): attr_values = self.data[attr_idx, self.data[class_index] == c] attr_values = attr_values[~np.isnan(attr_values)] if len(attr_values) == 0: curr.append((0, 0, 0)) continue if self.show_statistics == MEANS: m = attr_values.mean() dev = attr_values.std() curr.append((m - dev, m, m + dev)) elif self.show_statistics == MEDIAN: sorted_array = np.sort(attr_values) curr.append((sorted_array[int(len(attr_values) / 4.0)], sorted_array[int(len(attr_values) / 2.0)], sorted_array[int(len(attr_values) * 0.75)])) data.append(curr) # draw vertical lines for i in range(len(data)): for c in range(len(data[i])): if data[i][c] == (): continue x = i - 0.03 * (len(data[i]) - 1) / 2.0 + c * 0.03 col = QColor(self.discrete_palette[c]) col.setAlpha(self.alpha_value_2) self.add_curve("", col, col, 3, OWCurve.Lines, OWPoint.NoSymbol, xData=[x, x, x], yData=[data[i][c][0], data[i][c][1], data[i][c][2]], lineWidth=4) self.add_curve("", col, col, 1, OWCurve.Lines, OWPoint.NoSymbol, xData=[x - 0.03, x + 0.03], yData=[data[i][c][0], data[i][c][0]], lineWidth=4) self.add_curve("", col, col, 1, OWCurve.Lines, OWPoint.NoSymbol, xData=[x - 0.03, x + 0.03], yData=[data[i][c][1], data[i][c][1]], lineWidth=4) self.add_curve("", col, col, 1, OWCurve.Lines, OWPoint.NoSymbol, xData=[x - 0.03, x + 0.03], yData=[data[i][c][2], data[i][c][2]], lineWidth=4) # draw lines with mean/median values if not domain.class_var or domain.has_continuous_class: class_count = 1 else: class_count = len(self.domain.class_var.values) for c in range(class_count): diff = - 0.03 * (class_count - 1) / 2.0 + c * 0.03 ys = [] xs = [] for i in range(len(data)): if data[i] != [()]: ys.append(data[i][c][1]) xs.append(i + diff) else: if len(xs) > 1: col = QColor(self.discrete_palette[c]) col.setAlpha(self.alpha_value_2) self.add_curve("", col, col, 1, OWCurve.Lines, OWPoint.NoSymbol, xData=xs, yData=ys, lineWidth=4) xs = [] ys = [] col = QColor(self.discrete_palette[c]) col.setAlpha(self.alpha_value_2) self.add_curve("", col, col, 1, OWCurve.Lines, OWPoint.NoSymbol, xData=xs, yData=ys, lineWidth=4)
class Pen: DEFAULT = QPen(Qt.black, 0) SELECTED = QPen(QColor('#dd0000'), 3) HIGHLIGHTED = QPen(QColor('#ffaa22'), 3)
def __init__(self, color=QColor(Qt.white), size=12): p = QPixmap(size, size) p.fill(color) self.color = color QIcon.__init__(self, p)
def _setup_plot(self): """Setup the plot with new curve data.""" assert self.data is not None data, domain = self.data, self.data.domain if is_discrete(domain.class_var): class_col_data, _ = data.get_column_view(domain.class_var) group_indices = [np.flatnonzero(class_col_data == i) for i in range(len(domain.class_var.values))] else: group_indices = [np.arange(len(data))] X = np.arange(1, len(domain.attributes)+1) groups = [] for i, indices in enumerate(group_indices): if self.classes: color = self.class_colors[i] else: color = QColor(Qt.darkGray) group_data = data[indices, :] plot_x, plot_y, connect = disconnected_curve_data(group_data.X, x=X) color.setAlpha(200) lightcolor = QColor(color.lighter(factor=150)) lightcolor.setAlpha(150) pen = QPen(color, 2) pen.setCosmetic(True) lightpen = QPen(lightcolor, 1) lightpen.setCosmetic(True) hoverpen = QPen(pen) hoverpen.setWidth(2) curve = pg.PlotCurveItem( x=plot_x, y=plot_y, connect=connect, pen=lightpen, symbolSize=2, antialias=True, ) self.graph.addItem(curve) hovercurves = [] for index, profile in zip(indices, group_data.X): hcurve = HoverCurve(x=X, y=profile, pen=hoverpen, antialias=True) hcurve.setToolTip('{}'.format(index)) hcurve._data_index = index hovercurves.append(hcurve) self.graph.addItem(hcurve) mean = np.nanmean(group_data.X, axis=0) meancurve = pg.PlotDataItem( x=X, y=mean, pen=pen, size=5, symbol="o", pxMode=True, symbolSize=5, antialias=True ) hoverpen = QPen(hoverpen) hoverpen.setWidth(5) hc = HoverCurve(x=X, y=mean, pen=hoverpen, antialias=True) hc.setFlag(QGraphicsItem.ItemIsSelectable, False) self.graph.addItem(hc) self.graph.addItem(meancurve) self.legend_items.append(meancurve) q1, q2, q3 = np.nanpercentile(group_data.X, [25, 50, 75], axis=0) # TODO: implement and use a box plot item errorbar = pg.ErrorBarItem( x=X, y=mean, bottom=np.clip(mean - q1, 0, mean - q1), top=np.clip(q3 - mean, 0, q3 - mean), beam=0.5 ) self.graph.addItem(errorbar) groups.append( namespace( data=group_data, indices=indices, profiles=curve, hovercurves=hovercurves, mean=meancurve, boxplot=errorbar) ) self.__groups = groups self.__update_visibility() self.__update_tooltips()
def rgbToQColor(rgb): return QColor(rgb & 0xFFFFFFFF)
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"] 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.info.set_output_summary(self.info.NoOutput) 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(self.list_remote) 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 = self.list_local() 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) self.view.resizeColumnToContents(0) self.view.setColumnWidth( 1, min(self.view.sizeHintForColumn(1), self.view.fontMetrics().width("X" * 37))) 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 = self.list_local() 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) self.info.set_output_summary(self.info.NoOutput) else: data = self.load_data(path) self.Outputs.data.send(data) self.info.set_output_summary(len(data), format_summary_details(data)) self.current_output = path self.__update_cached_state() @staticmethod def load_data(path): return Orange.data.Table(path) def list_remote(self): # type: () -> Dict[Tuple[str, ...], dict] client = ServerFiles(server=self.INDEX_URL) return client.allinfo() def list_local(self): # type: () -> Dict[Tuple[str, ...], dict] return LocalFiles(self.local_cache_path).allinfo()
def show_splash_message(self, message: str, color=QColor()): """Display splash screen message""" splash = self.splash_screen() if splash is not None: splash.show() splash.showMessage(message, color=color)
def setColor(self, color): self.setBrush(QBrush(QColor(color)) if color else Node.BRUSH_DEFAULT)
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()
def _create_legend(self, anchor, brush=QBrush(QColor(232, 232, 232, 200))): # by default the legend transparency was to high for colorful maps legend = LegendItem(brush=brush) legend.setParentItem(self.plot_widget.getViewBox()) legend.restoreAnchor(anchor) return legend
from orangecontrib.imageanalytics.image_grid import ImageGrid 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) data_subset = Input("Data Subset", Orange.data.Table)
def add_rect(x0, x1, y0, y1, condition, used_attrs, used_vals, attr_vals=""): area_index = len(self.areas) if x0 == x1: x1 += 1 if y0 == y1: y1 += 1 # rectangles of width and height 1 are not shown - increase if x1 - x0 + y1 - y0 == 2: y1 += 1 if class_var: colors = [QColor(*col) for col in class_var.colors] else: colors = None def select_area(_, ev): self.select_area(area_index, ev) def rect(x, y, w, h, z, pen_color=None, brush_color=None, **args): if pen_color is None: return CanvasRectangle(self.canvas, x, y, w, h, z=z, onclick=select_area, **args) if brush_color is None: brush_color = pen_color return CanvasRectangle(self.canvas, x, y, w, h, pen_color, brush_color, z=z, onclick=select_area, **args) def line(x1, y1, x2, y2): r = QGraphicsLineItem(x1, y1, x2, y2, None) self.canvas.addItem(r) r.setPen(QPen(Qt.white, 2)) r.setZValue(30) outer_rect = rect(x0, y0, x1 - x0, y1 - y0, 30) self.areas.append((used_attrs, used_vals, outer_rect)) if not conditionaldict[attr_vals]: return if self.interior_coloring == self.PEARSON: s = sum(apriori_dists[0]) expected = s * reduce( mul, (apriori_dists[i][used_vals[i]] / float(s) for i in range(len(used_vals)))) actual = conditionaldict[attr_vals] pearson = (actual - expected) / sqrt(expected) if pearson == 0: ind = 0 else: ind = max(0, min(int(log(abs(pearson), 2)), 3)) color = [self.RED_COLORS, self.BLUE_COLORS][pearson > 0][ind] rect(x0, y0, x1 - x0, y1 - y0, -20, color) outer_rect.setToolTip( condition + "<hr/>" + "Expected instances: %.1f<br>" "Actual instances: %d<br>" "Standardized (Pearson) residual: %.1f" % (expected, conditionaldict[attr_vals], pearson)) else: cls_values = get_variable_values_sorted(class_var) prior = get_distribution(data, class_var.name) total = 0 for i, value in enumerate(cls_values): val = conditionaldict[attr_vals + "-" + value] if val == 0: continue if i == len(cls_values) - 1: v = y1 - y0 - total else: v = ((y1 - y0) * val) / conditionaldict[attr_vals] rect(x0, y0 + total, x1 - x0, v, -20, colors[i]) total += v if self.use_boxes and \ abs(x1 - x0) > bar_width and \ abs(y1 - y0) > bar_width: total = 0 line(x0 + bar_width, y0, x0 + bar_width, y1) n = sum(prior) for i, (val, color) in enumerate(zip(prior, colors)): if i == len(prior) - 1: h = y1 - y0 - total else: h = (y1 - y0) * val / n rect(x0, y0 + total, bar_width, h, 20, color) total += h if conditionalsubsetdict: if conditionalsubsetdict[attr_vals]: counts = [ conditionalsubsetdict[attr_vals + "-" + val] for val in cls_values ] if sum(counts) == 1: rect(x0 - 2, y0 - 2, x1 - x0 + 5, y1 - y0 + 5, -550, colors[counts.index(1)], Qt.white, penWidth=2, penStyle=Qt.DashLine) if self.subset_data is not None: line(x1 - bar_width, y0, x1 - bar_width, y1) total = 0 n = conditionalsubsetdict[attr_vals] if n: for i, (cls, color) in \ enumerate(zip(cls_values, colors)): val = conditionalsubsetdict[attr_vals + "-" + cls] if val == 0: continue if i == len(prior) - 1: v = y1 - y0 - total else: v = ((y1 - y0) * val) / n rect(x1 - bar_width, y0 + total, bar_width, v, 15, color) total += v actual = [ conditionaldict[attr_vals + "-" + cls_values[i]] for i in range(len(prior)) ] n_actual = sum(actual) if n_actual > 0: apriori = [prior[key] for key in cls_values] n_apriori = sum(apriori) text = "<br/>".join( "<b>%s</b>: %d / %.1f%% (Expected %.1f / %.1f%%)" % (cls, act, 100.0 * act / n_actual, apr / n_apriori * n_actual, 100.0 * apr / n_apriori) for cls, act, apr in zip(cls_values, actual, apriori)) else: text = "" outer_rect.setToolTip("{}<hr>Instances: {}<br><br>{}".format( condition, n_actual, text[:-4]))
def color(self): """ Return the arrow brush color. """ return QColor(self.__color)
def setColor(self, color): self.setPen(QPen(QColor(color or Qt.gray), self.pen().width()))