Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] boxplot labels overlap #3011

Merged
merged 6 commits into from
May 11, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 124 additions & 25 deletions Orange/widgets/visualize/owboxplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
QGraphicsPathItem, QGraphicsRectItem, QSizePolicy
)
from AnyQt.QtGui import QPen, QColor, QBrush, QPainterPath, QPainter, QFont
from AnyQt.QtCore import Qt, QEvent, QRectF, QSize
from AnyQt.QtCore import Qt, QEvent, QRectF, QSize, QRect

import scipy.special
from scipy.stats import f_oneway, chisquare
Expand Down Expand Up @@ -541,39 +541,94 @@ 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)
if label is not None and bar_part.boundingRect().width() >= label.MIN_LABEL_WIDTH:
# last label in row can extend beyond its bar
label.setMaxWidth(None)

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 +1027,50 @@ def send_report(self):
if text:
self.report_caption(text)

class Label(QGraphicsSimpleTextItem):
"""Boxplot Label with settable maxSize"""
# Minimum width to display label text
MIN_LABEL_WIDTH = 25

# amount of whitespace between the cut point on the label and
# the right bar boundary
RIGHT_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 text is wider than maxWidth, a white rect is drawn to obscure
letters that extend beyond allowed width.
"""
super().paint(painter, option, widget)

if self.__max_width is None or self.boundingRect().width() < self.__max_width:
return

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ales-erjavec Do you perhaps have any ideas how to limit label painting to some rect? So that we would not need to draw white rects over label parts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • set clip rect on the painter
  • actually implement bounded text painting (QPainter has quite a few drawText overloads for bounded text painting, also see QFontMetrics.elidedText)

if self.__max_width < self.MIN_LABEL_WIDTH:
# hide text for narrow labels
self.__draw_white_rect(painter, option.rect)
else:
# hide text extending over the boundary
r = QRect(option.rect)
r.setX(r.x() + self.__max_width - self.RIGHT_PADDING)
self.__draw_white_rect(painter, r)

def __draw_white_rect(self, painter, rect):
painter.save()
painter.setBrush(Qt.white)
painter.setPen(Qt.white)
painter.drawRect(rect)
painter.restore()


def main(argv=None):
from AnyQt.QtWidgets import QApplication
Expand Down
53 changes: 41 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,7 @@
# pylint: disable=missing-docstring

import numpy as np
from AnyQt.QtCore import QItemSelectionModel

from Orange.data import Table, ContinuousVariable, StringVariable, Domain
from Orange.widgets.visualize.owboxplot import OWBoxPlot, FilterGraphicsRectItem
Expand Down Expand Up @@ -88,11 +89,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 +144,22 @@ 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()
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 +168,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)