Skip to content

Commit 709dfdf

Browse files
authored
Merge pull request #1767 from VesnaT/boxplot_clickable
OWBoxPlot: Select and send selected data
2 parents 44c4b7a + 2a76c36 commit 709dfdf

File tree

4 files changed

+112
-20
lines changed

4 files changed

+112
-20
lines changed

Orange/data/filter.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ def __call__(self, inst):
286286
else:
287287
return value in self.values
288288

289+
def __eq__(self, other):
290+
return isinstance(other, FilterDiscrete) and \
291+
self.column == other.column and self.values == other.values
292+
289293

290294
class FilterContinuous(ValueFilter):
291295
"""
@@ -358,6 +362,11 @@ def __call__(self, inst):
358362
return True
359363
raise ValueError("invalid operator")
360364

365+
def __eq__(self, other):
366+
return isinstance(other, FilterContinuous) and \
367+
self.column == other.column and self.oper == other.oper and \
368+
self.oper == other.oper and self.oper == other.oper
369+
361370
def __str__(self):
362371
if isinstance(self.column, str):
363372
column = self.column

Orange/tests/test_filter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,18 @@ def test_str(self):
246246
flt.oper = -1
247247
self.assertEqual(str(flt), "invalid operator")
248248

249+
def test_eq(self):
250+
flt1 = FilterContinuous(1, FilterContinuous.Between, 1, 2)
251+
flt2 = FilterContinuous(1, FilterContinuous.Between, 1, 2)
252+
self.assertEqual(flt1, flt2)
253+
254+
255+
class TestFilterDiscrete(unittest.TestCase):
256+
def test_eq(self):
257+
flt1 = FilterDiscrete(1, None)
258+
flt2 = FilterDiscrete(1, None)
259+
self.assertEqual(flt1, flt2)
260+
249261

250262
class TestFilterString(unittest.TestCase):
251263

Orange/widgets/visualize/owboxplot.py

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
from scipy.stats import f_oneway, chisquare
1717

1818
import Orange.data
19+
from Orange.data.filter import FilterDiscrete, FilterContinuous, Values
1920
from Orange.statistics import contingency, distribution
2021

2122
from Orange.widgets import widget, gui
2223
from Orange.widgets.settings import (Setting, DomainContextHandler,
2324
ContextSetting)
2425
from Orange.widgets.utils.itemmodels import VariableListModel
26+
from Orange.widgets.utils.annotated_data import (create_annotated_table,
27+
ANNOTATED_DATA_SIGNAL_NAME)
28+
from Orange.widgets.widget import Default
2529

2630

2731
def compute_scale(min_, max_):
@@ -41,7 +45,7 @@ def compute_scale(min_, max_):
4145

4246

4347
class BoxData:
44-
def __init__(self, dist):
48+
def __init__(self, dist, attr, group_val_index=None, group_var=None):
4549
self.dist = dist
4650
self.n = n = np.sum(dist[1])
4751
if n == 0:
@@ -69,6 +73,17 @@ def __init__(self, dist):
6973
else:
7074
self.q25 = self.q75 = None
7175
self.median = q[1] if len(q) == 2 else None
76+
self.conditions = [FilterContinuous(attr, FilterContinuous.Between,
77+
self.q25, self.q75)]
78+
if group_val_index is not None:
79+
self.conditions.append(FilterDiscrete(group_var, [group_val_index]))
80+
81+
82+
class FilterGraphicsRectItem(QGraphicsRectItem):
83+
def __init__(self, conditions, *args):
84+
super().__init__(*args)
85+
self.filter = Values(conditions) if conditions else None
86+
self.setFlag(QGraphicsItem.ItemIsSelectable)
7287

7388

7489
class OWBoxPlot(widget.OWWidget):
@@ -103,11 +118,14 @@ class OWBoxPlot(widget.OWWidget):
103118
icon = "icons/BoxPlot.svg"
104119
priority = 100
105120
inputs = [("Data", Orange.data.Table, "set_data")]
121+
outputs = [("Selected Data", Orange.data.Table, Default),
122+
(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)]
106123

107124
#: Comparison types for continuous variables
108125
CompareNone, CompareMedians, CompareMeans = 0, 1, 2
109126

110127
settingsHandler = DomainContextHandler()
128+
conditions = ContextSetting([])
111129

112130
attribute = ContextSetting(None)
113131
order_by_importance = Setting(False)
@@ -117,6 +135,7 @@ class OWBoxPlot(widget.OWWidget):
117135
stattest = Setting(0)
118136
sig_threshold = Setting(0.05)
119137
stretched = Setting(True)
138+
auto_commit = Setting(True)
120139

121140
_sorting_criteria_attrs = {
122141
CompareNone: "", CompareMedians: "median", CompareMeans: "mean"
@@ -201,8 +220,12 @@ def __init__(self):
201220
callback=self.display_changed,
202221
sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum)).box
203222

223+
gui.auto_commit(self.controlArea, self, "auto_commit",
224+
"Send Selection", "Send Automatically")
225+
204226
gui.vBox(self.mainArea, addSpace=True)
205227
self.box_scene = QGraphicsScene()
228+
self.box_scene.selectionChanged.connect(self.commit)
206229
self.box_view = QGraphicsView(self.box_scene)
207230
self.box_view.setRenderHints(QPainter.Antialiasing |
208231
QPainter.TextAntialiasing |
@@ -258,6 +281,7 @@ def set_data(self, dataset):
258281
self.grouping_changed()
259282
else:
260283
self.reset_all_data()
284+
self.commit()
261285

262286
def apply_sorting(self):
263287
def compute_score(attr):
@@ -319,6 +343,13 @@ def grouping_changed(self):
319343
self.apply_sorting()
320344
self.attr_changed()
321345

346+
def select_box_items(self):
347+
temp_cond = self.conditions.copy()
348+
for box in self.box_scene.items():
349+
if isinstance(box, FilterGraphicsRectItem):
350+
box.setSelected(box.filter.conditions in
351+
[c.conditions for c in temp_cond])
352+
322353
def attr_changed(self):
323354
self.compute_box_data()
324355
self.update_display_box()
@@ -346,13 +377,14 @@ def compute_box_data(self):
346377
self.conts = contingency.get_contingency(
347378
dataset, attr, self.group_var)
348379
if self.is_continuous:
349-
self.stats = [BoxData(cont) for cont in self.conts]
380+
self.stats = [BoxData(cont, attr, i, self.group_var)
381+
for i, cont in enumerate(self.conts)]
350382
self.label_txts_all = self.group_var.values
351383
else:
352384
self.dist = distribution.get_distribution(dataset, attr)
353385
self.conts = []
354386
if self.is_continuous:
355-
self.stats = [BoxData(self.dist)]
387+
self.stats = [BoxData(self.dist, attr, None)]
356388
self.label_txts_all = [""]
357389
self.label_txts = [txts for stat, txts in zip(self.stats,
358390
self.label_txts_all)
@@ -369,12 +401,15 @@ def update_display_box(self):
369401
self.display_box.hide()
370402

371403
def clear_scene(self):
404+
self.closeContext()
405+
self.box_scene.clearSelection()
372406
self.box_scene.clear()
373407
self.attr_labels = []
374408
self.labels = []
375409
self.boxes = []
376410
self.mean_labels = []
377411
self.posthoc_lines = []
412+
self.openContext(self.dataset)
378413

379414
def layout_changed(self):
380415
attr = self.attribute
@@ -395,7 +430,7 @@ def layout_changed(self):
395430
for stat, mean_lab in zip(self.stats, self.mean_labels)]
396431
self.attr_labels = [QGraphicsSimpleTextItem(lab)
397432
for lab in self.label_txts]
398-
for it in chain(self.labels, self.boxes, self.attr_labels):
433+
for it in chain(self.labels, self.attr_labels):
399434
self.box_scene.addItem(it)
400435
self.display_changed()
401436

@@ -416,7 +451,9 @@ def display_changed(self):
416451

417452
for row, box_index in enumerate(self.order):
418453
y = (-len(self.stats) + row) * heights + 10
419-
self.boxes[box_index].setY(y)
454+
for item in self.boxes[box_index].childItems():
455+
self.box_scene.addItem(item)
456+
item.setY(y)
420457
labels = self.labels[box_index]
421458

422459
if self.show_annotations:
@@ -448,6 +485,7 @@ def display_changed(self):
448485

449486
self.compute_tests()
450487
self.show_posthoc()
488+
self.select_box_items()
451489

452490
def display_changed_disc(self):
453491
self.clear_scene()
@@ -465,7 +503,7 @@ def display_changed_disc(self):
465503

466504
self.draw_axis_disc()
467505
if self.group_var:
468-
self.boxes = [self.strudel(cont) for cont in self.conts]
506+
self.boxes = [self.strudel(cont, i) for i, cont in enumerate(self.conts)]
469507
else:
470508
self.boxes = [self.strudel(self.dist)]
471509

@@ -496,12 +534,14 @@ def display_changed_disc(self):
496534
self.box_scene.addItem(label)
497535
for text_item in box.childItems()[1::2]:
498536
box.removeFromGroup(text_item)
499-
self.box_scene.addItem(box)
500-
box.setPos(0, y)
537+
for item in box.childItems():
538+
self.box_scene.addItem(item)
539+
item.setPos(0, y)
501540
self.box_scene.setSceneRect(-self.label_width - 5,
502541
-30 - len(self.boxes) * 40,
503542
self.scene_width, len(self.boxes * 40) + 90)
504543
self.infot1.setText("")
544+
self.select_box_items()
505545

506546
# noinspection PyPep8Naming
507547
def compute_tests(self):
@@ -584,7 +624,7 @@ def mean_label(self, stat, attr, val_name):
584624
def draw_axis(self):
585625
"""Draw the horizontal axis and sets self.scale_x"""
586626
misssing_stats = not self.stats
587-
stats = self.stats or [BoxData(np.array([[0.], [1.]]))]
627+
stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]
588628
mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, "")]
589629
bottom = min(stat.a_min for stat in stats)
590630
top = max(stat.a_max for stat in stats)
@@ -763,9 +803,9 @@ def line(x0, y0, x1, y1, *args):
763803
var_line.setPen(self._pen_paramet)
764804

765805
if stat.q25 is not None and stat.q75 is not None:
766-
mbox = QGraphicsRectItem(stat.q25 * scale_x, -height / 2,
767-
(stat.q75 - stat.q25) * scale_x, height,
768-
box)
806+
mbox = FilterGraphicsRectItem(
807+
stat.conditions, stat.q25 * scale_x, -height / 2,
808+
(stat.q75 - stat.q25) * scale_x, height, box)
769809
mbox.setBrush(self._box_brush)
770810
mbox.setPen(QPen(Qt.NoPen))
771811
mbox.setZValue(-200)
@@ -778,20 +818,26 @@ def line(x0, y0, x1, y1, *args):
778818

779819
return box
780820

781-
def strudel(self, dist):
821+
def strudel(self, dist, group_val_index=None):
782822
attr = self.attribute
783823
ss = np.sum(dist)
784824
box = QGraphicsItemGroup()
785825
if ss < 1e-6:
786-
QGraphicsRectItem(0, -10, 1, 10, box)
826+
cond = [FilterDiscrete(attr, None)]
827+
if group_val_index is not None:
828+
cond.append(FilterDiscrete(self.group_var, [group_val_index]))
829+
FilterGraphicsRectItem(cond, 0, -10, 1, 10, box)
787830
cum = 0
788831
for i, v in enumerate(dist):
789832
if v < 1e-6:
790833
continue
791834
if self.stretched:
792835
v /= ss
793836
v *= self.scale_x
794-
rect = QGraphicsRectItem(cum + 1, -6, v - 2, 12, box)
837+
cond = [FilterDiscrete(attr, [i])]
838+
if group_val_index is not None:
839+
cond.append(FilterDiscrete(self.group_var, [group_val_index]))
840+
rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12, box)
795841
rect.setBrush(QBrush(QColor(*attr.colors[i])))
796842
rect.setPen(QPen(Qt.NoPen))
797843
if self.stretched:
@@ -805,6 +851,18 @@ def strudel(self, dist):
805851
cum += v
806852
return box
807853

854+
def commit(self):
855+
self.conditions = [item.filter for item in
856+
self.box_scene.selectedItems() if item.filter]
857+
selected, selection = None, []
858+
if self.conditions:
859+
selected = Values(self.conditions, conjunction=False)(self.dataset)
860+
selection = [i for i, inst in enumerate(self.dataset)
861+
if inst in selected]
862+
self.send("Selected Data", selected)
863+
self.send(ANNOTATED_DATA_SIGNAL_NAME,
864+
create_annotated_table(self.dataset, selection))
865+
808866
def show_posthoc(self):
809867
def line(y0, y1):
810868
it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)

Orange/widgets/visualize/tests/test_owboxplot.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
# Test methods with long descriptive names can omit docstrings
22
# pylint: disable=missing-docstring
3-
from unittest import skip
43

54
import numpy as np
5+
66
from Orange.data import Table, ContinuousVariable
7-
from Orange.widgets.visualize.owboxplot import OWBoxPlot
8-
from Orange.widgets.tests.base import WidgetTest
7+
from Orange.widgets.visualize.owboxplot import OWBoxPlot, FilterGraphicsRectItem
8+
from Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin
99

1010

11-
class OWBoxPlotTests(WidgetTest):
11+
class TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):
1212
@classmethod
1313
def setUpClass(cls):
1414
super().setUpClass()
15+
WidgetOutputsTestMixin.init(cls)
16+
1517
cls.iris = Table("iris")
1618
cls.zoo = Table("zoo")
1719
cls.housing = Table("housing")
1820
cls.titanic = Table("titanic")
1921
cls.heart = Table("heart_disease")
22+
cls.data = cls.iris
23+
cls.signal_name = "Data"
24+
cls.signal_data = cls.data
2025

2126
def setUp(self):
2227
self.widget = self.create_widget(OWBoxPlot)
@@ -36,7 +41,7 @@ def test_input_data(self):
3641

3742
def test_input_data_missings_cont_group_var(self):
3843
"""Check widget with continuous data with missing values and group variable"""
39-
data = self.iris
44+
data = self.iris.copy()
4045
data.X[:, 0] = np.nan
4146
self.send_signal("Data", data)
4247
# used to crash, see #1568
@@ -100,3 +105,11 @@ def select_group(i):
100105
'slope peak exc ST', 'gender', 'age', 'rest SBP',
101106
'rest ECG', 'cholesterol',
102107
'fasting blood sugar > 120', 'diameter narrowing'])
108+
109+
def _select_data(self):
110+
items = [item for item in self.widget.box_scene.items()
111+
if isinstance(item, FilterGraphicsRectItem)]
112+
items[0].setSelected(True)
113+
return [100, 103, 104, 108, 110, 111, 112, 115, 116,
114+
120, 123, 124, 126, 128, 132, 133, 136, 137,
115+
139, 140, 141, 143, 144, 145, 146, 147, 148]

0 commit comments

Comments
 (0)