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, DISPERSION, MIN, MAX, MISSING = range(8) @property def name(self): return {self.ICON: '', self.NAME: 'Name', self.DISTRIBUTION: 'Distribution', self.CENTER: 'Center', 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 self.__distributions_cache = {} # 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.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.__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=lambda x: np.sqrt(ut.nanvar(x, axis=0)) / ut.nanmean(x, axis=0), ) 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), ) 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] self._center = self.__compute_stat( matrices, discrete_f=lambda x: __mode(x, axis=0), continuous_f=lambda x: ut.nanmean(x, axis=0), time_f=lambda x: ut.nanmean(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 if variables is not None and len(variables): indices = [self.domain.index(var) for var in variables] else: indices = ... matrix = np.vstack(( self._center[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.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 len(matrices): return np.array([]) def _to_float(data): if not np.issubdtype(data.dtype, np.number): data = data.astype(np.float64) return data def _to_object(data): if data.dtype is not np.object: data = data.astype(np.object) return data 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 disc_idx, cont_idx, time_idx, str_idx = self._attr_indices(variables) if discrete_f: x_ = x[:, disc_idx] if x_.size: result[disc_idx] = discrete_f(_to_float(x_)) if continuous_f: x_ = x[:, cont_idx] if x_.size: result[cont_idx] = continuous_f(_to_float(x_)) if time_f: x_ = x[:, time_idx] if x_.size: result[time_idx] = time_f(_to_float(x_)) if string_f: x_ = x[:, str_idx] if x_.size: result[str_idx] = string_f(_to_object(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 = [DiscreteVariable, ContinuousVariable, TimeVariable, 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 var_name_indices = 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, 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: indices = np.argsort(data, kind='mergesort') if order == Qt.DescendingOrder: indices = indices[::-1] # Always sort NaNs last if np.issubdtype(data.dtype, np.number): indices = np.roll(indices, -np.isnan(data).sum()) 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 # Text formatting for various data simply requires a lot of branches. # This is much better than overengineering various formatters... # pylint: disable=too-many-branches if not index.isValid(): 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 QVariant() attribute = self.variables[row] if role == Qt.BackgroundRole: if attribute in self.domain.attributes: return self.COLOR_FOR_ROLE[self.ATTRIBUTE] elif attribute in self.domain.metas: return self.COLOR_FOR_ROLE[self.META] elif attribute in self.domain.class_vars: return self.COLOR_FOR_ROLE[self.CLASS_VAR] elif role == Qt.TextAlignmentRole: if column == self.Columns.NAME: return Qt.AlignLeft | Qt.AlignVCenter return Qt.AlignRight | Qt.AlignVCenter output = None if column == self.Columns.ICON: if role == Qt.DecorationRole: return gui.attributeIconDict[attribute] elif column == self.Columns.NAME: if role == Qt.DisplayRole: output = attribute.name elif column == self.Columns.DISTRIBUTION: if role == Qt.DisplayRole: 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: if role == Qt.DisplayRole: if isinstance(attribute, DiscreteVariable): output = self._center[row] if not np.isnan(output): output = attribute.str_val(self._center[row]) elif isinstance(attribute, TimeVariable): output = attribute.str_val(self._center[row]) else: output = self._center[row] elif column == self.Columns.DISPERSION: if role == Qt.DisplayRole: if isinstance(attribute, TimeVariable): output = format_time_diff(self._min[row], self._max[row]) else: output = self._dispersion[row] elif column == self.Columns.MIN: if role == Qt.DisplayRole: if isinstance(attribute, DiscreteVariable): if attribute.ordered: output = attribute.str_val(self._min[row]) elif isinstance(attribute, TimeVariable): output = attribute.str_val(self._min[row]) else: output = self._min[row] elif column == self.Columns.MAX: if role == Qt.DisplayRole: if isinstance(attribute, DiscreteVariable): if attribute.ordered: output = attribute.str_val(self._max[row]) elif isinstance(attribute, TimeVariable): output = attribute.str_val(self._max[row]) else: output = self._max[row] elif column == self.Columns.MISSING: if role == Qt.DisplayRole: output = '%d (%d%%)' % ( self._missing[row], 100 * self._missing[row] / self.n_instances ) # Consistently format the text inside the table cells # The easiest way to check for NaN is to compare with itself if output != output: # pylint: disable=comparison-with-itself output = '' # Format ∞ properly elif output in (np.inf, -np.inf): output = '%s∞' % ['', '-'][output < 0] elif isinstance(output, int): output = locale.format('%d', output, grouping=True) elif isinstance(output, float): output = locale.format('%.2f', output, grouping=True) return output 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)
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 Pen: DEFAULT = QPen(Qt.black, 0) SELECTED = QPen(QColor('#dd0000'), 3) HIGHLIGHTED = QPen(QColor('#ffaa22'), 3)
class Style: DEFAULT = QPen(Qt.black, 2), QBrush(Qt.white) SELECTED = QPen(QColor('#880000'), 2), QBrush(QColor('#ffcc33'))
class Color: # = pen color, font brush DEFAULT = Qt.gray, QBrush(Qt.black) SELECTED = QColor('#dd4455'), QBrush(QColor('#770000'))
def __paintEventNoStyle(self): p = QPainter(self) opt = QStyleOptionToolButton() self.initStyleOption(opt) fm = QFontMetrics(opt.font) palette = opt.palette # highlight brush is used as the background for the icon and background # when the tab is expanded and as mouse hover color (lighter). brush_highlight = palette.highlight() foregroundrole = QPalette.ButtonText if opt.state & QStyle.State_Sunken: # State 'down' pressed during a mouse press (slightly darker). background_brush = brush_darker(brush_highlight, 110) foregroundrole = QPalette.HighlightedText elif opt.state & QStyle.State_MouseOver: background_brush = brush_darker(brush_highlight, 95) foregroundrole = QPalette.HighlightedText elif opt.state & QStyle.State_On: background_brush = brush_highlight foregroundrole = QPalette.HighlightedText else: # The default button brush. background_brush = palette.button() rect = opt.rect icon_area_rect = QRect(rect) icon_area_rect.setWidth(int(icon_area_rect.height() * 1.26)) text_rect = QRect(rect) text_rect.setLeft(icon_area_rect.x() + icon_area_rect.width() + 10) # Background # TODO: Should the tab button have native toolbutton shape, drawn # using PE_PanelButtonTool or even QToolBox tab shape # Default outline pen pen = QPen(palette.color(QPalette.Mid)) p.save() p.setPen(Qt.NoPen) p.setBrush(QBrush(background_brush)) p.drawRect(rect) # Draw the background behind the icon if the background_brush # is different. if not opt.state & QStyle.State_On: p.setBrush(brush_highlight) p.drawRect(icon_area_rect) # Line between the icon and text p.setPen(pen) p.drawLine(icon_area_rect.x() + icon_area_rect.width(), icon_area_rect.y(), icon_area_rect.x() + icon_area_rect.width(), icon_area_rect.y() + icon_area_rect.height()) if opt.state & QStyle.State_HasFocus: # Set the focus frame pen and draw the border pen = QPen(QColor(brush_highlight)) p.setPen(pen) p.setBrush(Qt.NoBrush) # Adjust for pen rect = rect.adjusted(0, 0, -1, -1) p.drawRect(rect) else: p.setPen(pen) # Draw the top/bottom border if self.position == QStyleOptionToolBox.OnlyOneTab or \ self.position == QStyleOptionToolBox.Beginning or \ self.selected & QStyleOptionToolBox.PreviousIsSelected: p.drawLine(rect.x(), rect.y(), rect.x() + rect.width(), rect.y()) p.drawLine(rect.x(), rect.y() + rect.height(), rect.x() + rect.width(), rect.y() + rect.height()) p.restore() p.save() text = fm.elidedText(opt.text, Qt.ElideRight, text_rect.width()) p.setPen(QPen(palette.color(foregroundrole))) p.setFont(opt.font) p.drawText( text_rect, int(Qt.AlignVCenter | Qt.AlignLeft) | int(Qt.TextSingleLine), text) if not opt.icon.isNull(): if opt.state & QStyle.State_Enabled: mode = QIcon.Normal else: mode = QIcon.Disabled if opt.state & QStyle.State_On: state = QIcon.On else: state = QIcon.Off icon_area_rect = icon_area_rect icon_rect = QRect(QPoint(0, 0), opt.iconSize) icon_rect.moveCenter(icon_area_rect.center()) opt.icon.paint(p, icon_rect, Qt.AlignCenter, mode, state) p.restore()
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.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) else: data = self.load_data(path) self.Outputs.data.send(data) self.current_output = path self.__update_cached_state() @staticmethod def load_data(path): return Orange.data.Table(path) def 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 __init__(self): super().__init__() # Instance variables self.tree_type = self.GENERAL self.model = None self.instances = None self.clf_dataset = None # The tree adapter instance which is passed from the outside self.tree_adapter = None self.legend = None self.color_palette = None # Different methods to calculate the size of squares self.SIZE_CALCULATION = [ ('Normal', lambda x: x), ('Square root', lambda x: sqrt(x)), # The +1 is there so that we don't get division by 0 exceptions ('Logarithmic', lambda x: log(x * self.size_log_scale + 1)), ] # Color modes for regression trees self.REGRESSION_COLOR_CALC = [ ('None', lambda _, __: QColor(255, 255, 255)), ('Class mean', self._color_class_mean), ('Standard deviation', self._color_stddev), ] # CONTROL AREA # Tree info area box_info = gui.widgetBox(self.controlArea, 'Tree Info') self.info = gui.widgetLabel(box_info) # Display settings area box_display = gui.widgetBox(self.controlArea, 'Display Settings') self.depth_slider = gui.hSlider(box_display, self, 'depth_limit', label='Depth', ticks=False, callback=self.update_depth) self.target_class_combo = gui.comboBox(box_display, self, 'target_class_index', label='Target class', orientation=Qt.Horizontal, items=[], contentsLength=8, callback=self.update_colors) self.size_calc_combo = gui.comboBox( box_display, self, 'size_calc_idx', label='Size', orientation=Qt.Horizontal, items=list(zip(*self.SIZE_CALCULATION))[0], contentsLength=8, callback=self.update_size_calc) self.log_scale_box = gui.hSlider(box_display, self, 'size_log_scale', label='Log scale factor', minValue=1, maxValue=100, ticks=False, callback=self.invalidate_tree) # Plot properties area box_plot = gui.widgetBox(self.controlArea, 'Plot Properties') self.cb_show_tooltips = gui.checkBox( box_plot, self, 'tooltips_enabled', label='Enable tooltips', callback=self.update_tooltip_enabled) self.cb_show_legend = gui.checkBox(box_plot, self, 'show_legend', label='Show legend', callback=self.update_show_legend) # Stretch to fit the rest of the unsused area gui.rubber(self.controlArea) self.controlArea.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) # MAIN AREA # The QGraphicsScene doesn't actually require a parent, but not linking # the widget to the scene causes errors and a segfault on close due to # the way Qt deallocates memory and deletes objects. self.scene = TreeGraphicsScene(self) self.scene.selectionChanged.connect(self.commit) self.view = TreeGraphicsView(self.scene, padding=(150, 150)) self.view.setRenderHint(QPainter.Antialiasing, True) self.mainArea.layout().addWidget(self.view) self.ptree = PythagorasTreeViewer() self.scene.addItem(self.ptree) self.view.set_central_widget(self.ptree) self.resize(800, 500) # Clear the widget to correctly set the intial values self.clear()
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_commit(self.buttonsArea, self, "auto_commit", "Send Selected Rows", "Send Automatically") # GUI with tabs self.tabs = gui.tabWidget(self.mainArea) self.tabs.currentChanged.connect(self._on_current_tab_changed)
class Normal(BaseCommandMode): color = QColor('#33cc33') name = 'normal' def _processChar(self): ev = yield None # Get action count typedCount = 0 if ev.key() != _0: char = ev.text() while char.isdigit(): digit = int(char) typedCount = (typedCount * 10) + digit ev = yield char = ev.text() effectiveCount = typedCount or 1 # Now get the action action = code(ev) if action in self._SIMPLE_COMMANDS: cmdFunc = self._SIMPLE_COMMANDS[action] cmdFunc(self, action, effectiveCount) return True elif action == _g: ev = yield if code(ev) == _g: self._moveCursor('gg', 1) return True elif action in (_f, _F, _t, _T): ev = yield if not isChar(ev): return True searchChar = ev.text() self._moveCursor(action, effectiveCount, searchChar=searchChar, select=False) return True elif action == _Period: # repeat command if self._vim.lastEditCmdFunc is not None: if typedCount: self._vim.lastEditCmdFunc(typedCount) else: self._vim.lastEditCmdFunc() return True elif action in self._MOTIONS: self._moveCursor(action, typedCount, select=False) return True elif action in self._COMPOSITE_COMMANDS: moveCount = 0 ev = yield if ev.key() != _0: # 0 is a command, not a count char = ev.text() while char.isdigit(): digit = int(char) moveCount = (moveCount * 10) + digit ev = yield char = ev.text() if moveCount == 0: moveCount = 1 count = effectiveCount * moveCount # Get motion for a composite command motion = code(ev) searchChar = None if motion == _g: ev = yield if code(ev) == _g: motion = 'gg' else: return True elif motion in (_f, _F, _t, _T): ev = yield if not isChar(ev): return True searchChar = ev.text() if (action != _z and motion in self._MOTIONS) or \ (action, motion) in ((_d, _d), (_y, _y), (_Less, _Less), (_Greater, _Greater), (_Equal, _Equal), (_z, _z)): cmdFunc = self._COMPOSITE_COMMANDS[action] cmdFunc(self, action, motion, searchChar, count) return True elif isChar(ev): return True # ignore unknown character else: return False # but do not ignore not-a-character keys assert 0 # must StopIteration on if def _repeat(self, count, func): """ Repeat action 1 or more times. If more than one - do it as 1 undoble action """ if count != 1: with self._qpart: for _ in range(count): func() else: func() def _saveLastEditSimpleCmd(self, cmd, count): def doCmd(count=count): self._SIMPLE_COMMANDS[cmd](self, cmd, count) self._vim.lastEditCmdFunc = doCmd def _saveLastEditCompositeCmd(self, cmd, motion, searchChar, count): def doCmd(count=count): self._COMPOSITE_COMMANDS[cmd](self, cmd, motion, searchChar, count) self._vim.lastEditCmdFunc = doCmd # # Simple commands # def cmdInsertMode(self, cmd, count): self.switchMode(Insert) def cmdInsertAtLineStartMode(self, cmd, count): cursor = self._qpart.textCursor() text = cursor.block().text() spaceLen = len(text) - len(text.lstrip()) cursor.setPosition(cursor.block().position() + spaceLen) self._qpart.setTextCursor(cursor) self.switchMode(Insert) def cmdJoinLines(self, cmd, count): cursor = self._qpart.textCursor() if not cursor.block().next().isValid(): # last block return with self._qpart: for _ in range(count): cursor.movePosition(QTextCursor.EndOfBlock) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) self.moveToFirstNonSpace(cursor, QTextCursor.KeepAnchor) nonEmptyBlock = cursor.block().length() > 1 cursor.removeSelectedText() if nonEmptyBlock: cursor.insertText(' ') if not cursor.block().next().isValid(): # last block break self._qpart.setTextCursor(cursor) def cmdReplaceMode(self, cmd, count): self.switchMode(Replace) self._qpart.setOverwriteMode(True) def cmdReplaceCharMode(self, cmd, count): self.switchMode(ReplaceChar) self._qpart.setOverwriteMode(True) def cmdAppendAfterLine(self, cmd, count): cursor = self._qpart.textCursor() cursor.movePosition(QTextCursor.EndOfBlock) self._qpart.setTextCursor(cursor) self.switchMode(Insert) def cmdAppendAfterChar(self, cmd, count): cursor = self._qpart.textCursor() cursor.movePosition(QTextCursor.Right) self._qpart.setTextCursor(cursor) self.switchMode(Insert) def cmdUndo(self, cmd, count): for _ in range(count): self._qpart.undo() def cmdRedo(self, cmd, count): for _ in range(count): self._qpart.redo() def cmdNewLineBelow(self, cmd, count): cursor = self._qpart.textCursor() cursor.movePosition(QTextCursor.EndOfBlock) self._qpart.setTextCursor(cursor) self._repeat(count, self._qpart._insertNewBlock) self._saveLastEditSimpleCmd(cmd, count) self.switchMode(Insert) def cmdNewLineAbove(self, cmd, count): cursor = self._qpart.textCursor() def insert(): cursor.movePosition(QTextCursor.StartOfBlock) self._qpart.setTextCursor(cursor) self._qpart._insertNewBlock() cursor.movePosition(QTextCursor.Up) self._qpart._indenter.autoIndentBlock(cursor.block()) self._repeat(count, insert) self._qpart.setTextCursor(cursor) self._saveLastEditSimpleCmd(cmd, count) self.switchMode(Insert) def cmdInternalPaste(self, cmd, count): if not _globalClipboard.value: return if isinstance(_globalClipboard.value, str): cursor = self._qpart.textCursor() if cmd == _p: cursor.movePosition(QTextCursor.Right) self._qpart.setTextCursor(cursor) self._repeat(count, lambda: cursor.insertText(_globalClipboard.value)) cursor.movePosition(QTextCursor.Left) self._qpart.setTextCursor(cursor) elif isinstance(_globalClipboard.value, list): index = self._qpart.cursorPosition[0] if cmd == _p: index += 1 self._repeat( count, lambda: self._qpart.lines.insert( index, '\n'.join(_globalClipboard.value))) self._saveLastEditSimpleCmd(cmd, count) def cmdSubstitute(self, cmd, count): """ s """ cursor = self._qpart.textCursor() for _ in range(count): cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) if cursor.selectedText(): _globalClipboard.value = cursor.selectedText() cursor.removeSelectedText() self._saveLastEditSimpleCmd(cmd, count) self.switchMode(Insert) def cmdSubstituteLines(self, cmd, count): """ S """ lineIndex = self._qpart.cursorPosition[0] availableCount = len(self._qpart.lines) - lineIndex effectiveCount = min(availableCount, count) _globalClipboard.value = self._qpart.lines[lineIndex:lineIndex + effectiveCount] with self._qpart: del self._qpart.lines[lineIndex:lineIndex + effectiveCount] self._qpart.lines.insert(lineIndex, '') self._qpart.cursorPosition = (lineIndex, 0) self._qpart._indenter.autoIndentBlock( self._qpart.textCursor().block()) self._saveLastEditSimpleCmd(cmd, count) self.switchMode(Insert) def cmdVisualMode(self, cmd, count): cursor = self._qpart.textCursor() cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) self._qpart.setTextCursor(cursor) self.switchMode(Visual) def cmdVisualLinesMode(self, cmd, count): self.switchMode(VisualLines) def cmdDelete(self, cmd, count): """ x """ cursor = self._qpart.textCursor() direction = QTextCursor.Left if cmd == _X else QTextCursor.Right for _ in range(count): cursor.movePosition(direction, QTextCursor.KeepAnchor) if cursor.selectedText(): _globalClipboard.value = cursor.selectedText() cursor.removeSelectedText() self._saveLastEditSimpleCmd(cmd, count) def cmdDeleteUntilEndOfBlock(self, cmd, count): """ C and D """ cursor = self._qpart.textCursor() for _ in range(count - 1): cursor.movePosition(QTextCursor.Down, QTextCursor.KeepAnchor) cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) _globalClipboard.value = cursor.selectedText() cursor.removeSelectedText() if cmd == _C: self.switchMode(Insert) self._saveLastEditSimpleCmd(cmd, count) def cmdYankUntilEndOfLine(self, cmd, count): oldCursor = self._qpart.textCursor() cursor = self._qpart.textCursor() cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) _globalClipboard.value = cursor.selectedText() self._qpart.setTextCursor(cursor) self._qpart.copy() self._qpart.setTextCursor(oldCursor) _SIMPLE_COMMANDS = { _A: cmdAppendAfterLine, _a: cmdAppendAfterChar, _C: cmdDeleteUntilEndOfBlock, _D: cmdDeleteUntilEndOfBlock, _i: cmdInsertMode, _I: cmdInsertAtLineStartMode, _J: cmdJoinLines, _r: cmdReplaceCharMode, _R: cmdReplaceMode, _v: cmdVisualMode, _V: cmdVisualLinesMode, _o: cmdNewLineBelow, _O: cmdNewLineAbove, _p: cmdInternalPaste, _P: cmdInternalPaste, _s: cmdSubstitute, _S: cmdSubstituteLines, _u: cmdUndo, _U: cmdRedo, _x: cmdDelete, _X: cmdDelete, _Y: cmdYankUntilEndOfLine, } # # Composite commands # def cmdCompositeDelete(self, cmd, motion, searchChar, count): if motion in (_j, _Down): lineIndex = self._qpart.cursorPosition[0] availableCount = len(self._qpart.lines) - lineIndex if availableCount < 2: # last line return effectiveCount = min(availableCount, count) _globalClipboard.value = self._qpart.lines[lineIndex:lineIndex + effectiveCount + 1] del self._qpart.lines[lineIndex:lineIndex + effectiveCount + 1] elif motion in (_k, _Up): lineIndex = self._qpart.cursorPosition[0] if lineIndex == 0: # first line return effectiveCount = min(lineIndex, count) _globalClipboard.value = self._qpart.lines[ lineIndex - effectiveCount:lineIndex + 1] del self._qpart.lines[lineIndex - effectiveCount:lineIndex + 1] elif motion == _d: # delete whole line lineIndex = self._qpart.cursorPosition[0] availableCount = len(self._qpart.lines) - lineIndex effectiveCount = min(availableCount, count) _globalClipboard.value = self._qpart.lines[lineIndex:lineIndex + effectiveCount] del self._qpart.lines[lineIndex:lineIndex + effectiveCount] elif motion == _G: currentLineIndex = self._qpart.cursorPosition[0] _globalClipboard.value = self._qpart.lines[currentLineIndex:] del self._qpart.lines[currentLineIndex:] elif motion == 'gg': currentLineIndex = self._qpart.cursorPosition[0] _globalClipboard.value = self._qpart.lines[:currentLineIndex + 1] del self._qpart.lines[:currentLineIndex + 1] else: self._moveCursor(motion, count, select=True, searchChar=searchChar) selText = self._qpart.textCursor().selectedText() if selText: _globalClipboard.value = selText self._qpart.textCursor().removeSelectedText() self._saveLastEditCompositeCmd(cmd, motion, searchChar, count) def cmdCompositeChange(self, cmd, motion, searchChar, count): # TODO deletion and next insertion should be undo-ble as 1 action self.cmdCompositeDelete(cmd, motion, searchChar, count) self.switchMode(Insert) def cmdCompositeYank(self, cmd, motion, searchChar, count): oldCursor = self._qpart.textCursor() if motion == _y: cursor = self._qpart.textCursor() cursor.movePosition(QTextCursor.StartOfBlock) for _ in range(count - 1): cursor.movePosition(QTextCursor.Down, QTextCursor.KeepAnchor) cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) self._qpart.setTextCursor(cursor) _globalClipboard.value = [self._qpart.selectedText] else: self._moveCursor(motion, count, select=True, searchChar=searchChar) _globalClipboard.value = self._qpart.selectedText self._qpart.copy() self._qpart.setTextCursor(oldCursor) def cmdCompositeUnIndent(self, cmd, motion, searchChar, count): if motion == _Less: pass # current line is already selected else: self._moveCursor(motion, count, select=True, searchChar=searchChar) self._expandSelection() self._qpart._indenter.onChangeSelectedBlocksIndent(increase=False, withSpace=False) self._resetSelection(moveToTop=True) self._saveLastEditCompositeCmd(cmd, motion, searchChar, count) def cmdCompositeIndent(self, cmd, motion, searchChar, count): if motion == _Greater: pass # current line is already selected else: self._moveCursor(motion, count, select=True, searchChar=searchChar) self._expandSelection() self._qpart._indenter.onChangeSelectedBlocksIndent(increase=True, withSpace=False) self._resetSelection(moveToTop=True) self._saveLastEditCompositeCmd(cmd, motion, searchChar, count) def cmdCompositeAutoIndent(self, cmd, motion, searchChar, count): if motion == _Equal: pass # current line is already selected else: self._moveCursor(motion, count, select=True, searchChar=searchChar) self._expandSelection() self._qpart._indenter.onAutoIndentTriggered() self._resetSelection(moveToTop=True) self._saveLastEditCompositeCmd(cmd, motion, searchChar, count) def cmdCompositeScrollView(self, cmd, motion, searchChar, count): if motion == _z: self._qpart.centerCursor() _COMPOSITE_COMMANDS = { _c: cmdCompositeChange, _d: cmdCompositeDelete, _y: cmdCompositeYank, _Less: cmdCompositeUnIndent, _Greater: cmdCompositeIndent, _Equal: cmdCompositeAutoIndent, _z: cmdCompositeScrollView, }
def _classification_get_color_palette(self): return [QColor(*c) for c in self.model.domain.class_var.colors]
class BaseVisual(BaseCommandMode): color = QColor('#6699ff') _selectLines = NotImplementedError() def _processChar(self): ev = yield None # Get count typedCount = 0 if ev.key() != _0: char = ev.text() while char.isdigit(): digit = int(char) typedCount = (typedCount * 10) + digit ev = yield char = ev.text() count = typedCount if typedCount else 1 # Now get the action action = code(ev) if action in self._SIMPLE_COMMANDS: cmdFunc = self._SIMPLE_COMMANDS[action] for _ in range(count): cmdFunc(self, action) if action not in (_v, _V): # if not switched to another visual mode self._resetSelection(moveToTop=True) if self._vim.mode( ) is self: # if the command didn't switch the mode self.switchMode(Normal) return True elif action == _Esc: self._resetSelection() self.switchMode(Normal) return True elif action == _g: ev = yield if code(ev) == _g: self._moveCursor('gg', 1, select=True) if self._selectLines: self._expandSelection() return True elif action in (_f, _F, _t, _T): ev = yield if not isChar(ev): return True searchChar = ev.text() self._moveCursor(action, typedCount, searchChar=searchChar, select=True) return True elif action == _z: ev = yield if code(ev) == _z: self._qpart.centerCursor() return True elif action in self._MOTIONS: if self._selectLines and action in (_k, _Up, _j, _Down): # There is a bug in visual mode: # If a line is wrapped, cursor moves up, but stays on same line. # Then selection is expanded and cursor returns to previous position. # So user can't move the cursor up. So, in Visual mode we move cursor up until it # moved to previous line. The same bug when moving down cursorLine = self._qpart.cursorPosition[0] if (action in (_k, _Up) and cursorLine > 0) or \ (action in (_j, _Down) and (cursorLine + 1) < len(self._qpart.lines)): while self._qpart.cursorPosition[0] == cursorLine: self._moveCursor(action, typedCount, select=True) else: self._moveCursor(action, typedCount, select=True) if self._selectLines: self._expandSelection() return True elif action == _r: ev = yield newChar = ev.text() if newChar: newChars = [newChar if char != '\n' else '\n' \ for char in self._qpart.selectedText ] newText = ''.join(newChars) self._qpart.selectedText = newText self.switchMode(Normal) return True elif isChar(ev): return True # ignore unknown character else: return False # but do not ignore not-a-character keys assert 0 # must StopIteration on if def _selectedLinesRange(self): """ Selected lines range for line manipulation methods """ (startLine, _), (endLine, _) = self._qpart.selectedPosition start = min(startLine, endLine) end = max(startLine, endLine) return start, end def _selectRangeForRepeat(self, repeatLineCount): start = self._qpart.cursorPosition[0] self._qpart.selectedPosition = ((start, 0), (start + repeatLineCount - 1, 0)) cursor = self._qpart.textCursor() # expand until the end of line cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) self._qpart.setTextCursor(cursor) def _saveLastEditLinesCmd(self, cmd, lineCount): self._vim.lastEditCmdFunc = lambda: self._SIMPLE_COMMANDS[cmd]( self, cmd, lineCount) # # Simple commands # def cmdDelete(self, cmd, repeatLineCount=None): if repeatLineCount is not None: self._selectRangeForRepeat(repeatLineCount) cursor = self._qpart.textCursor() if cursor.selectedText(): if self._selectLines: start, end = self._selectedLinesRange() self._saveLastEditLinesCmd(cmd, end - start + 1) _globalClipboard.value = self._qpart.lines[start:end + 1] del self._qpart.lines[start:end + 1] else: _globalClipboard.value = cursor.selectedText() cursor.removeSelectedText() def cmdDeleteLines(self, cmd, repeatLineCount=None): if repeatLineCount is not None: self._selectRangeForRepeat(repeatLineCount) start, end = self._selectedLinesRange() self._saveLastEditLinesCmd(cmd, end - start + 1) _globalClipboard.value = self._qpart.lines[start:end + 1] del self._qpart.lines[start:end + 1] def cmdInsertMode(self, cmd): self.switchMode(Insert) def cmdJoinLines(self, cmd, repeatLineCount=None): if repeatLineCount is not None: self._selectRangeForRepeat(repeatLineCount) start, end = self._selectedLinesRange() count = end - start if not count: # nothing to join return self._saveLastEditLinesCmd(cmd, end - start + 1) cursor = QTextCursor(self._qpart.document().findBlockByNumber(start)) with self._qpart: for _ in range(count): cursor.movePosition(QTextCursor.EndOfBlock) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) self.moveToFirstNonSpace(cursor, QTextCursor.KeepAnchor) nonEmptyBlock = cursor.block().length() > 1 cursor.removeSelectedText() if nonEmptyBlock: cursor.insertText(' ') self._qpart.setTextCursor(cursor) def cmdAppendAfterChar(self, cmd): cursor = self._qpart.textCursor() cursor.clearSelection() cursor.movePosition(QTextCursor.Right) self._qpart.setTextCursor(cursor) self.switchMode(Insert) def cmdReplaceSelectedLines(self, cmd): start, end = self._selectedLinesRange() _globalClipboard.value = self._qpart.lines[start:end + 1] lastLineLen = len(self._qpart.lines[end]) self._qpart.selectedPosition = ((start, 0), (end, lastLineLen)) self._qpart.selectedText = '' self.switchMode(Insert) def cmdResetSelection(self, cmd): self._qpart.cursorPosition = self._qpart.selectedPosition[0] def cmdInternalPaste(self, cmd): if not _globalClipboard.value: return with self._qpart: cursor = self._qpart.textCursor() if self._selectLines: start, end = self._selectedLinesRange() del self._qpart.lines[start:end + 1] else: cursor.removeSelectedText() if isinstance(_globalClipboard.value, str): self._qpart.textCursor().insertText(_globalClipboard.value) elif isinstance(_globalClipboard.value, list): currentLineIndex = self._qpart.cursorPosition[0] text = '\n'.join(_globalClipboard.value) index = currentLineIndex if self._selectLines else currentLineIndex + 1 self._qpart.lines.insert(index, text) def cmdVisualMode(self, cmd): if not self._selectLines: self._resetSelection() return # already in visual mode self.switchMode(Visual) def cmdVisualLinesMode(self, cmd): if self._selectLines: self._resetSelection() return # already in visual lines mode self.switchMode(VisualLines) def cmdYank(self, cmd): if self._selectLines: start, end = self._selectedLinesRange() _globalClipboard.value = self._qpart.lines[start:end + 1] else: _globalClipboard.value = self._qpart.selectedText self._qpart.copy() def cmdChange(self, cmd): cursor = self._qpart.textCursor() if cursor.selectedText(): if self._selectLines: _globalClipboard.value = cursor.selectedText().splitlines() else: _globalClipboard.value = cursor.selectedText() cursor.removeSelectedText() self.switchMode(Insert) def cmdUnIndent(self, cmd, repeatLineCount=None): if repeatLineCount is not None: self._selectRangeForRepeat(repeatLineCount) else: start, end = self._selectedLinesRange() self._saveLastEditLinesCmd(cmd, end - start + 1) self._qpart._indenter.onChangeSelectedBlocksIndent(increase=False, withSpace=False) if repeatLineCount: self._resetSelection(moveToTop=True) def cmdIndent(self, cmd, repeatLineCount=None): if repeatLineCount is not None: self._selectRangeForRepeat(repeatLineCount) else: start, end = self._selectedLinesRange() self._saveLastEditLinesCmd(cmd, end - start + 1) self._qpart._indenter.onChangeSelectedBlocksIndent(increase=True, withSpace=False) if repeatLineCount: self._resetSelection(moveToTop=True) def cmdAutoIndent(self, cmd, repeatLineCount=None): if repeatLineCount is not None: self._selectRangeForRepeat(repeatLineCount) else: start, end = self._selectedLinesRange() self._saveLastEditLinesCmd(cmd, end - start + 1) self._qpart._indenter.onAutoIndentTriggered() if repeatLineCount: self._resetSelection(moveToTop=True) _SIMPLE_COMMANDS = { _A: cmdAppendAfterChar, _c: cmdChange, _C: cmdReplaceSelectedLines, _d: cmdDelete, _D: cmdDeleteLines, _i: cmdInsertMode, _J: cmdJoinLines, _R: cmdReplaceSelectedLines, _p: cmdInternalPaste, _u: cmdResetSelection, _x: cmdDelete, _s: cmdChange, _S: cmdReplaceSelectedLines, _v: cmdVisualMode, _V: cmdVisualLinesMode, _X: cmdDeleteLines, _y: cmdYank, _Less: cmdUnIndent, _Greater: cmdIndent, _Equal: cmdAutoIndent, }
asciiCode = ord(text) if asciiCode <= 31 or asciiCode == 0x7f: # control characters return False if text == ' ' and ev.modifiers() == Qt.ShiftModifier: return False # Shift+Space is a shortcut, not a text return True NORMAL = 'normal' INSERT = 'insert' REPLACE_CHAR = 'replace character' MODE_COLORS = { NORMAL: QColor('#33cc33'), INSERT: QColor('#ff9900'), REPLACE_CHAR: QColor('#ff3300') } class _GlobalClipboard: def __init__(self): self.value = '' _globalClipboard = _GlobalClipboard() class Vim(QObject): """Vim mode implementation.
def main(argv=None): 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("--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) parser.add_option("--config", help="Configuration namespace", type="str", default="orangecanvas.example") # -m canvas orange.widgets # -m canvas --config orange.widgets (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() # Set http_proxy environment variable(s) for some clients fix_set_proxy_env() # Try to fix macOS automatic window tabbing (Sierra and later) fix_macos_nswindow_tabbing() # File handler should always be at least INFO level so we need # the application root level to be at least at INFO. root_level = min(levels[options.log_level], logging.INFO) rootlogger = logging.getLogger(__package__) rootlogger.setLevel(root_level) # Standard output stream handler at the requested level stream_hander = logging.StreamHandler() stream_hander.setLevel(level=levels[options.log_level]) rootlogger.addHandler(stream_hander) if options.config is not None: try: cfg = utils.name_lookup(options.config) except (ImportError, AttributeError): pass else: config.set_default(cfg()) log.info("activating %s", options.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) 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" # NOTE: config.init() must be called after the QApplication constructor config.init() file_handler = logging.FileHandler( filename=os.path.join(config.log_dir(), "canvas.log"), mode="w" ) file_handler.setLevel(root_level) rootlogger.addHandler(file_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() stylesheet = options.stylesheet or defaultstylesheet stylesheet_string = None if stylesheet != "none": if os.path.isfile(stylesheet): with io.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 = __package__ resource = "styles/" + stylesheet if pkg_resources.resource_exists(pkg_name, resource): stylesheet_string = \ pkg_resources.resource_string(pkg_name, resource).decode("utf-8") 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(__file__)) QDir.addSearchPath("canvas_icons", os.path.join(dirpath, "icons")) canvas_window = CanvasMainWindow() canvas_window.setAttribute(Qt.WA_DeleteOnClose) canvas_window.setWindowIcon(config.application_icon()) 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.setAttribute(Qt.WA_DeleteOnClose) 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) show_splash = splash_screen.show close_splash = splash_screen.close else: show_splash = close_splash = lambda: None 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: show_splash() widget_discovery.run(config.widgets_entry_points()) close_splash() # 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 want_welcome and not args and not open_requests: canvas_window.welcome_dialog() elif 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()) # Tee stdout and stderr into Output dock output_view = canvas_window.output_view() stdout = TextStream() stdout.stream.connect(output_view.write) if sys.stdout: stdout.stream.connect(sys.stdout.write) stdout.flushed.connect(sys.stdout.flush) stderr = TextStream() error_writer = output_view.formated(color=Qt.red) stderr.stream.connect(error_writer.write) if sys.stderr: stderr.stream.connect(sys.stderr.write) stderr.flushed.connect(sys.stderr.flush) with ExitStack() as stack: stack.enter_context(closing(stderr)) stack.enter_context(closing(stdout)) stack.enter_context(redirect_stdout(stdout)) stack.enter_context(redirect_stderr(stderr)) log.info("Entering main event loop.") sys.excepthook = ExceptHook(stream=stderr) try: status = app.exec_() finally: sys.excepthook = sys.__excepthook__ del canvas_window app.processEvents() app.flush() # Collect any cycles before deleting the QApplication instance gc.collect() del app if status == 96: log.info('Restarting via exit code 96.') run_after_exit([sys.executable, sys.argv[0]]) return status
def hoverLeaveEvent(self, event): super().hoverLeaveEvent(event) self.setBrush(QBrush(QColor(200, 200, 200))) self.update()
def show_splash_message(self, message: str, color=QColor("#FFFFFF")): super().show_splash_message(message, color)
def advance(): clock = time.clock() * 10 item.setLineWidth(5 + math.sin(clock) * 5) item.setColor(QColor(Qt.red).lighter(100 + 30 * math.cos(clock))) self.singleShot(0, advance)
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) 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) 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: self.stats = [ BoxData(cont, attr, i, self.group_var) for i, cont in enumerate(self.conts) if np.sum(cont) > 0 ] 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() def clear_scene(self): self.closeContext() self.box_scene.clearSelection() self.box_scene.clear() 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): 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.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] else: self.boxes = [self.strudel(self.dist)] for row, box in enumerate(self.boxes): y = (-len(self.boxes) + row) * 40 + 10 bars, labels = box[::2], box[1::2] self.__draw_group_labels(y, row) if not self.stretched: self.__draw_row_counts(y, row) 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 """ 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 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( self.attribute.repr_val(val) 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 """ 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() 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)
def __setup(self): # Setup the subwidgets/groups/layout smax = max( (np.nanmax(g.scores) for g in self.__groups if g.scores.size), default=1) smax = 1 if np.isnan(smax) else smax smin = min( (np.nanmin(g.scores) for g in self.__groups if g.scores.size), default=-1) smin = -1 if np.isnan(smin) else smin smin = min(smin, 0) font = self.font() font.setPixelSize(self.__barHeight) axispen = QPen(Qt.black) ax = pg.AxisItem(parent=self, orientation="top", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.layout().addItem(ax, 0, 2) for i, group in enumerate(self.__groups): silhouettegroup = BarPlotItem(parent=self) silhouettegroup.setBrush(QBrush(QColor(*group.color))) silhouettegroup.setPen(self.__pen) silhouettegroup.setDataRange(smin, smax) silhouettegroup.setPlotData(group.scores) silhouettegroup.setPreferredBarSize(self.__barHeight) silhouettegroup.setData(0, group.indices) self.layout().addItem(silhouettegroup, i + 1, 2) if group.label: self.layout().addItem(Line(orientation=Qt.Vertical), i + 1, 1) label = QGraphicsSimpleTextItem(self) label.setText("{} ({})".format(escape(group.label), len(group.scores))) item = WrapperLayoutItem(label, Qt.Vertical, parent=self) self.layout().addItem(item, i + 1, 0, Qt.AlignCenter) textlist = TextListWidget(self, font=font) sp = textlist.sizePolicy() sp.setVerticalPolicy(QSizePolicy.Ignored) textlist.setSizePolicy(sp) textlist.setParent(self) if group.rownames is not None: textlist.setItems(group.items) textlist.setVisible(self.__rowNamesVisible) else: textlist.setVisible(False) self.layout().addItem(textlist, i + 1, 3) ax = pg.AxisItem(parent=self, orientation="bottom", maxTickLength=7, pen=axispen) ax.setRange(smin, smax) self.layout().addItem(ax, len(self.__groups) + 1, 2)
def _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)) 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 TableModel(QAbstractTableModel): """ 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"]) 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_meta") 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") self.__sortColumn = -1 self.__sortOrder = Qt.AscendingOrder # Indices sorting the source table self.__sortInd = None # The inverse of __sortInd self.__sortIndInv = None def sort(self, column, order): """ Sort the data by `column` index into `order` To reset the sort order pass -1 as the column. :type column: int :type order: Qt.SortOrder Reimplemented from QAbstractItemModel.sort .. note:: This only affects the model's data presentation, the underlying data table is left unmodified. """ self.layoutAboutToBeChanged.emit() # Store persistent indices as well as their (actual) rows in the # source data table. persistent = self.persistentIndexList() persistent_rows = numpy.array([ind.row() for ind in persistent], int) if self.__sortInd is not None: persistent_rows = self.__sortInd[persistent_rows] self.__sortColumn = column self.__sortOrder = order if column < 0: indices = None else: keydata = self.columnSortKeyData(column, TableModel.ValueRole) if keydata is not None: if keydata.dtype == object: indices = sorted(range(self.__rowCount), key=lambda i: str(keydata[i])) indices = numpy.array(indices) else: indices = numpy.argsort(keydata, kind="mergesort") else: indices = numpy.arange(0, self.__rowCount) if order == Qt.DescendingOrder: indices = indices[::-1] if self.__sortInd is not None: indices = self.__sortInd[indices] if indices is not None: self.__sortInd = indices self.__sortIndInv = numpy.argsort(indices) else: self.__sortInd = None self.__sortIndInv = None if self.__sortInd is not None: persistent_rows = self.__sortIndInv[persistent_rows] for pind, row in zip(persistent, persistent_rows): self.changePersistentIndex(pind, self.index(row, pind.column())) self.layoutChanged.emit() def columnSortKeyData(self, column, role): """ Return a sequence of 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_view, _ = self.source.get_column_view(coldesc.var) col_data = numpy.asarray(col_view) if coldesc.var.is_continuous: # continuous from metas have dtype object; cast it to float col_data = col_data.astype(float) if self.__sortInd is not None: col_data = col_data[self.__sortInd] return col_data else: if self.__sortInd is not None: indices = self.__sortInd else: indices = range(self.rowCount()) return numpy.asarray( [self.index(i, column).data(role) for i in indices]) def sortColumn(self): """ The column currently used for sorting (-1 if no sorting is applied). """ return self.__sortColumn def sortOrder(self): """ The current sort order. """ return self.__sortOrder def mapToTableRows(self, modelrows): """ Return the row indices in the source table for the given model rows. """ if self.__sortColumn < 0: return modelrows else: return self.__sortInd[modelrows].tolist() def mapFromTableRows(self, tablerows): """ Return the row indices in the model for the given source table rows. """ if self.__sortColumn < 0: return tablerows else: return self.__sortIndInv[tablerows].tolist() 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=set([ 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 if self.__sortInd is not None: row = self.__sortInd[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 return self.color_for_role[coldesc.role] 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, col = self.__sortIndInv[index.row()], index.column() if role == Qt.EditRole: try: self.source[row, col] = 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: if self.__sortInd is not None: return int(self.__sortInd[section] + 1) else: return int(section + 1) else: 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]
def hoverEnterEvent(self, event): self.setBrush(QBrush(QColor(0, 100, 0, 100)))
def selected(self, value): for item in self.childItems(): item.setBrush(QColor('red' if value else 'pink'))
def set_color(self, color): self.path.setBrush(QColor(color))
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if DefaultDiscModel.icon is None: DefaultDiscModel.icon = gui.createAttributePixmap( "★", QColor(0, 0, 0, 0), Qt.black) self.hint: VarHint = DefaultHint
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())
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('')
def hoverEnterEvent(self, event): super().hoverEnterEvent(event) self.setBrush(QBrush(QColor(100, 100, 100))) self.update()
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()