def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: downPos = event.buttonDownPos(Qt.LeftButton) if not self.__tmpLine and self.__dragStartItem and \ (downPos - event.pos()).manhattanLength() > \ QApplication.instance().startDragDistance(): # Start a line drag line = QGraphicsLineItem(self) start = self.__dragStartItem.boundingRect().center() start = self.mapFromItem(self.__dragStartItem, start) line.setLine(start.x(), start.y(), event.pos().x(), event.pos().y()) pen = QPen(Qt.black, 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) line.show() self.__tmpLine = line if self.__tmpLine: # Update the temp line line = self.__tmpLine.line() line.setP2(event.pos()) self.__tmpLine.setLine(line) QGraphicsWidget.mouseMoveEvent(self, event)
def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: downPos = event.buttonDownPos(Qt.LeftButton) if not self.__tmpLine and self.__dragStartItem and \ (downPos - event.pos()).manhattanLength() > \ QApplication.instance().startDragDistance(): # Start a line drag line = QGraphicsLineItem(self) start = self.__dragStartItem.boundingRect().center() start = self.mapFromItem(self.__dragStartItem, start) line.setLine(start.x(), start.y(), event.pos().x(), event.pos().y()) pen = QPen(self.palette().color(QPalette.Foreground), 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) line.show() self.__tmpLine = line if self.__tmpLine: # Update the temp line line = self.__tmpLine.line() line.setP2(event.pos()) self.__tmpLine.setLine(line) QGraphicsWidget.mouseMoveEvent(self, event)
class LinePlotViewBox(ViewBox): def __init__(self, graph, enable_menu=False): ViewBox.__init__(self, enableMenu=enable_menu) self.graph = graph self.setMouseMode(self.PanMode) pen = mkPen(LinePlotStyle.SELECTION_LINE_COLOR, width=LinePlotStyle.SELECTION_LINE_WIDTH) self.selection_line = QGraphicsLineItem() self.selection_line.setPen(pen) self.selection_line.setZValue(1e9) self.addItem(self.selection_line, ignoreBounds=True) def update_selection_line(self, button_down_pos, current_pos): p1 = self.childGroup.mapFromParent(button_down_pos) p2 = self.childGroup.mapFromParent(current_pos) self.selection_line.setLine(QLineF(p1, p2)) self.selection_line.resetTransform() self.selection_line.show() def mouseDragEvent(self, event, axis=None): if self.graph.state == SELECT and axis is None: event.accept() if event.button() == Qt.LeftButton: self.update_selection_line(event.buttonDownPos(), event.pos()) if event.isFinish(): self.selection_line.hide() p1 = self.childGroup.mapFromParent( event.buttonDownPos(event.button())) p2 = self.childGroup.mapFromParent(event.pos()) self.graph.select_by_line(p1, p2) else: self.update_selection_line( event.buttonDownPos(), event.pos()) elif self.graph.state == ZOOMING or self.graph.state == PANNING: event.ignore() super().mouseDragEvent(event, axis=axis) else: event.ignore() def mouseClickEvent(self, event): if event.button() == Qt.RightButton: self.autoRange() else: event.accept() self.graph.deselect_all()
def addLink(self, output, input): """ Add a link between `output` (:class:`OutputSignal`) and `input` (:class:`InputSignal`). """ if not compatible_channels(output, input): return if output not in self.source.output_channels(): raise ValueError("%r is not an output channel of %r" % \ (output, self.source)) if input not in self.sink.input_channels(): raise ValueError("%r is not an input channel of %r" % \ (input, self.sink)) if input.single: # Remove existing link if it exists. for s1, s2, _ in self.__links: if s2 == input: self.removeLink(s1, s2) line = QGraphicsLineItem(self) source_anchor = self.sourceNodeWidget.anchor(output) sink_anchor = self.sinkNodeWidget.anchor(input) source_pos = source_anchor.boundingRect().center() source_pos = self.mapFromItem(source_anchor, source_pos) sink_pos = sink_anchor.boundingRect().center() sink_pos = self.mapFromItem(sink_anchor, sink_pos) line.setLine(source_pos.x(), source_pos.y(), sink_pos.x(), sink_pos.y()) pen = QPen(self.palette().color(QPalette.Foreground), 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) self.__links.append(_Link(output, input, line))
def addLink(self, output, input): """ Add a link between `output` (:class:`OutputSignal`) and `input` (:class:`InputSignal`). """ if not compatible_channels(output, input): return if output not in self.source.output_channels(): raise ValueError("%r is not an output channel of %r" % \ (output, self.source)) if input not in self.sink.input_channels(): raise ValueError("%r is not an input channel of %r" % \ (input, self.sink)) if input.single: # Remove existing link if it exists. for s1, s2, _ in self.__links: if s2 == input: self.removeLink(s1, s2) line = QGraphicsLineItem(self) source_anchor = self.sourceNodeWidget.anchor(output) sink_anchor = self.sinkNodeWidget.anchor(input) source_pos = source_anchor.boundingRect().center() source_pos = self.mapFromItem(source_anchor, source_pos) sink_pos = sink_anchor.boundingRect().center() sink_pos = self.mapFromItem(sink_anchor, sink_pos) line.setLine(source_pos.x(), source_pos.y(), sink_pos.x(), sink_pos.y()) pen = QPen(Qt.black, 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) self.__links.append(_Link(output, input, line))
class StripeItem(QGraphicsWidget): WIDTH = 70 def __init__(self, parent): super().__init__(parent) self.__range = None # type: Tuple[float] self.__value_range = None # type: Tuple[float] self.__model_output = None # type: float self.__base_value = None # type: float self.__group = QGraphicsItemGroup(self) low_color, high_color = QColor(*RGB_LOW), QColor(*RGB_HIGH) self.__low_item = QGraphicsRectItem() self.__low_item.setPen(QPen(low_color)) self.__low_item.setBrush(QBrush(low_color)) self.__high_item = QGraphicsRectItem() self.__high_item.setPen(QPen(high_color)) self.__high_item.setBrush(QBrush(high_color)) self.__low_cover_item = LowCoverItem() self.__high_cover_item = HighCoverItem() pen = QPen(IndicatorItem.COLOR) pen.setStyle(Qt.DashLine) pen.setWidth(1) self.__model_output_line = QGraphicsLineItem() self.__model_output_line.setPen(pen) self.__base_value_line = QGraphicsLineItem() self.__base_value_line.setPen(pen) self.__model_output_ind = IndicatorItem("Model prediction: {}") self.__base_value_ind = IndicatorItem("Base value: {}\nThe average " "prediction for selected class.") self.__group.addToGroup(self.__low_item) self.__group.addToGroup(self.__high_item) self.__group.addToGroup(self.__low_cover_item) self.__group.addToGroup(self.__high_cover_item) self.__group.addToGroup(self.__model_output_line) self.__group.addToGroup(self.__base_value_line) self.__group.addToGroup(self.__model_output_ind) self.__group.addToGroup(self.__base_value_ind) self.__low_parts = [] # type: List[LowPartItem] self.__high_parts = [] # type: List[HighPartItem] @property def total_width(self): widths = [ part.label_item.boundingRect().width() for part in self.__low_parts + self.__high_parts ] + [0] return self.WIDTH + StripePlot.SPACING + max(widths) @property def model_output_ind(self) -> IndicatorItem: return self.__model_output_ind @property def base_value_ind(self) -> IndicatorItem: return self.__base_value_ind def set_data(self, data: PlotData, y_range: Tuple[float, float], height: float): self.__range = y_range self.__value_range = data.value_range self.__model_output = data.model_output self.__base_value = data.base_value r = self.__range[1] - self.__range[0] self.__model_output_ind.set_text(self.__model_output, r) self.__base_value_ind.set_text(self.__base_value, r) for value, label in zip(data.low_values, data.low_labels): self.__add_part(value, label, value / sum(data.low_values), self.__low_parts, LowPartItem) for value, label in zip(data.high_values, data.high_labels): self.__add_part(value, label, value / sum(data.high_values), self.__high_parts, HighPartItem) if self.__low_parts: self.__low_parts[-1].setVisible(False) else: self.__low_item.setVisible(False) self.__low_cover_item.setVisible(False) if self.__high_parts: self.__high_parts[-1].setVisible(False) else: self.__high_item.setVisible(False) self.__high_cover_item.setVisible(False) self.set_z_values() self.set_height(height) def __add_part(self, value: float, label: Tuple[str, str], norm_val: float, list_: List[PartItem], cls_: Type[PartItem]): item = cls_(value, label, norm_val) list_.append(item) self.__group.addToGroup(item) self.__group.addToGroup(item.value_item) self.__group.addToGroup(item.label_item) def set_z_values(self): if len(self.__high_parts) < len(self.__low_parts): self.__high_cover_item.setZValue(-2) self.__high_item.setZValue(-3) for i, item in enumerate(self.__high_parts): item.setZValue(-1) else: self.__low_cover_item.setZValue(-2) self.__low_item.setZValue(-3) for i, item in enumerate(self.__low_parts): item.setZValue(-1) def set_height(self, height: float): if self.__range[1] == self.__range[0]: return height = height / (self.__range[1] - self.__range[0]) y_top = height * (self.__range[1] - self.__value_range[1]) h_top = height * (self.__value_range[1] - self.__model_output) y_bot = height * (self.__range[1] - self.__model_output) h_bot = height * (self.__model_output - self.__value_range[0]) self.__low_item.setRect(QRectF(0, y_top, self.WIDTH, h_top)) self.__high_item.setRect(QRectF(0, y_bot, self.WIDTH, h_bot)) self.__low_cover_item.setY(y_top) self.__high_cover_item.setY(y_bot + h_bot) self._set_indicators_pos(height) def adjust_y_text_low(i): # adjust label y according to TIP_LEN # adjust for 0.8 * TIP_LEN, because label is not 1 pixel wide k = 0.4 if i == len(self.__low_parts) - 1 else 0.8 return PartItem.TIP_LEN * k def adjust_y_text_high(i): k = 0.4 if i == 0 else 0.8 return -PartItem.TIP_LEN * k self._set_parts_pos(height, y_top, h_top / height, self.__low_parts, adjust_y_text_low) self._set_parts_pos(height, y_bot, h_bot / height, self.__high_parts, adjust_y_text_high) def _set_parts_pos(self, height: float, y: float, diff: float, parts: List[PartItem], adjust_y: Callable): for i, item in enumerate(parts): y_delta = height * item.norm_value * diff y_text = y + y_delta / 2 - item.value_height / 2 visible = y_delta > item.value_height + 8 y_test_adj = y_text + adjust_y(i) y_mid = height * (self.__range[1] - self.__model_output) collides = _collides( y_mid, y_mid, y_test_adj, y_test_adj + item.value_item.boundingRect().height()) item.value_item.setVisible(visible and not collides) item.value_item.setY(y_test_adj) item.label_item.setVisible(visible) item.label_item.setY(y_text) y = y + y_delta item.setY(y) def _set_indicators_pos(self, height: float): mo_y = height * (self.__range[1] - self.__model_output) mo_h = self.__model_output_ind.boundingRect().height() self.__model_output_ind.setY(mo_y - mo_h / 2) self.__model_output_line.setLine( 0, mo_y, -StripePlot.SPACING - IndicatorItem.MARGIN, mo_y) bv_y = height * (self.__range[1] - self.__base_value) bv_h = self.__base_value_ind.boundingRect().height() self.__base_value_ind.setY(bv_y - bv_h / 2) self.__base_value_line.setLine( -StripePlot.SPACING, bv_y, -StripePlot.SPACING - IndicatorItem.MARGIN, bv_y) collides = _collides(mo_y, mo_y + mo_h, bv_y, bv_y + bv_h, d=6) self.__base_value_ind.setVisible(not collides)
class AnchorItem(pg.GraphicsWidget): def __init__(self, parent=None, line=QLineF(), text="", **kwargs): super().__init__(parent, **kwargs) self._text = text self.setFlag(pg.GraphicsObject.ItemHasNoContents) self._spine = QGraphicsLineItem(line, self) angle = line.angle() self._arrow = pg.ArrowItem(parent=self, angle=0) self._arrow.setPos(self._spine.line().p2()) self._arrow.setRotation(angle) self._arrow.setStyle(headLen=10) self._label = TextItem(text=text, color=(10, 10, 10)) self._label.setParentItem(self) self._label.setPos(*self.get_xy()) self._label.setColor(self.palette().color(QPalette.Text)) if parent is not None: self.setParentItem(parent) def get_xy(self): point = self._spine.line().p2() return point.x(), point.y() def setFont(self, font): self._label.setFont(font) def setText(self, text): if text != self._text: self._text = text self._label.setText(text) self._label.setVisible(bool(text)) def text(self): return self._text def setLine(self, *line): line = QLineF(*line) if line != self._spine.line(): self._spine.setLine(line) self.__updateLayout() def line(self): return self._spine.line() def setPen(self, pen): self._spine.setPen(pen) def setArrowVisible(self, visible): self._arrow.setVisible(visible) def paint(self, painter, option, widget): pass def boundingRect(self): return QRectF() def viewTransformChanged(self): self.__updateLayout() def __updateLayout(self): T = self.sceneTransform() if T is None: T = QTransform() # map the axis spine to scene coord. system. viewbox_line = T.map(self._spine.line()) angle = viewbox_line.angle() assert not np.isnan(angle) # note in Qt the y axis is inverted (90 degree angle 'points' down) left_quad = 270 < angle <= 360 or -0.0 <= angle < 90 # position the text label along the viewbox_line label_pos = self._spine.line().pointAt(0.90) if left_quad: # Anchor the text under the axis spine anchor = (0.5, -0.1) else: # Anchor the text over the axis spine anchor = (0.5, 1.1) self._label.setPos(label_pos) self._label.setAnchor(pg.Point(*anchor)) self._label.setRotation(-angle if left_quad else 180 - angle) self._arrow.setPos(self._spine.line().p2()) self._arrow.setRotation(180 - angle) def changeEvent(self, event): if event.type() == QEvent.PaletteChange: self._label.setColor(self.palette().color(QPalette.Text)) super().changeEvent(event)
class LinePlotViewBox(ViewBox): selection_changed = Signal(np.ndarray) def __init__(self): super().__init__(enableMenu=False) self._profile_items = None self._can_select = True self._graph_state = SELECT self.setMouseMode(self.PanMode) pen = mkPen(LinePlotStyle.SELECTION_LINE_COLOR, width=LinePlotStyle.SELECTION_LINE_WIDTH) self.selection_line = QGraphicsLineItem() self.selection_line.setPen(pen) self.selection_line.setZValue(1e9) self.addItem(self.selection_line, ignoreBounds=True) def update_selection_line(self, button_down_pos, current_pos): p1 = self.childGroup.mapFromParent(button_down_pos) p2 = self.childGroup.mapFromParent(current_pos) self.selection_line.setLine(QLineF(p1, p2)) self.selection_line.resetTransform() self.selection_line.show() def set_graph_state(self, state): self._graph_state = state def enable_selection(self, enable): self._can_select = enable def get_selected(self, p1, p2): if self._profile_items is None: return np.array(False) return line_intersects_profiles(np.array([p1.x(), p1.y()]), np.array([p2.x(), p2.y()]), self._profile_items) def add_profiles(self, y): if sp.issparse(y): y = y.todense() self._profile_items = np.array( [np.vstack((np.full((1, y.shape[0]), i + 1), y[:, i].flatten())).T for i in range(y.shape[1])]) def remove_profiles(self): self._profile_items = None def mouseDragEvent(self, ev, axis=None): if self._graph_state == SELECT and axis is None and self._can_select: ev.accept() if ev.button() == Qt.LeftButton: self.update_selection_line(ev.buttonDownPos(), ev.pos()) if ev.isFinish(): self.selection_line.hide() p1 = self.childGroup.mapFromParent( ev.buttonDownPos(ev.button())) p2 = self.childGroup.mapFromParent(ev.pos()) self.selection_changed.emit(self.get_selected(p1, p2)) elif self._graph_state == ZOOMING or self._graph_state == PANNING: ev.ignore() super().mouseDragEvent(ev, axis=axis) else: ev.ignore() def mouseClickEvent(self, ev): if ev.button() == Qt.RightButton: self.autoRange() self.enableAutoRange() else: ev.accept() self.selection_changed.emit(np.array(False)) def reset(self): self._profile_items = None self._can_select = True self._graph_state = SELECT
class AnchorItem(pg.GraphicsObject): def __init__(self, parent=None, line=QLineF(), text="", **kwargs): super().__init__(parent, **kwargs) self._text = text self.setFlag(pg.GraphicsObject.ItemHasNoContents) self._spine = QGraphicsLineItem(line, self) angle = line.angle() self._arrow = pg.ArrowItem(parent=self, angle=0) self._arrow.setPos(self._spine.line().p2()) self._arrow.setRotation(angle) self._arrow.setStyle(headLen=10) self._label = TextItem(text=text, color=(10, 10, 10)) self._label.setParentItem(self) self._label.setPos(*self.get_xy()) if parent is not None: self.setParentItem(parent) def get_xy(self): point = self._spine.line().p2() return point.x(), point.y() def setText(self, text): if text != self._text: self._text = text self._label.setText(text) self._label.setVisible(bool(text)) def text(self): return self._text def setLine(self, *line): line = QLineF(*line) if line != self._spine.line(): self._spine.setLine(line) self.__updateLayout() def line(self): return self._spine.line() def setPen(self, pen): self._spine.setPen(pen) def setArrowVisible(self, visible): self._arrow.setVisible(visible) def paint(self, painter, option, widget): pass def boundingRect(self): return QRectF() def viewTransformChanged(self): self.__updateLayout() def __updateLayout(self): T = self.sceneTransform() if T is None: T = QTransform() # map the axis spine to scene coord. system. viewbox_line = T.map(self._spine.line()) angle = viewbox_line.angle() assert not np.isnan(angle) # note in Qt the y axis is inverted (90 degree angle 'points' down) left_quad = 270 < angle <= 360 or -0.0 <= angle < 90 # position the text label along the viewbox_line label_pos = self._spine.line().pointAt(0.90) if left_quad: # Anchor the text under the axis spine anchor = (0.5, -0.1) else: # Anchor the text over the axis spine anchor = (0.5, 1.1) self._label.setPos(label_pos) self._label.setAnchor(pg.Point(*anchor)) self._label.setRotation(-angle if left_quad else 180 - angle) self._arrow.setPos(self._spine.line().p2()) self._arrow.setRotation(180 - angle)
class LinePlotViewBox(ViewBox): selection_changed = Signal(np.ndarray) def __init__(self): super().__init__(enableMenu=False) self._profile_items = None self._can_select = True self._graph_state = SELECT self.setMouseMode(self.PanMode) pen = mkPen(LinePlotStyle.SELECTION_LINE_COLOR, width=LinePlotStyle.SELECTION_LINE_WIDTH) self.selection_line = QGraphicsLineItem() self.selection_line.setPen(pen) self.selection_line.setZValue(1e9) self.addItem(self.selection_line, ignoreBounds=True) def update_selection_line(self, button_down_pos, current_pos): p1 = self.childGroup.mapFromParent(button_down_pos) p2 = self.childGroup.mapFromParent(current_pos) self.selection_line.setLine(QLineF(p1, p2)) self.selection_line.resetTransform() self.selection_line.show() def set_graph_state(self, state): self._graph_state = state def enable_selection(self, enable): self._can_select = enable def get_selected(self, p1, p2): if self._profile_items is None: return np.array(False) return line_intersects_profiles(np.array([p1.x(), p1.y()]), np.array([p2.x(), p2.y()]), self._profile_items) def add_profiles(self, y): if sp.issparse(y): y = y.todense() self._profile_items = np.array( [np.vstack((np.full((1, y.shape[0]), i + 1), y[:, i].flatten())).T for i in range(y.shape[1])]) def remove_profiles(self): self._profile_items = None def mouseDragEvent(self, event, axis=None): if self._graph_state == SELECT and axis is None and self._can_select: event.accept() if event.button() == Qt.LeftButton: self.update_selection_line(event.buttonDownPos(), event.pos()) if event.isFinish(): self.selection_line.hide() p1 = self.childGroup.mapFromParent( event.buttonDownPos(event.button())) p2 = self.childGroup.mapFromParent(event.pos()) self.selection_changed.emit(self.get_selected(p1, p2)) elif self._graph_state == ZOOMING or self._graph_state == PANNING: event.ignore() super().mouseDragEvent(event, axis=axis) else: event.ignore() def mouseClickEvent(self, event): if event.button() == Qt.RightButton: self.autoRange() self.enableAutoRange() else: event.accept() self.selection_changed.emit(np.array(False)) def reset(self): self._profile_items = None self._can_select = True self._graph_state = SELECT
class OWAxis(QGraphicsItem): Role = OWPalette.Axis def __init__(self, id, title='', title_above=False, title_location=AxisMiddle, line=None, arrows=0, plot=None, bounds=None): QGraphicsItem.__init__(self) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setZValue(AxisZValue) self.id = id self.title = title self.title_location = title_location self.data_line = line self.plot = plot self.graph_line = None self.size = None self.scale = None self.tick_length = (10, 5, 0) self.arrows = arrows self.title_above = title_above self.line_item = QGraphicsLineItem(self) self.title_item = QGraphicsTextItem(self) self.end_arrow_item = None self.start_arrow_item = None self.show_title = False self.scale = None path = QPainterPath() path.setFillRule(Qt.WindingFill) path.moveTo(0, 3.09) path.lineTo(0, -3.09) path.lineTo(9.51, 0) path.closeSubpath() self.arrow_path = path self.label_items = [] self.label_bg_items = [] self.tick_items = [] self._ticks = [] self.zoom_transform = QTransform() self.labels = None self.values = None self._bounds = bounds self.auto_range = None self.auto_scale = True self.zoomable = False self.update_callback = None self.max_text_width = 50 self.text_margin = 5 self.always_horizontal_text = False @staticmethod def compute_scale(min, max): magnitude = int(3 * log10(abs(max - min)) + 1) if magnitude % 3 == 0: first_place = 1 elif magnitude % 3 == 1: first_place = 2 else: first_place = 5 magnitude = magnitude // 3 - 1 step = first_place * pow(10, magnitude) first_val = ceil(min / step) * step return first_val, step def update_ticks(self): self._ticks = [] major, medium, minor = self.tick_length if self.labels is not None and not self.auto_scale: values = self.values or range(len(self.labels)) for i, text in zip(values, self.labels): self._ticks.append((i, text, medium, 1)) else: if self.scale and not self.auto_scale: min, max, step = self.scale elif self.auto_range: min, max = self.auto_range if min is not None and max is not None: step = (max - min) / 10 else: return else: return if max == min: return val, step = self.compute_scale(min, max) while val <= max: self._ticks.append((val, "%.4g" % val, medium, step)) val += step def update_graph(self): if self.update_callback: self.update_callback() def update(self, zoom_only=False): self.update_ticks() line_color = self.plot.color(OWPalette.Axis) text_color = self.plot.color(OWPalette.Text) if not self.graph_line or not self.scene(): return self.line_item.setLine(self.graph_line) self.line_item.setPen(line_color) if self.title: self.title_item.setHtml('<b>' + self.title + '</b>') self.title_item.setDefaultTextColor(text_color) if self.title_location == AxisMiddle: title_p = 0.5 elif self.title_location == AxisEnd: title_p = 0.95 else: title_p = 0.05 title_pos = self.graph_line.pointAt(title_p) v = self.graph_line.normalVector().unitVector() dense_text = False if hasattr(self, 'title_margin'): offset = self.title_margin elif self._ticks: if self.should_be_expanded(): offset = 55 dense_text = True else: offset = 35 else: offset = 10 if self.title_above: title_pos += (v.p2() - v.p1()) * ( offset + QFontMetrics(self.title_item.font()).height()) else: title_pos -= (v.p2() - v.p1()) * offset ## TODO: Move it according to self.label_pos self.title_item.setVisible(self.show_title) self.title_item.setRotation(-self.graph_line.angle()) c = self.title_item.mapToParent( self.title_item.boundingRect().center()) tl = self.title_item.mapToParent( self.title_item.boundingRect().topLeft()) self.title_item.setPos(title_pos - c + tl) ## Arrows if not zoom_only: if self.start_arrow_item: self.scene().removeItem(self.start_arrow_item) self.start_arrow_item = None if self.end_arrow_item: self.scene().removeItem(self.end_arrow_item) self.end_arrow_item = None if self.arrows & AxisStart: if not zoom_only or not self.start_arrow_item: self.start_arrow_item = QGraphicsPathItem( self.arrow_path, self) self.start_arrow_item.setPos(self.graph_line.p1()) self.start_arrow_item.setRotation(-self.graph_line.angle() + 180) self.start_arrow_item.setBrush(line_color) self.start_arrow_item.setPen(line_color) if self.arrows & AxisEnd: if not zoom_only or not self.end_arrow_item: self.end_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.end_arrow_item.setPos(self.graph_line.p2()) self.end_arrow_item.setRotation(-self.graph_line.angle()) self.end_arrow_item.setBrush(line_color) self.end_arrow_item.setPen(line_color) ## Labels n = len(self._ticks) resize_plot_item_list(self.label_items, n, QGraphicsTextItem, self) resize_plot_item_list(self.label_bg_items, n, QGraphicsRectItem, self) resize_plot_item_list(self.tick_items, n, QGraphicsLineItem, self) test_rect = QRectF(self.graph_line.p1(), self.graph_line.p2()).normalized() test_rect.adjust(-1, -1, 1, 1) n_v = self.graph_line.normalVector().unitVector() if self.title_above: n_p = n_v.p2() - n_v.p1() else: n_p = n_v.p1() - n_v.p2() l_v = self.graph_line.unitVector() l_p = l_v.p2() - l_v.p1() for i in range(n): pos, text, size, step = self._ticks[i] hs = 0.5 * step tick_pos = self.map_to_graph(pos) if not test_rect.contains(tick_pos): self.tick_items[i].setVisible(False) self.label_items[i].setVisible(False) continue item = self.label_items[i] item.setVisible(True) if not zoom_only: if self.id in XAxes or getattr(self, 'is_horizontal', False): item.setHtml('<center>' + Qt.escape(text.strip()) + '</center>') else: item.setHtml(Qt.escape(text.strip())) item.setTextWidth(-1) text_angle = 0 if dense_text: w = min(item.boundingRect().width(), self.max_text_width) item.setTextWidth(w) if self.title_above: label_pos = tick_pos + n_p * ( w + self.text_margin ) + l_p * item.boundingRect().height() / 2 else: label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect( ).height() / 2 text_angle = -90 if self.title_above else 90 else: w = min( item.boundingRect().width(), QLineF(self.map_to_graph(pos - hs), self.map_to_graph(pos + hs)).length()) label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect( ).height() / 2 item.setTextWidth(w) if not self.always_horizontal_text: if self.title_above: item.setRotation(-self.graph_line.angle() - text_angle) else: item.setRotation(self.graph_line.angle() - text_angle) item.setPos(label_pos) item.setDefaultTextColor(text_color) self.label_bg_items[i].setRect(item.boundingRect()) self.label_bg_items[i].setPen(QPen(Qt.NoPen)) self.label_bg_items[i].setBrush(self.plot.color(OWPalette.Canvas)) item = self.tick_items[i] item.setVisible(True) tick_line = QLineF(v) tick_line.translate(-tick_line.p1()) tick_line.setLength(size) if self.title_above: tick_line.setAngle(tick_line.angle() + 180) item.setLine(tick_line) item.setPen(line_color) item.setPos(self.map_to_graph(pos)) @staticmethod def make_title(label, unit=None): lab = '<i>' + label + '</i>' if unit: lab = lab + ' [' + unit + ']' return lab def set_line(self, line): self.graph_line = line self.update() def set_title(self, title): self.title = title self.update() def set_show_title(self, b): self.show_title = b self.update() def set_labels(self, labels, values): self.labels = labels self.values = values self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_scale(self, min, max, step_size): self.scale = (min, max, step_size) self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_tick_length(self, minor, medium, major): self.tick_length = (minor, medium, major) self.update() def map_to_graph(self, x): min, max = self.plot.bounds_for_axis(self.id) if min == max: return QPointF() line_point = self.graph_line.pointAt((x - min) / (max - min)) end_point = line_point * self.zoom_transform return self.projection(end_point, self.graph_line) @staticmethod def projection(point, line): norm = line.normalVector() norm.translate(point - norm.p1()) p = QPointF() type = line.intersect(norm, p) return p def continuous_labels(self): min, max, step = self.scale magnitude = log10(abs(max - min)) def paint(self, painter, option, widget): pass def boundingRect(self): return QRectF() def ticks(self): if not self._ticks: self.update_ticks() return self._ticks def bounds(self): if self._bounds: return self._bounds if self.labels: return -0.2, len(self.labels) - 0.8 elif self.scale: min, max, _step = self.scale return min, max elif self.auto_range: return self.auto_range else: return 0, 1 def set_bounds(self, value): self._bounds = value def should_be_expanded(self): self.update_ticks() return self.id in YAxes or self.always_horizontal_text or sum( len(t[1]) for t in self._ticks) * 12 > self.plot.width()
class ViolinPlot(QGraphicsWidget): LABEL_COLUMN, VIOLIN_COLUMN, LEGEND_COLUMN = range(3) VIOLIN_COLUMN_WIDTH, OFFSET = 300, 80 MAX_N_ITEMS = 100 selection_cleared = Signal() selection_changed = Signal(float, float, str) resized = Signal() def __init__(self): super().__init__() self.__violin_column_width = self.VIOLIN_COLUMN_WIDTH # type: int self.__range = None # type: Optional[Tuple[float, float]] self.__violin_items = [] # type: List[ViolinItem] self.__variable_items = [] # type: List[VariableItem] self.__bottom_axis = AxisItem(parent=self, orientation="bottom", maxTickLength=7, pen=QPen(Qt.black)) self.__bottom_axis.setLabel("Impact on model output") self.__vertical_line = QGraphicsLineItem(self.__bottom_axis) self.__vertical_line.setPen(QPen(Qt.gray)) self.__legend = Legend(self) self.__layout = QGraphicsGridLayout() self.__layout.addItem(self.__legend, 0, ViolinPlot.LEGEND_COLUMN) self.__layout.setVerticalSpacing(0) self.setLayout(self.__layout) self.parameter_setter = ParameterSetter(self) @property def violin_column_width(self): return self.__violin_column_width @violin_column_width.setter def violin_column_width(self, view_width: int): j = ViolinPlot.LABEL_COLUMN w = max([self.__layout.itemAt(i, j).item.boundingRect().width() for i in range(len(self.__violin_items))] + [0]) width = view_width - self.legend.sizeHint().width() - self.OFFSET - w self.__violin_column_width = max(self.VIOLIN_COLUMN_WIDTH, width) @property def bottom_axis(self): return self.__bottom_axis @property def labels(self): return self.__variable_items @property def legend(self): return self.__legend def set_data(self, x: np.ndarray, colors: np.ndarray, names: List[str], n_attrs: float, view_width: int): self.violin_column_width = view_width abs_max = np.max(np.abs(x)) * 1.05 self.__range = (-abs_max, abs_max) self._set_violin_items(x, colors, names) self._set_labels(names) self._set_bottom_axis() self.set_n_visible(n_attrs) def set_n_visible(self, n: int): for i in range(len(self.__violin_items)): violin_item = self.__layout.itemAt(i, ViolinPlot.VIOLIN_COLUMN) violin_item.setVisible(i < n) text_item = self.__layout.itemAt(i, ViolinPlot.LABEL_COLUMN).item text_item.setVisible(i < n) self.set_vertical_line() def rescale(self, view_width: int): self.violin_column_width = view_width with temp_seed(0): for item in self.__violin_items: item.rescale(self.violin_column_width) self.__bottom_axis.setWidth(self.violin_column_width) x = self.violin_column_width / 2 self.__vertical_line.setLine(x, 0, x, self.__vertical_line.line().y2()) def show_legend(self, show: bool): self.__legend.setVisible(show) self.__bottom_axis.setWidth(self.violin_column_width) x = self.violin_column_width / 2 self.__vertical_line.setLine(x, 0, x, self.__vertical_line.line().y2()) def _set_violin_items(self, x: np.ndarray, colors: np.ndarray, labels: List[str]): with temp_seed(0): for i in range(x.shape[1]): item = ViolinItem(self, labels[i], self.__range, self.violin_column_width) item.set_data(x[:, i], colors[:, i]) item.selection_changed.connect(self.select) self.__violin_items.append(item) self.__layout.addItem(item, i, ViolinPlot.VIOLIN_COLUMN) if i == self.MAX_N_ITEMS: break def _set_labels(self, labels: List[str]): for i, (label, _) in enumerate(zip(labels, self.__violin_items)): text = VariableItem(self, label) item = SimpleLayoutItem(text) item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__layout.addItem(item, i, ViolinPlot.LABEL_COLUMN, Qt.AlignRight | Qt.AlignVCenter) self.__variable_items.append(item) def _set_bottom_axis(self): self.__bottom_axis.setRange(*self.__range) self.__layout.addItem(self.__bottom_axis, len(self.__violin_items), ViolinPlot.VIOLIN_COLUMN) def set_vertical_line(self): x = self.violin_column_width / 2 height = 0 for i in range(len(self.__violin_items)): violin_item = self.__layout.itemAt(i, ViolinPlot.VIOLIN_COLUMN) text_item = self.__layout.itemAt(i, ViolinPlot.LABEL_COLUMN).item if violin_item.isVisible(): height += max(text_item.boundingRect().height(), violin_item.preferredSize().height()) self.__vertical_line.setLine(x, 0, x, -height) def deselect(self): self.selection_cleared.emit() def select(self, *args): self.selection_changed.emit(*args) def select_from_settings(self, x1: float, x2: float, attr_name: str): point_r_diff = 2 * self.__range[1] / (self.violin_column_width / 2) for item in self.__violin_items: if item.attr_name == attr_name: item.add_selection_rect(x1 - point_r_diff, x2 + point_r_diff) break self.select(x1, x2, attr_name) def apply_visual_settings(self, settings: Dict): for key, value in settings.items(): self.parameter_setter.set_parameter(key, value)
class ViolinPlot(QGraphicsWidget): LABEL_COLUMN, VIOLIN_COLUMN, LEGEND_COLUMN = range(3) VIOLIN_COLUMN_WIDTH, OFFSET = 300, 250 MAX_N_ITEMS = 100 MAX_ATTR_LEN = 20 selection_cleared = Signal() selection_changed = Signal(float, float, str) def __init__(self): super().__init__() self.__violin_column_width = self.VIOLIN_COLUMN_WIDTH # type: int self.__range = None # type: Optional[Tuple[float, float]] self.__violin_items = [] # type: List[ViolinItem] self.__bottom_axis = pg.AxisItem(parent=self, orientation="bottom", maxTickLength=7, pen=QPen(Qt.black)) self.__bottom_axis.setLabel("Impact on model output") self.__vertical_line = QGraphicsLineItem(self.__bottom_axis) self.__vertical_line.setPen(QPen(Qt.gray)) self.__legend = Legend(self) self.__layout = QGraphicsGridLayout() self.__layout.addItem(self.__legend, 0, ViolinPlot.LEGEND_COLUMN) self.__layout.setVerticalSpacing(0) self.setLayout(self.__layout) @property def violin_column_width(self): return self.__violin_column_width @violin_column_width.setter def violin_column_width(self, view_width: int): self.__violin_column_width = max(self.VIOLIN_COLUMN_WIDTH, view_width - self.OFFSET) @property def bottom_axis(self): return self.__bottom_axis def set_data(self, x: np.ndarray, colors: np.ndarray, names: List[str], n_attrs: float, view_width: int): self.violin_column_width = view_width abs_max = np.max(np.abs(x)) * 1.05 self.__range = (-abs_max, abs_max) self._set_violin_items(x, colors, names) self._set_labels(names) self._set_bottom_axis() self._set_vertical_line() self.set_n_visible(n_attrs) def set_n_visible(self, n: int): for i in range(len(self.__violin_items)): violin_item = self.__layout.itemAt(i, ViolinPlot.VIOLIN_COLUMN) violin_item.setVisible(i < n) text_item = self.__layout.itemAt(i, ViolinPlot.LABEL_COLUMN).item text_item.setVisible(i < n) x = self.__vertical_line.line().x1() n = min(n, len(self.__violin_items)) self.__vertical_line.setLine(x, 0, x, -ViolinItem.HEIGHT * n) def rescale(self, view_width: int): self.violin_column_width = view_width with temp_seed(0): for item in self.__violin_items: item.rescale(self.violin_column_width) self.__bottom_axis.setWidth(self.violin_column_width) x = self.violin_column_width / 2 self.__vertical_line.setLine(x, 0, x, self.__vertical_line.line().y2()) def show_legend(self, show: bool): self.__legend.setVisible(show) self.__bottom_axis.setWidth(self.violin_column_width) x = self.violin_column_width / 2 self.__vertical_line.setLine(x, 0, x, self.__vertical_line.line().y2()) def _set_violin_items(self, x: np.ndarray, colors: np.ndarray, labels: List[str]): with temp_seed(0): for i in range(x.shape[1]): item = ViolinItem(self, labels[i], self.__range, self.violin_column_width) item.set_data(x[:, i], colors[:, i]) item.selection_changed.connect(self.select) self.__violin_items.append(item) self.__layout.addItem(item, i, ViolinPlot.VIOLIN_COLUMN) if i == self.MAX_N_ITEMS: break def _set_labels(self, labels: List[str]): for i, (label, _) in enumerate(zip(labels, self.__violin_items)): short = f"{label[:self.MAX_ATTR_LEN - 1]}..." \ if len(label) > self.MAX_ATTR_LEN else label text = QGraphicsSimpleTextItem(short, self) text.setToolTip(label) item = SimpleLayoutItem(text) item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__layout.addItem(item, i, ViolinPlot.LABEL_COLUMN, Qt.AlignRight | Qt.AlignVCenter) def _set_bottom_axis(self): self.__bottom_axis.setRange(*self.__range) self.__layout.addItem(self.__bottom_axis, len(self.__violin_items), ViolinPlot.VIOLIN_COLUMN) def _set_vertical_line(self): x = self.violin_column_width / 2 n = len(self.__violin_items) self.__vertical_line.setLine(x, 0, x, -ViolinItem.HEIGHT * n) def deselect(self): self.selection_cleared.emit() def select(self, *args): self.selection_changed.emit(*args) def select_from_settings(self, x1: float, x2: float, attr_name: str): point_r_diff = 2 * self.__range[1] / (self.violin_column_width / 2) for item in self.__violin_items: if item.attr_name == attr_name: item.add_selection_rect(x1 - point_r_diff, x2 + point_r_diff) break self.select(x1, x2, attr_name)
class FeaturesPlot(QGraphicsWidget): BOTTOM_AXIS_LABEL = "Feature Importance" LABEL_COLUMN, ITEM_COLUMN = range(2) ITEM_COLUMN_WIDTH, OFFSET = 300, 80 selection_cleared = Signal() selection_changed = Signal(object) resized = Signal() def __init__(self): super().__init__() self._item_column_width = self.ITEM_COLUMN_WIDTH self._range: Optional[Tuple[float, float]] = None self._items: List[FeatureItem] = [] self._variable_items: List[VariableItem] = [] self._bottom_axis = AxisItem(parent=self, orientation="bottom", maxTickLength=7, pen=QPen(Qt.black)) self._bottom_axis.setLabel(self.BOTTOM_AXIS_LABEL) self._vertical_line = QGraphicsLineItem(self._bottom_axis) self._vertical_line.setPen(QPen(Qt.gray)) self._layout = QGraphicsGridLayout() self._layout.setVerticalSpacing(0) self.setLayout(self._layout) self.parameter_setter = BaseParameterSetter(self) @property def item_column_width(self) -> int: return self._item_column_width @item_column_width.setter def item_column_width(self, view_width: int): j = FeaturesPlot.LABEL_COLUMN w = max([ self._layout.itemAt(i, j).item.boundingRect().width() for i in range(len(self._items)) ] + [0]) width = view_width - self.OFFSET - w self._item_column_width = max(self.ITEM_COLUMN_WIDTH, width) @property def x0_scaled(self) -> float: min_max = self._range[1] - self._range[0] return -self._range[0] * self.item_column_width / min_max @property def bottom_axis(self) -> AxisItem: return self._bottom_axis @property def labels(self) -> List[VariableItem]: return self._variable_items def set_data(self, x: np.ndarray, names: List[str], n_attrs: int, view_width: int, *plot_args): self.item_column_width = view_width self._set_range(x, *plot_args) self._set_items(x, names, *plot_args) self._set_labels(names) self._set_bottom_axis() self.set_n_visible(n_attrs) def _set_range(self, *_): raise NotImplementedError def _set_items(self, *_): raise NotImplementedError def set_n_visible(self, n: int): for i in range(len(self._items)): item = self._layout.itemAt(i, FeaturesPlot.ITEM_COLUMN) item.setVisible(i < n) text_item = self._layout.itemAt(i, FeaturesPlot.LABEL_COLUMN).item text_item.setVisible(i < n) self.set_vertical_line() def rescale(self, view_width: int): self.item_column_width = view_width for item in self._items: item.rescale(self.item_column_width) self._bottom_axis.setWidth(self.item_column_width) x = self.x0_scaled self._vertical_line.setLine(x, 0, x, self._vertical_line.line().y2()) self.updateGeometry() def set_height(self, height: float): for i in range(len(self._items)): item = self._layout.itemAt(i, FeaturesPlot.ITEM_COLUMN) item.set_height(height) self.set_vertical_line() self.updateGeometry() def _set_labels(self, labels: List[str]): for i, (label, _) in enumerate(zip(labels, self._items)): text = VariableItem(self, label) item = SimpleLayoutItem(text) item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self._layout.addItem(item, i, FeaturesPlot.LABEL_COLUMN, Qt.AlignRight | Qt.AlignVCenter) self._variable_items.append(item) def _set_bottom_axis(self): self._bottom_axis.setRange(*self._range) self._layout.addItem(self._bottom_axis, len(self._items), FeaturesPlot.ITEM_COLUMN) def set_vertical_line(self): height = 0 for i in range(len(self._items)): item = self._layout.itemAt(i, FeaturesPlot.ITEM_COLUMN) text_item = self._layout.itemAt(i, FeaturesPlot.LABEL_COLUMN).item if item.isVisible(): height += max(text_item.boundingRect().height(), item.preferredSize().height()) self._vertical_line.setLine(self.x0_scaled, 0, self.x0_scaled, -height) def deselect(self): self.selection_cleared.emit() def select(self, *args): self.selection_changed.emit(*args) def select_from_settings(self, *_): raise NotImplementedError def apply_visual_settings(self, settings: Dict): for key, value in settings.items(): self.parameter_setter.set_parameter(key, value)
class OWAxis(QGraphicsItem): Role = OWPalette.Axis def __init__(self, id, title='', title_above=False, title_location=AxisMiddle, line=None, arrows=0, plot=None, bounds=None): QGraphicsItem.__init__(self) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setZValue(AxisZValue) self.id = id self.title = title self.title_location = title_location self.data_line = line self.plot = plot self.graph_line = None self.size = None self.scale = None self.tick_length = (10, 5, 0) self.arrows = arrows self.title_above = title_above self.line_item = QGraphicsLineItem(self) self.title_item = QGraphicsTextItem(self) self.end_arrow_item = None self.start_arrow_item = None self.show_title = False self.scale = None path = QPainterPath() path.setFillRule(Qt.WindingFill) path.moveTo(0, 3.09) path.lineTo(0, -3.09) path.lineTo(9.51, 0) path.closeSubpath() self.arrow_path = path self.label_items = [] self.label_bg_items = [] self.tick_items = [] self._ticks = [] self.zoom_transform = QTransform() self.labels = None self.values = None self._bounds = bounds self.auto_range = None self.auto_scale = True self.zoomable = False self.update_callback = None self.max_text_width = 50 self.text_margin = 5 self.always_horizontal_text = False @staticmethod def compute_scale(min, max): magnitude = int(3 * log10(abs(max - min)) + 1) if magnitude % 3 == 0: first_place = 1 elif magnitude % 3 == 1: first_place = 2 else: first_place = 5 magnitude = magnitude // 3 - 1 step = first_place * pow(10, magnitude) first_val = ceil(min / step) * step return first_val, step def update_ticks(self): self._ticks = [] major, medium, minor = self.tick_length if self.labels is not None and not self.auto_scale: values = self.values or range(len(self.labels)) for i, text in zip(values, self.labels): self._ticks.append((i, text, medium, 1)) else: if self.scale and not self.auto_scale: min, max, step = self.scale elif self.auto_range: min, max = self.auto_range if min is not None and max is not None: step = (max - min) / 10 else: return else: return if max == min: return val, step = self.compute_scale(min, max) while val <= max: self._ticks.append((val, "%.4g" % val, medium, step)) val += step def update_graph(self): if self.update_callback: self.update_callback() def update(self, zoom_only=False): self.update_ticks() line_color = self.plot.color(OWPalette.Axis) text_color = self.plot.color(OWPalette.Text) if not self.graph_line or not self.scene(): return self.line_item.setLine(self.graph_line) self.line_item.setPen(line_color) if self.title: self.title_item.setHtml('<b>' + self.title + '</b>') self.title_item.setDefaultTextColor(text_color) if self.title_location == AxisMiddle: title_p = 0.5 elif self.title_location == AxisEnd: title_p = 0.95 else: title_p = 0.05 title_pos = self.graph_line.pointAt(title_p) v = self.graph_line.normalVector().unitVector() dense_text = False if hasattr(self, 'title_margin'): offset = self.title_margin elif self._ticks: if self.should_be_expanded(): offset = 55 dense_text = True else: offset = 35 else: offset = 10 if self.title_above: title_pos += (v.p2() - v.p1()) * (offset + QFontMetrics(self.title_item.font()).height()) else: title_pos -= (v.p2() - v.p1()) * offset ## TODO: Move it according to self.label_pos self.title_item.setVisible(self.show_title) self.title_item.setRotation(-self.graph_line.angle()) c = self.title_item.mapToParent(self.title_item.boundingRect().center()) tl = self.title_item.mapToParent(self.title_item.boundingRect().topLeft()) self.title_item.setPos(title_pos - c + tl) ## Arrows if not zoom_only: if self.start_arrow_item: self.scene().removeItem(self.start_arrow_item) self.start_arrow_item = None if self.end_arrow_item: self.scene().removeItem(self.end_arrow_item) self.end_arrow_item = None if self.arrows & AxisStart: if not zoom_only or not self.start_arrow_item: self.start_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.start_arrow_item.setPos(self.graph_line.p1()) self.start_arrow_item.setRotation(-self.graph_line.angle() + 180) self.start_arrow_item.setBrush(line_color) self.start_arrow_item.setPen(line_color) if self.arrows & AxisEnd: if not zoom_only or not self.end_arrow_item: self.end_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.end_arrow_item.setPos(self.graph_line.p2()) self.end_arrow_item.setRotation(-self.graph_line.angle()) self.end_arrow_item.setBrush(line_color) self.end_arrow_item.setPen(line_color) ## Labels n = len(self._ticks) resize_plot_item_list(self.label_items, n, QGraphicsTextItem, self) resize_plot_item_list(self.label_bg_items, n, QGraphicsRectItem, self) resize_plot_item_list(self.tick_items, n, QGraphicsLineItem, self) test_rect = QRectF(self.graph_line.p1(), self.graph_line.p2()).normalized() test_rect.adjust(-1, -1, 1, 1) n_v = self.graph_line.normalVector().unitVector() if self.title_above: n_p = n_v.p2() - n_v.p1() else: n_p = n_v.p1() - n_v.p2() l_v = self.graph_line.unitVector() l_p = l_v.p2() - l_v.p1() for i in range(n): pos, text, size, step = self._ticks[i] hs = 0.5 * step tick_pos = self.map_to_graph(pos) if not test_rect.contains(tick_pos): self.tick_items[i].setVisible(False) self.label_items[i].setVisible(False) continue item = self.label_items[i] item.setVisible(True) if not zoom_only: if self.id in XAxes or getattr(self, 'is_horizontal', False): item.setHtml('<center>' + Qt.escape(text.strip()) + '</center>') else: item.setHtml(Qt.escape(text.strip())) item.setTextWidth(-1) text_angle = 0 if dense_text: w = min(item.boundingRect().width(), self.max_text_width) item.setTextWidth(w) if self.title_above: label_pos = tick_pos + n_p * (w + self.text_margin) + l_p * item.boundingRect().height() / 2 else: label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect().height() / 2 text_angle = -90 if self.title_above else 90 else: w = min(item.boundingRect().width(), QLineF(self.map_to_graph(pos - hs), self.map_to_graph(pos + hs)).length()) label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect().height() / 2 item.setTextWidth(w) if not self.always_horizontal_text: if self.title_above: item.setRotation(-self.graph_line.angle() - text_angle) else: item.setRotation(self.graph_line.angle() - text_angle) item.setPos(label_pos) item.setDefaultTextColor(text_color) self.label_bg_items[i].setRect(item.boundingRect()) self.label_bg_items[i].setPen(QPen(Qt.NoPen)) self.label_bg_items[i].setBrush(self.plot.color(OWPalette.Canvas)) item = self.tick_items[i] item.setVisible(True) tick_line = QLineF(v) tick_line.translate(-tick_line.p1()) tick_line.setLength(size) if self.title_above: tick_line.setAngle(tick_line.angle() + 180) item.setLine(tick_line) item.setPen(line_color) item.setPos(self.map_to_graph(pos)) @staticmethod def make_title(label, unit=None): lab = '<i>' + label + '</i>' if unit: lab = lab + ' [' + unit + ']' return lab def set_line(self, line): self.graph_line = line self.update() def set_title(self, title): self.title = title self.update() def set_show_title(self, b): self.show_title = b self.update() def set_labels(self, labels, values): self.labels = labels self.values = values self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_scale(self, min, max, step_size): self.scale = (min, max, step_size) self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_tick_length(self, minor, medium, major): self.tick_length = (minor, medium, major) self.update() def map_to_graph(self, x): min, max = self.plot.bounds_for_axis(self.id) if min == max: return QPointF() line_point = self.graph_line.pointAt((x - min) / (max - min)) end_point = line_point * self.zoom_transform return self.projection(end_point, self.graph_line) @staticmethod def projection(point, line): norm = line.normalVector() norm.translate(point - norm.p1()) p = QPointF() type = line.intersect(norm, p) return p def continuous_labels(self): min, max, step = self.scale magnitude = log10(abs(max - min)) def paint(self, painter, option, widget): pass def boundingRect(self): return QRectF() def ticks(self): if not self._ticks: self.update_ticks() return self._ticks def bounds(self): if self._bounds: return self._bounds if self.labels: return -0.2, len(self.labels) - 0.8 elif self.scale: min, max, _step = self.scale return min, max elif self.auto_range: return self.auto_range else: return 0, 1 def set_bounds(self, value): self._bounds = value def should_be_expanded(self): self.update_ticks() return self.id in YAxes or self.always_horizontal_text or sum( len(t[1]) for t in self._ticks) * 12 > self.plot.width()