def __init__(self, pixmap, title="", parentItem=None, **kwargs): super().__init__(parentItem, **kwargs) self.setFocusPolicy(Qt.StrongFocus) self._title = None self._size = QSizeF() layout = QGraphicsLinearLayout(Qt.Vertical, self) layout.setSpacing(2) layout.setContentsMargins(5, 5, 5, 5) self.setContentsMargins(0, 0, 0, 0) self.pixmapWidget = GraphicsPixmapWidget(pixmap, self) self.labelWidget = GraphicsTextWidget(title, self) layout.addItem(self.pixmapWidget) layout.addItem(self.labelWidget) layout.addStretch() layout.setAlignment(self.pixmapWidget, Qt.AlignCenter) layout.setAlignment(self.labelWidget, Qt.AlignHCenter | Qt.AlignBottom) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setTitle(title) self.setTitleWidth(100)
def __init__(self, pixmap, parentItem=None, crop=False, in_subset=True, **kwargs): super().__init__(parentItem, **kwargs) self.setFocusPolicy(Qt.StrongFocus) self._size = QSizeF() layout = QGraphicsLinearLayout(Qt.Vertical, self) layout.setSpacing(1) layout.setContentsMargins(5, 5, 5, 5) self.setContentsMargins(0, 0, 0, 0) self.pixmapWidget = GraphicsPixmapWidget(pixmap, self) self.pixmapWidget.setCrop(crop) self.pixmapWidget.setSubset(in_subset) self.selectionBrush = DEFAULT_SELECTION_BRUSH self.selectionPen = DEFAULT_SELECTION_PEN layout.addItem(self.pixmapWidget) layout.setAlignment(self.pixmapWidget, Qt.AlignCenter) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
def __init__(self, pixmap, parentItem=None, crop=False, in_subset=True, add_label=False, text="", **kwargs): super().__init__(parentItem, **kwargs) self.setFocusPolicy(Qt.StrongFocus) self._size = QSizeF() layout = QGraphicsLinearLayout(Qt.Vertical, self) layout.setSpacing(1) layout.setContentsMargins(5, 5, 5, 5) self.setContentsMargins(0, 0, 0, 0) self.pixmapWidget = GraphicsPixmapWidget(pixmap, self) self.pixmapWidget.setCrop(crop) self.pixmapWidget.setSubset(in_subset) self.selectionBrush = DEFAULT_SELECTION_BRUSH self.selectionPen = DEFAULT_SELECTION_PEN layout.addItem(self.pixmapWidget) self.label = None if add_label: l1 = ElidedLabel(text) l1.setStyleSheet("background-color: rgba(255, 255, 255, 10);") l1.setAlignment(Qt.AlignCenter) l1.setFixedHeight(16) self.label = l1 gs = QGraphicsScene() w = gs.addWidget(l1) layout.addItem(w) layout.setAlignment(self.pixmapWidget, Qt.AlignCenter) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
class StripePlot(QGraphicsWidget): HEIGHT = 400 SPACING = 20 HMARGIN = 30 VMARGIN = 20 def __init__(self): super().__init__() self.__height = None # type: int self.__range = None # type: Tuple[float, float] self.__layout = QGraphicsLinearLayout() self.__layout.setOrientation(Qt.Horizontal) self.__layout.setSpacing(self.SPACING) self.__layout.setContentsMargins(self.HMARGIN, self.VMARGIN, self.HMARGIN, self.VMARGIN) self.setLayout(self.__layout) self.__stripe_item = StripeItem(self) self.__left_axis = AxisItem([ self.__stripe_item.model_output_ind, self.__stripe_item.base_value_ind ], parent=self, orientation="left", maxTickLength=7, pen=QPen(Qt.black)) self.__layout.addItem(self.__left_axis) self.__layout.addItem(self.__stripe_item) @property def height(self) -> float: return self.HEIGHT + 10 * self.__height def set_data(self, data: PlotData, height: float): diff = (data.value_range[1] - data.value_range[0]) * 0.01 self.__range = (data.value_range[0] - diff, data.value_range[1] + diff) self.__left_axis.setRange(*self.__range) self.__height = height self.__stripe_item.set_data(data, self.__range, self.height) def set_height(self, height: float): self.__height = height self.__stripe_item.set_height(self.height) self.updateGeometry() def sizeHint(self, *_) -> QSizeF: return QSizeF( self.__left_axis.boundingRect().width() + self.__stripe_item.total_width + self.HMARGIN * 2, self.height + self.VMARGIN * 2)
class Legend(Anchorable): """Base legend class. This class provides common attributes for any legend subclasses: - Behaviour on `QGraphicsScene` - Appearance of legend Parameters ---------- parent : QGraphicsItem, optional orientation : Qt.Orientation, optional The default orientation is vertical domain : Orange.data.domain.Domain, optional This field is left optional as in some cases, we may want to simply pass in a list that represents the legend. items : Iterable[QColor, str] bg_color : QColor, optional font : QFont, optional color_indicator_cls : ColorIndicator The color indicator class that will be used to render the indicators. See Also -------- OWDiscreteLegend OWContinuousLegend OWContinuousLegend Notes ----- .. warning:: If the domain parameter is supplied, the items parameter will be ignored. """ def __init__(self, parent=None, orientation=Qt.Vertical, domain=None, items=None, bg_color=QColor(232, 232, 232, 196), font=None, color_indicator_cls=LegendItemSquare, **kwargs): super().__init__(parent, **kwargs) self._layout = None self.orientation = orientation self.bg_color = QBrush(bg_color) self.color_indicator_cls = color_indicator_cls # Set default font if none is given if font is None: self.font = QFont() self.font.setPointSize(10) else: self.font = font self.setFlags(QGraphicsWidget.ItemIsMovable | QGraphicsItem.ItemIgnoresTransformations) self._setup_layout() if domain is not None: self.set_domain(domain) elif items is not None: self.set_items(items) def _clear_layout(self): self._layout = None for child in self.children(): child.setParent(None) def _setup_layout(self): self._clear_layout() self._layout = QGraphicsLinearLayout(self.orientation) self._layout.setContentsMargins(10, 5, 10, 5) # If horizontal, there needs to be horizontal space between the items if self.orientation == Qt.Horizontal: self._layout.setSpacing(10) # If vertical spacing, vertical space is provided by child layouts else: self._layout.setSpacing(0) self.setLayout(self._layout) def set_domain(self, domain): """Handle receiving the domain object. Parameters ---------- domain : Orange.data.domain.Domain Returns ------- Raises ------ AttributeError If the domain does not contain the correct type of class variable. """ raise NotImplementedError() def set_items(self, values): """Handle receiving an array of items. Parameters ---------- values : iterable[object, QColor] Returns ------- """ raise NotImplementedError() @staticmethod def _convert_to_color(obj): if isinstance(obj, QColor): return obj elif isinstance(obj, tuple) or isinstance(obj, list) \ or isinstance(obj, np.ndarray): assert len(obj) in (3, 4) return QColor(*obj) else: return QColor(obj) def setVisible(self, is_visible): """Only display the legend if it contains any items.""" return super().setVisible(is_visible and len(self._layout) > 0) def paint(self, painter, options, widget=None): painter.save() pen = QPen(QColor(196, 197, 193, 200), 1) brush = QBrush(QColor(self.bg_color)) painter.setPen(pen) painter.setBrush(brush) painter.drawRect(self.contentsRect()) painter.restore()
class Histogram(QGraphicsWidget): """A basic histogram widget. Parameters ---------- data : Table variable : Union[int, str, Variable] parent : QObject height : Union[int, float] width : Union[int, float] side_padding : Union[int, float] Specify the padding between the edges of the histogram and the first and last bars. top_padding : Union[int, float] Specify the padding between the top of the histogram and the highest bar. bar_spacing : Union[int, float] Specify the amount of spacing to place between individual bars. border : Union[Tuple[Union[int, float]], int, float] Can be anything that can go into the ``'QColor'`` constructor. Draws a border around the entire histogram in a given color. border_color : Union[QColor, str] class_index : int The index of the target variable in ``'data'``. n_bins : int """ def __init__(self, data, variable, parent=None, height=200, width=300, side_padding=5, top_padding=20, bottom_padding=0, bar_spacing=4, border=0, border_color=None, color_attribute=None, n_bins=10): super().__init__(parent) self.height, self.width = height, width self.padding = side_padding self.bar_spacing = bar_spacing self.data = data self.attribute = data.domain[variable] self.x = data.get_column_view(self.attribute)[0].astype(np.float64) self.x_nans = np.isnan(self.x) self.x = self.x[~self.x_nans] if self.attribute.is_discrete: self.n_bins = len(self.attribute.values) elif self.attribute.is_continuous: # If the attribute is continuous but contains fewer values than the # bins, it is better to assign each their own bin. We will require # at least 2 bins so that the histogram still visually makes sense # except if there is only a single value, then we use 3 bins for # symmetry num_unique = ut.nanunique(self.x).shape[0] if num_unique == 1: self.n_bins = 3 else: self.n_bins = min(max(2, num_unique), n_bins) # Handle target variable index self.color_attribute = color_attribute if self.color_attribute is not None: self.target_var = data.domain[color_attribute] self.y = data.get_column_view(color_attribute)[0] self.y = self.y[~self.x_nans] if not np.issubdtype(self.y.dtype, np.number): self.y = self.y.astype(np.float64) else: self.target_var, self.y = None, None # Borders self.border_color = border_color if border_color is not None else '#000' if isinstance(border, tuple): assert len(border) == 4, 'Border tuple must be of size 4.' self.border = border else: self.border = (border, border, border, border) t, r, b, l = self.border def _draw_border(point_1, point_2, border_width, parent): pen = QPen(QColor(self.border_color)) pen.setCosmetic(True) pen.setWidth(border_width) line = QGraphicsLineItem(QLineF(point_1, point_2), parent) line.setPen(pen) return line top_left = QPointF(0, 0) bottom_left = QPointF(0, self.height) top_right = QPointF(self.width, 0) bottom_right = QPointF(self.width, self.height) self.border_top = _draw_border(top_left, top_right, t, self) if t else None self.border_bottom = _draw_border(bottom_left, bottom_right, b, self) if b else None self.border_left = _draw_border(top_left, bottom_left, l, self) if l else None self.border_right = _draw_border(top_right, bottom_right, r, self) if r else None # _plot_`dim` accounts for all the paddings and spacings self._plot_height = self.height self._plot_height -= top_padding + bottom_padding self._plot_height -= t / 4 + b / 4 self._plot_width = self.width self._plot_width -= 2 * side_padding self._plot_width -= (self.n_bins - 2) * bar_spacing self._plot_width -= l / 4 + r / 4 self.__layout = QGraphicsLinearLayout(Qt.Horizontal, self) self.__layout.setContentsMargins(side_padding + r / 2, top_padding + t / 2, side_padding + l / 2, bottom_padding + b / 2) self.__layout.setSpacing(bar_spacing) # If the data contains any non-NaN values, we can draw a histogram if self.x.size > 0: self.edges, self.distributions = self._histogram() self._draw_histogram() def _get_histogram_edges(self): """Get the edges in the histogram based on the attribute type. In case of a continuous variable, we split the variable range into n bins. In case of a discrete variable, bins don't make sense, so we just return the attribute values. This will return the staring and ending edge, not just the edges in between (in the case of a continuous variable). Returns ------- np.ndarray """ if self.attribute.is_discrete: return np.array( [self.attribute.to_val(v) for v in self.attribute.values]) else: edges = np.linspace(ut.nanmin(self.x), ut.nanmax(self.x), self.n_bins) edge_diff = edges[1] - edges[0] edges = np.hstack((edges, [edges[-1] + edge_diff])) # If the variable takes on a single value, we still need to spit # out some reasonable bin edges if np.all(edges == edges[0]): edges = np.array([edges[0] - 1, edges[0], edges[0] + 1]) return edges def _get_bin_distributions(self, bin_indices): """Compute the distribution of instances within bins. Parameters ---------- bin_indices : np.ndarray An array with same shape as `x` but containing the bin index of the instance. Returns ------- np.ndarray A 2d array; the first dimension represents different bins, the second - the counts of different target values. """ if self.target_var and self.target_var.is_discrete: y = self.y # TODO This probably also isn't the best handling of sparse data... if sp.issparse(y): y = np.squeeze(np.array(y.todense())) # Since y can contain missing values, we need to filter them out as # well as their corresponding `x` values y_nan_mask = np.isnan(y) y, bin_indices = y[~y_nan_mask], bin_indices[~y_nan_mask] y = one_hot(y, dim=len(self.target_var.values)) bins = np.arange(self.n_bins)[:, np.newaxis] mask = bin_indices == bins distributions = np.zeros((self.n_bins, y.shape[1])) for bin_idx in range(self.n_bins): distributions[bin_idx] = y[mask[bin_idx]].sum(axis=0) else: distributions, _ = ut.bincount(bin_indices.astype(np.int64)) # To keep things consistent across different variable types, we # want to return a 2d array where the first dim represent different # bins, and the second the distributions. distributions = distributions[:, np.newaxis] return distributions def _histogram(self): assert self.x.size > 0, 'Cannot calculate histogram on empty array' edges = self._get_histogram_edges() if self.attribute.is_discrete: bin_indices = self.x # TODO It probably isn't a very good idea to convert a sparse row # to a dense array... Converts sparse to 1d numpy array if sp.issparse(bin_indices): bin_indices = np.squeeze( np.asarray(bin_indices.todense(), dtype=np.int64)) elif self.attribute.is_continuous: bin_indices = ut.digitize(self.x, bins=edges[1:-1]).flatten() distributions = self._get_bin_distributions(bin_indices) return edges, distributions def _draw_histogram(self): # In case the data for the variable were all NaNs, then the # distributions will be empty, and we don't need to display any bars if self.x.size == 0: return # In case we have a target var, but the values are all NaNs, then there # is no sense in displaying anything if self.target_var: y_nn = self.y[~np.isnan(self.y)] if y_nn.size == 0: return if self.distributions.ndim > 1: largest_bin_count = self.distributions.sum(axis=1).max() else: largest_bin_count = self.distributions.max() bar_size = self._plot_width / self.n_bins for distr, bin_colors in zip(self.distributions, self._get_colors()): bin_count = distr.sum() bar_height = bin_count / largest_bin_count * self._plot_height bar_layout = QGraphicsLinearLayout(Qt.Vertical) bar_layout.setSpacing(0) bar_layout.addStretch() self.__layout.addItem(bar_layout) bar = ProportionalBarItem( # pylint: disable=blacklisted-name distribution=distr, colors=bin_colors, height=bar_height, bar_size=bar_size, ) bar_layout.addItem(bar) self.layout() def _get_colors(self): """Compute colors for different kinds of histograms.""" target = self.target_var if target and target.is_discrete: colors = [list(target.palette)[:len(target.values)]] * self.n_bins elif self.target_var and self.target_var.is_continuous: palette = self.target_var.palette bins = np.arange(self.n_bins)[:, np.newaxis] edges = self.edges if self.attribute.is_discrete else self.edges[ 1:-1] bin_indices = ut.digitize(self.x, bins=edges) mask = bin_indices == bins colors = [] for bin_idx in range(self.n_bins): biny = self.y[mask[bin_idx]] if np.isfinite(biny).any(): mean = ut.nanmean(biny) / ut.nanmax(self.y) else: mean = 0 # bin is empty, color does not matter colors.append([palette.value_to_qcolor(mean)]) else: colors = [[QColor('#ccc')]] * self.n_bins return colors def boundingRect(self): return QRectF(0, 0, self.width, self.height) def sizeHint(self, which, constraint): return QSizeF(self.width, self.height) def sizePolicy(self): return QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
class Histogram(QGraphicsWidget): """A basic histogram widget. Parameters ---------- data : Table variable : Union[int, str, Variable] parent : QObject height : Union[int, float] width : Union[int, float] side_padding : Union[int, float] Specify the padding between the edges of the histogram and the first and last bars. top_padding : Union[int, float] Specify the padding between the top of the histogram and the highest bar. bar_spacing : Union[int, float] Specify the amount of spacing to place between individual bars. border : Union[Tuple[Union[int, float]], int, float] Can be anything that can go into the ``'QColor'`` constructor. Draws a border around the entire histogram in a given color. border_color : Union[QColor, str] class_index : int The index of the target variable in ``'data'``. n_bins : int """ def __init__(self, data, variable, parent=None, height=200, width=300, side_padding=5, top_padding=20, bar_spacing=4, border=0, border_color=None, color_attribute=None, n_bins=10): super().__init__(parent) self.height, self.width = height, width self.padding = side_padding self.bar_spacing = bar_spacing self.data = data self.attribute = data.domain[variable] self.x = data.get_column_view(self.attribute)[0].astype(np.float64) self.x_nans = np.isnan(self.x) self.x = self.x[~self.x_nans] if self.attribute.is_discrete: self.n_bins = len(self.attribute.values) elif self.attribute.is_continuous: # If the attribute is continuous but contains fewer values than the # bins, it is better to assign each their own bin. We will require # at least 2 bins so that the histogram still visually makes sense # except if there is only a single value, then we use 3 bins for # symmetry num_unique = ut.nanunique(self.x).shape[0] if num_unique == 1: self.n_bins = 3 else: self.n_bins = min(max(2, num_unique), n_bins) # Handle target variable index self.color_attribute = color_attribute if self.color_attribute is not None: self.target_var = data.domain[color_attribute] self.y = data.get_column_view(color_attribute)[0] self.y = self.y[~self.x_nans] if not np.issubdtype(self.y.dtype, np.number): self.y = self.y.astype(np.float64) else: self.target_var, self.y = None, None # Borders self.border_color = border_color if border_color is not None else '#000' if isinstance(border, tuple): assert len(border) == 4, 'Border tuple must be of size 4.' self.border = border else: self.border = (border, border, border, border) t, r, b, l = self.border def _draw_border(point_1, point_2, border_width, parent): pen = QPen(QColor(self.border_color)) pen.setCosmetic(True) pen.setWidth(border_width) line = QGraphicsLineItem(QLineF(point_1, point_2), parent) line.setPen(pen) return line top_left = QPointF(0, 0) bottom_left = QPointF(0, self.height) top_right = QPointF(self.width, 0) bottom_right = QPointF(self.width, self.height) self.border_top = _draw_border(top_left, top_right, t, self) if t else None self.border_bottom = _draw_border(bottom_left, bottom_right, b, self) if b else None self.border_left = _draw_border(top_left, bottom_left, l, self) if l else None self.border_right = _draw_border(top_right, bottom_right, r, self) if r else None # _plot_`dim` accounts for all the paddings and spacings self._plot_height = self.height self._plot_height -= top_padding self._plot_height -= t / 4 + b / 4 self._plot_width = self.width self._plot_width -= 2 * side_padding self._plot_width -= (self.n_bins - 2) * bar_spacing self._plot_width -= l / 4 + r / 4 self.__layout = QGraphicsLinearLayout(Qt.Horizontal, self) self.__layout.setContentsMargins( side_padding + r / 2, top_padding + t / 2, side_padding + l / 2, b / 2 ) self.__layout.setSpacing(bar_spacing) # If the data contains any non-NaN values, we can draw a histogram if self.x.size > 0: self.edges, self.distributions = self._histogram() self._draw_histogram() def _get_histogram_edges(self): """Get the edges in the histogram based on the attribute type. In case of a continuous variable, we split the variable range into n bins. In case of a discrete variable, bins don't make sense, so we just return the attribute values. This will return the staring and ending edge, not just the edges in between (in the case of a continuous variable). Returns ------- np.ndarray """ if self.attribute.is_discrete: return np.array([self.attribute.to_val(v) for v in self.attribute.values]) else: edges = np.linspace(ut.nanmin(self.x), ut.nanmax(self.x), self.n_bins) edge_diff = edges[1] - edges[0] edges = np.hstack((edges, [edges[-1] + edge_diff])) # If the variable takes on a single value, we still need to spit # out some reasonable bin edges if np.all(edges == edges[0]): edges = np.array([edges[0] - 1, edges[0], edges[0] + 1]) return edges def _get_bin_distributions(self, bin_indices): """Compute the distribution of instances within bins. Parameters ---------- bin_indices : np.ndarray An array with same shape as `x` but containing the bin index of the instance. Returns ------- np.ndarray A 2d array; the first dimension represents different bins, the second - the counts of different target values. """ if self.target_var and self.target_var.is_discrete: y = self.y # TODO This probably also isn't the best handling of sparse data... if sp.issparse(y): y = np.squeeze(np.array(y.todense())) # Since y can contain missing values, we need to filter them out as # well as their corresponding `x` values y_nan_mask = np.isnan(y) y, bin_indices = y[~y_nan_mask], bin_indices[~y_nan_mask] y = one_hot(y) # In the event that y does not take up all the values and the # largest discrete value does not appear at all, one hot encoding # will produce too few columns. This causes problems, so we need to # pad y with zeros to properly compute the distribution if y.shape[1] != len(self.target_var.values): n_missing_columns = len(self.target_var.values) - y.shape[1] y = np.hstack((y, np.zeros((y.shape[0], n_missing_columns)))) bins = np.arange(self.n_bins)[:, np.newaxis] mask = bin_indices == bins distributions = np.zeros((self.n_bins, y.shape[1])) for bin_idx in range(self.n_bins): distributions[bin_idx] = y[mask[bin_idx]].sum(axis=0) else: distributions, _ = ut.bincount(bin_indices.astype(np.int64)) # To keep things consistent across different variable types, we # want to return a 2d array where the first dim represent different # bins, and the second the distributions. distributions = distributions[:, np.newaxis] return distributions def _histogram(self): assert self.x.size > 0, 'Cannot calculate histogram on empty array' edges = self._get_histogram_edges() if self.attribute.is_discrete: bin_indices = self.x # TODO It probably isn't a very good idea to convert a sparse row # to a dense array... Converts sparse to 1d numpy array if sp.issparse(bin_indices): bin_indices = np.squeeze(np.asarray( bin_indices.todense(), dtype=np.int64 )) elif self.attribute.is_continuous: bin_indices = ut.digitize(self.x, bins=edges[1:-1]).flatten() distributions = self._get_bin_distributions(bin_indices) return edges, distributions def _draw_histogram(self): # In case the data for the variable were all NaNs, then the # distributions will be empty, and we don't need to display any bars if self.x.size == 0: return # In case we have a target var, but the values are all NaNs, then there # is no sense in displaying anything if self.target_var: y_nn = self.y[~np.isnan(self.y)] if y_nn.size == 0: return if self.distributions.ndim > 1: largest_bin_count = self.distributions.sum(axis=1).max() else: largest_bin_count = self.distributions.max() bar_size = self._plot_width / self.n_bins for distr, bin_colors in zip(self.distributions, self._get_colors()): bin_count = distr.sum() bar_height = bin_count / largest_bin_count * self._plot_height bar_layout = QGraphicsLinearLayout(Qt.Vertical) bar_layout.setSpacing(0) bar_layout.addStretch() self.__layout.addItem(bar_layout) bar = ProportionalBarItem( # pylint: disable=blacklisted-name distribution=distr, colors=bin_colors, height=bar_height, bar_size=bar_size, ) bar_layout.addItem(bar) self.layout() def _get_colors(self): """Compute colors for different kinds of histograms.""" if self.target_var and self.target_var.is_discrete: colors = [[QColor(*color) for color in self.target_var.colors]] * self.n_bins elif self.target_var and self.target_var.is_continuous: palette = ContinuousPaletteGenerator(*self.target_var.colors) bins = np.arange(self.n_bins)[:, np.newaxis] edges = self.edges if self.attribute.is_discrete else self.edges[1:-1] # Need to digitize on `right` here so the samples will be assigned # to the correct bin for coloring bin_indices = ut.digitize(self.x, bins=edges, right=True) mask = bin_indices == bins colors = [] for bin_idx in range(self.n_bins): biny = self.y[mask[bin_idx]] if np.isfinite(biny).any(): mean = ut.nanmean(biny) / ut.nanmax(self.y) else: mean = 0 # bin is empty, color does not matter colors.append([palette[mean]]) else: colors = [[QColor('#ccc')]] * self.n_bins return colors def boundingRect(self): return QRectF(0, 0, self.width, self.height) def sizeHint(self, which, constraint): return QSizeF(self.width, self.height) def sizePolicy(self): return QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
class Legend(Anchorable): """Base legend class. This class provides common attributes for any legend subclasses: - Behaviour on `QGraphicsScene` - Appearance of legend Parameters ---------- parent : QGraphicsItem, optional orientation : Qt.Orientation, optional The default orientation is vertical domain : Orange.data.domain.Domain, optional This field is left optional as in some cases, we may want to simply pass in a list that represents the legend. items : Iterable[QColor, str] bg_color : QColor, optional font : QFont, optional color_indicator_cls : ColorIndicator The color indicator class that will be used to render the indicators. See Also -------- OWDiscreteLegend OWContinuousLegend OWContinuousLegend Notes ----- .. warning:: If the domain parameter is supplied, the items parameter will be ignored. """ def __init__(self, parent=None, orientation=Qt.Vertical, domain=None, items=None, bg_color=QColor(232, 232, 232, 196), font=None, color_indicator_cls=LegendItemSquare, **kwargs): super().__init__(parent, **kwargs) self._layout = None self.orientation = orientation self.bg_color = QBrush(bg_color) self.color_indicator_cls = color_indicator_cls # Set default font if none is given if font is None: self.font = QFont() self.font.setPointSize(10) else: self.font = font self.setFlags(QGraphicsWidget.ItemIsMovable | QGraphicsItem.ItemIgnoresTransformations) self._setup_layout() if domain is not None: self.set_domain(domain) elif items is not None: self.set_items(items) def _clear_layout(self): self._layout = None for child in self.children(): child.setParent(None) def _setup_layout(self): self._clear_layout() self._layout = QGraphicsLinearLayout(self.orientation) self._layout.setContentsMargins(10, 5, 10, 5) # If horizontal, there needs to be horizontal space between the items if self.orientation == Qt.Horizontal: self._layout.setSpacing(10) # If vertical spacing, vertical space is provided by child layouts else: self._layout.setSpacing(0) self.setLayout(self._layout) def set_domain(self, domain): """Handle receiving the domain object. Parameters ---------- domain : Orange.data.domain.Domain Returns ------- Raises ------ AttributeError If the domain does not contain the correct type of class variable. """ raise NotImplementedError() def set_items(self, values): """Handle receiving an array of items. Parameters ---------- values : iterable[object, QColor] Returns ------- """ raise NotImplementedError() @staticmethod def _convert_to_color(obj): if isinstance(obj, QColor): return obj elif isinstance(obj, tuple) or isinstance(obj, list): assert len(obj) in (3, 4) return QColor(*obj) else: return QColor(obj) def setVisible(self, is_visible): """Only display the legend if it contains any items.""" return super().setVisible(is_visible and len(self._layout) > 0) def paint(self, painter, options, widget=None): painter.save() pen = QPen(QColor(196, 197, 193, 200), 1) brush = QBrush(QColor(self.bg_color)) painter.setPen(pen) painter.setBrush(brush) painter.drawRect(self.contentsRect()) painter.restore()