diff --git a/Orange/widgets/visualize/owboxplot.py b/Orange/widgets/visualize/owboxplot.py index 42359e8df22..dcf351939c5 100644 --- a/Orange/widgets/visualize/owboxplot.py +++ b/Orange/widgets/visualize/owboxplot.py @@ -541,39 +541,91 @@ def display_changed_disc(self): for row, box in enumerate(self.boxes): y = (-len(self.boxes) + row) * 40 + 10 + bars, labels = box[::2], box[1::2] - label = self.attr_labels[row] - b = label.boundingRect() - label.setPos(-b.width() - 10, y - b.height() / 2) - self.box_scene.addItem(label) + self.__draw_group_labels(y, row) if not self.stretched: - 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) - + self.__draw_row_counts(y, row) if self.show_labels and self.attribute is not self.group_var: - for text_item, bar_part in zip(box[1::2], box[::2]): - label = QGraphicsSimpleTextItem( - text_item.toPlainText()) - label.setPos(bar_part.boundingRect().x(), - y - label.boundingRect().height() - 8) - self.box_scene.addItem(label) - for item in box: - if isinstance(item, QGraphicsTextItem): - continue - self.box_scene.addItem(item) - item.setPos(0, y) + 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 @@ -972,6 +1024,44 @@ def send_report(self): 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 main(argv=None): from AnyQt.QtWidgets import QApplication diff --git a/Orange/widgets/visualize/tests/test_owboxplot.py b/Orange/widgets/visualize/tests/test_owboxplot.py index a5d652c8d5e..7825e98bb1a 100644 --- a/Orange/widgets/visualize/tests/test_owboxplot.py +++ b/Orange/widgets/visualize/tests/test_owboxplot.py @@ -2,6 +2,8 @@ # pylint: disable=missing-docstring import numpy as np +from AnyQt.QtCore import QItemSelectionModel +from AnyQt.QtTest import QTest from Orange.data import Table, ContinuousVariable, StringVariable, Domain from Orange.widgets.visualize.owboxplot import OWBoxPlot, FilterGraphicsRectItem @@ -88,11 +90,6 @@ def test_attribute_combinations(self): m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect) self._select_list_items(self.widget.controls.attribute) - def _select_list_items(self, _list): - model = _list.selectionModel() - for i in range(len(_list.model())): - model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect) - def test_apply_sorting(self): controls = self.widget.controls group_list = controls.group_var @@ -148,6 +145,23 @@ def test_saved_selection(self): np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X, self.data.X[selected_indices]) + def test_continuous_metas(self): + domain = self.iris.domain + metas = domain.attributes[:-1] + (StringVariable("str"),) + domain = Domain([], domain.class_var, metas) + data = Table.from_table(domain, self.iris) + self.send_signal(self.widget.Inputs.data, data) + self.widget.controls.order_by_importance.setChecked(True) + + def test_label_overlap(self): + self.send_signal(self.widget.Inputs.data, self.heart) + self.widget.stretched = False + self.__select_variable("chest pain") + self.__select_group("gender") + self.widget.show() + QTest.qWait(3000) + self.widget.hide() + def _select_data(self): items = [item for item in self.widget.box_scene.items() if isinstance(item, FilterGraphicsRectItem)] @@ -156,10 +170,27 @@ def _select_data(self): 120, 123, 124, 126, 128, 132, 133, 136, 137, 139, 140, 141, 143, 144, 145, 146, 147, 148] - def test_continuous_metas(self): - domain = self.iris.domain - metas = domain.attributes[:-1] + (StringVariable("str"),) - domain = Domain([], domain.class_var, metas) - data = Table.from_table(domain, self.iris) - self.send_signal(self.widget.Inputs.data, data) - self.widget.controls.order_by_importance.setChecked(True) + def _select_list_items(self, _list): + model = _list.selectionModel() + for i in range(len(_list.model())): + model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect) + + def __select_variable(self, name, widget=None): + if widget is None: + widget = self.widget + + self.__select_value(widget.controls.attribute, name) + + def __select_group(self, name, widget=None): + if widget is None: + widget = self.widget + + self.__select_value(widget.controls.group_var, name) + + def __select_value(self, list, value): + m = list.model() + for i in range(m.rowCount()): + idx = m.index(i) + if m.data(idx) == value: + list.selectionModel().setCurrentIndex( + idx, QItemSelectionModel.ClearAndSelect)