Skip to content

Commit b62d04f

Browse files
authored
Merge pull request #3011 from astaric/boxplot-label-overlap
[FIX] boxplot labels overlap
2 parents 422bc94 + d3ae1f5 commit b62d04f

File tree

2 files changed

+157
-36
lines changed

2 files changed

+157
-36
lines changed

Orange/widgets/visualize/owboxplot.py

Lines changed: 114 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -541,39 +541,91 @@ def display_changed_disc(self):
541541

542542
for row, box in enumerate(self.boxes):
543543
y = (-len(self.boxes) + row) * 40 + 10
544+
bars, labels = box[::2], box[1::2]
544545

545-
label = self.attr_labels[row]
546-
b = label.boundingRect()
547-
label.setPos(-b.width() - 10, y - b.height() / 2)
548-
self.box_scene.addItem(label)
546+
self.__draw_group_labels(y, row)
549547
if not self.stretched:
550-
label = self.labels[row]
551-
b = label.boundingRect()
552-
if self.group_var:
553-
right = self.scale_x * sum(self.conts[row])
554-
else:
555-
right = self.scale_x * sum(self.dist)
556-
label.setPos(right + 10, y - b.height() / 2)
557-
self.box_scene.addItem(label)
558-
548+
self.__draw_row_counts(y, row)
559549
if self.show_labels and self.attribute is not self.group_var:
560-
for text_item, bar_part in zip(box[1::2], box[::2]):
561-
label = QGraphicsSimpleTextItem(
562-
text_item.toPlainText())
563-
label.setPos(bar_part.boundingRect().x(),
564-
y - label.boundingRect().height() - 8)
565-
self.box_scene.addItem(label)
566-
for item in box:
567-
if isinstance(item, QGraphicsTextItem):
568-
continue
569-
self.box_scene.addItem(item)
570-
item.setPos(0, y)
550+
self.__draw_bar_labels(y, bars, labels)
551+
self.__draw_bars(y, bars)
552+
571553
self.box_scene.setSceneRect(-self.label_width - 5,
572554
-30 - len(self.boxes) * 40,
573555
self.scene_width, len(self.boxes * 40) + 90)
574556
self.infot1.setText("")
575557
self.select_box_items()
576558

559+
def __draw_group_labels(self, y, row):
560+
"""Draw group labels
561+
562+
Parameters
563+
----------
564+
y: int
565+
vertical offset of bars
566+
row: int
567+
row index
568+
"""
569+
label = self.attr_labels[row]
570+
b = label.boundingRect()
571+
label.setPos(-b.width() - 10, y - b.height() / 2)
572+
self.box_scene.addItem(label)
573+
574+
def __draw_row_counts(self, y, row):
575+
"""Draw row counts
576+
577+
Parameters
578+
----------
579+
y: int
580+
vertical offset of bars
581+
row: int
582+
row index
583+
"""
584+
label = self.labels[row]
585+
b = label.boundingRect()
586+
if self.group_var:
587+
right = self.scale_x * sum(self.conts[row])
588+
else:
589+
right = self.scale_x * sum(self.dist)
590+
label.setPos(right + 10, y - b.height() / 2)
591+
self.box_scene.addItem(label)
592+
593+
def __draw_bar_labels(self, y, bars, labels):
594+
"""Draw bar labels
595+
596+
Parameters
597+
----------
598+
y: int
599+
vertical offset of bars
600+
bars: List[FilterGraphicsRectItem]
601+
list of bars being drawn
602+
labels: List[QGraphicsTextItem]
603+
list of labels for corresponding bars
604+
"""
605+
label = bar_part = None
606+
for text_item, bar_part in zip(labels, bars):
607+
label = self.Label(
608+
text_item.toPlainText())
609+
label.setPos(bar_part.boundingRect().x(),
610+
y - label.boundingRect().height() - 8)
611+
label.setMaxWidth(bar_part.boundingRect().width())
612+
self.box_scene.addItem(label)
613+
614+
def __draw_bars(self, y, bars):
615+
"""Draw bars
616+
617+
Parameters
618+
----------
619+
y: int
620+
vertical offset of bars
621+
622+
bars: List[FilterGraphicsRectItem]
623+
list of bars to draw
624+
"""
625+
for item in bars:
626+
item.setPos(0, y)
627+
self.box_scene.addItem(item)
628+
577629
# noinspection PyPep8Naming
578630
def compute_tests(self):
579631
# The t-test and ANOVA are implemented here since they efficiently use
@@ -972,6 +1024,44 @@ def send_report(self):
9721024
if text:
9731025
self.report_caption(text)
9741026

