Skip to content

Commit

Permalink
Merge pull request #3011 from astaric/boxplot-label-overlap
Browse files Browse the repository at this point in the history
[FIX] boxplot labels overlap
  • Loading branch information
ajdapretnar authored May 11, 2018
2 parents 422bc94 + d3ae1f5 commit b62d04f
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 36 deletions.
138 changes: 114 additions & 24 deletions Orange/widgets/visualize/owboxplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
55 changes: 43 additions & 12 deletions Orange/widgets/visualize/tests/test_owboxplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand All @@ -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)

0 comments on commit b62d04f

Please sign in to comment.