1027+
class Label(QGraphicsSimpleTextItem):
1028+
"""Boxplot Label with settable maxWidth"""
1029+
# Minimum width to display label text
1030+
MIN_LABEL_WIDTH = 25
1031+
1032+
# padding bellow the text
1033+
PADDING = 3
1034+
1035+
__max_width = None
1036+
1037+
def maxWidth(self):
1038+
return self.__max_width
1039+
1040+
def setMaxWidth(self, max_width):
1041+
self.__max_width = max_width
1042+
1043+
def paint(self, painter, option, widget):
1044+
"""Overrides QGraphicsSimpleTextItem.paint
1045+
1046+
If label text is too long, it is elided
1047+
to fit into the allowed region
1048+
"""
1049+
if self.__max_width is None:
1050+
width = option.rect.width()
1051+
else:
1052+
width = self.__max_width
1053+
1054+
if width < self.MIN_LABEL_WIDTH:
1055+
# if space is too narrow, no label
1056+
return
1057+
1058+
fm = painter.fontMetrics()
1059+
text = fm.elidedText(self.text(), Qt.ElideRight, width)
1060+
painter.drawText(
1061+
option.rect.x(),
1062+
option.rect.y() + self.boundingRect().height() - self.PADDING,
1063+
text)
1064+
9751065

9761066
def main(argv=None):
9771067
from AnyQt.QtWidgets import QApplication

Orange/widgets/visualize/tests/test_owboxplot.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# pylint: disable=missing-docstring
33

44
import numpy as np
5+
from AnyQt.QtCore import QItemSelectionModel
6+
from AnyQt.QtTest import QTest
57

68
from Orange.data import Table, ContinuousVariable, StringVariable, Domain
79
from Orange.widgets.visualize.owboxplot import OWBoxPlot, FilterGraphicsRectItem
@@ -88,11 +90,6 @@ def test_attribute_combinations(self):
8890
m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect)
8991
self._select_list_items(self.widget.controls.attribute)
9092

91-
def _select_list_items(self, _list):
92-
model = _list.selectionModel()
93-
for i in range(len(_list.model())):
94-
model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)
95-
9693
def test_apply_sorting(self):
9794
controls = self.widget.controls
9895
group_list = controls.group_var
@@ -148,6 +145,23 @@ def test_saved_selection(self):
148145
np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X,
149146
self.data.X[selected_indices])
150147

148+
def test_continuous_metas(self):
149+
domain = self.iris.domain
150+
metas = domain.attributes[:-1] + (StringVariable("str"),)
151+
domain = Domain([], domain.class_var, metas)
152+
data = Table.from_table(domain, self.iris)
153+
self.send_signal(self.widget.Inputs.data, data)
154+
self.widget.controls.order_by_importance.setChecked(True)
155+
156+
def test_label_overlap(self):
157+
self.send_signal(self.widget.Inputs.data, self.heart)
158+
self.widget.stretched = False
159+
self.__select_variable("chest pain")
160+
self.__select_group("gender")
161+
self.widget.show()
162+
QTest.qWait(3000)
163+
self.widget.hide()
164+
151165
def _select_data(self):
152166
items = [item for item in self.widget.box_scene.items()
153167
if isinstance(item, FilterGraphicsRectItem)]
@@ -156,10 +170,27 @@ def _select_data(self):
156170
120, 123, 124, 126, 128, 132, 133, 136, 137,
157171
139, 140, 141, 143, 144, 145, 146, 147, 148]
158172

159-
def test_continuous_metas(self):
160-
domain = self.iris.domain
161-
metas = domain.attributes[:-1] + (StringVariable("str"),)
162-
domain = Domain([], domain.class_var, metas)
163-
data = Table.from_table(domain, self.iris)
164-
self.send_signal(self.widget.Inputs.data, data)
165-
self.widget.controls.order_by_importance.setChecked(True)
173+
def _select_list_items(self, _list):
174+
model = _list.selectionModel()
175+
for i in range(len(_list.model())):
176+
model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)
177+
178+
def __select_variable(self, name, widget=None):
179+
if widget is None:
180+
widget = self.widget
181+
182+
self.__select_value(widget.controls.attribute, name)
183+
184+
def __select_group(self, name, widget=None):
185+
if widget is None:
186+
widget = self.widget
187+
188+
self.__select_value(widget.controls.group_var, name)
189+
190+
def __select_value(self, list, value):
191+
m = list.model()
192+
for i in range(m.rowCount()):
193+
idx = m.index(i)
194+
if m.data(idx) == value:
195+
list.selectionModel().setCurrentIndex(
196+
idx, QItemSelectionModel.ClearAndSelect)

0 commit comments

Comments
 (0)