Skip to content

Commit 89ac3bf

Browse files
committed
owtable: Add data subset input
1 parent 3bea541 commit 89ac3bf

File tree

3 files changed

+185
-43
lines changed

3 files changed

+185
-43
lines changed

Orange/widgets/data/owtable.py

Lines changed: 164 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import concurrent.futures
22
from dataclasses import dataclass
3-
from typing import Optional, Union, Sequence, List, TypedDict, Tuple, Dict
3+
from typing import (
4+
Optional, Union, Sequence, List, TypedDict, Tuple, Dict, Any, Container
5+
)
46

57
from scipy.sparse import issparse
68

7-
from AnyQt.QtWidgets import QTableView, QHeaderView, QApplication, QStyle
8-
from AnyQt.QtGui import QColor, QClipboard
9-
from AnyQt.QtCore import Qt, QSize, QMetaObject, QItemSelectionModel
9+
from AnyQt.QtWidgets import (
10+
QTableView, QHeaderView, QApplication, QStyle, QStyleOptionHeader,
11+
QStyleOptionViewItem
12+
)
13+
from AnyQt.QtGui import QColor, QClipboard, QPainter
14+
from AnyQt.QtCore import (
15+
Qt, QSize, QMetaObject, QItemSelectionModel, QModelIndex, QRect
16+
)
1017
from AnyQt.QtCore import Slot
1118

19+
from orangewidget.gui import OrangeUserRole
20+
1221
import Orange.data
1322
from Orange.data import Variable
1423
from Orange.data.table import Table
@@ -26,18 +35,130 @@
2635
from Orange.widgets.utils.itemmodels import TableModel
2736
from Orange.widgets.utils.state_summary import format_summary_details
2837
from Orange.widgets.utils import disconnected
38+
from Orange.widgets.utils.headerview import HeaderView
2939
from Orange.widgets.data.utils.tableview import RichTableView
3040
from Orange.widgets.data.utils import tablesummary as tsummary
3141

3242

43+
SubsetRole = next(OrangeUserRole)
44+
45+
46+
class HeaderViewWithSubsetIndicator(HeaderView):
47+
_IndicatorChar = "\N{BULLET}"
48+
49+
def paintSection(
50+
self, painter: QPainter, rect: QRect, logicalIndex: int
51+
) -> None:
52+
opt = QStyleOptionHeader()
53+
self.initStyleOption(opt)
54+
self.initStyleOptionForIndex(opt, logicalIndex)
55+
model = self.model()
56+
if model is None:
57+
return # pragma: no cover
58+
opt.rect = rect
59+
issubset = model.headerData(logicalIndex, Qt.Vertical, SubsetRole)
60+
style = self.style()
61+
# draw background
62+
style.drawControl(QStyle.CE_HeaderSection, opt, painter, self)
63+
indicator_rect = QRect(rect)
64+
text_rect = QRect(rect)
65+
indicator_width = opt.fontMetrics.horizontalAdvance(
66+
self._IndicatorChar + " "
67+
)
68+
indicator_rect.setWidth(indicator_width)
69+
text_rect.setLeft(indicator_width)
70+
if issubset:
71+
optindicator = QStyleOptionHeader(opt)
72+
optindicator.rect = indicator_rect
73+
optindicator.textAlignment = Qt.AlignCenter
74+
optindicator.text = self._IndicatorChar
75+
# draw subset indicator
76+
style.drawControl(QStyle.CE_HeaderLabel, optindicator, painter, self)
77+
opt.rect = text_rect
78+
# draw section label
79+
style.drawControl(QStyle.CE_HeaderLabel, opt, painter, self)
80+
81+
def sectionSizeFromContents(self, logicalIndex: int) -> QSize:
82+
opt = QStyleOptionHeader()
83+
self.initStyleOption(opt)
84+
super().initStyleOptionForIndex(opt, logicalIndex)
85+
opt.text = self._IndicatorChar + " " + opt.text
86+
return self.style().sizeFromContents(QStyle.CT_HeaderSection, opt, QSize(), self)
87+
88+
3389
class DataTableView(gui.HScrollStepMixin, RichTableView):
34-
pass
90+
def __init__(self, *args, **kwargs):
91+
super().__init__(*args, **kwargs)
92+
vheader = HeaderViewWithSubsetIndicator(
93+
Qt.Vertical, self, highlightSections=True
94+
)
95+
vheader.setSectionsClickable(True)
96+
self.setVerticalHeader(vheader)
97+
98+
99+
class _TableDataDelegate(TableDataDelegate):
100+
DefaultRoles = TableDataDelegate.DefaultRoles + (SubsetRole,)
101+
102+
103+
class SubsetTableDataDelegate(_TableDataDelegate):
104+
def __init__(self, *args, **kwargs):
105+
super().__init__(*args, **kwargs)
106+
self.subset_opacity = 0.5
35107

108+
def paint(
109+
self, painter: QPainter, option: QStyleOptionViewItem,
110+
index: QModelIndex
111+
) -> None:
112+
issubset = self.cachedData(index, SubsetRole)
113+
opacity = painter.opacity()
114+
if not issubset:
115+
painter.setOpacity(self.subset_opacity)
116+
super().paint(painter, option, index)
117+
if not issubset:
118+
painter.setOpacity(opacity)
36119

37-
class TableBarItemDelegate(gui.TableBarItem, TableDataDelegate):
120+
121+
class TableBarItemDelegate(SubsetTableDataDelegate, gui.TableBarItem,
122+
_TableDataDelegate):
38123
pass
39124

40125

126+
class _TableModel(RichTableModel):
127+
SubsetRole = SubsetRole
128+
129+
def __init__(self, *args, subsets=None, **kwargs):
130+
super().__init__(*args, **kwargs)
131+
self._subset = subsets or set()
132+
133+
def setSubsetRowIds(self, subsetids: Container[int]):
134+
self._subset = subsetids
135+
if self.rowCount():
136+
self.headerDataChanged.emit(Qt.Vertical, 0, self.rowCount() - 1)
137+
self.dataChanged.emit(
138+
self.index(0, 0),
139+
self.index(self.rowCount() - 1, self.columnCount() - 1),
140+
[SubsetRole],
141+
)
142+
143+
def _is_subset(self, row):
144+
row = self.mapToSourceRows(row)
145+
try:
146+
id_ = self.source.ids[row]
147+
except (IndexError, AttributeError): # pragma: no cover
148+
return False
149+
return int(id_) in self._subset
150+
151+
def data(self, index: QModelIndex, role=Qt.DisplayRole) -> Any:
152+
if role == _TableModel.SubsetRole:
153+
return self._is_subset(index.row())
154+
return super().data(index, role)
155+
156+
def headerData(self, section, orientation, role):
157+
if orientation == Qt.Vertical and role == _TableModel.SubsetRole:
158+
return self._is_subset(section)
159+
return super().headerData(section, orientation, role)
160+
161+
41162
@dataclass
42163
class InputData:
43164
table: Table
@@ -61,7 +182,8 @@ class OWTable(OWWidget):
61182
keywords = "data table, view"
62183

63184
class Inputs:
64-
data = Input("Data", Table)
185+
data = Input("Data", Table, default=True)
186+
data_subset = Input("Data Subset", Table)
65187

66188
class Outputs:
67189
selected_data = Output("Selected Data", Table, default=True)
@@ -95,6 +217,7 @@ class Warning(OWWidget.Warning):
95217
def __init__(self):
96218
super().__init__()
97219
self.input: Optional[InputData] = None
220+
self._subset_ids: Optional[set] = None
98221
self.__pending_selection: Optional[_Selection] = self.stored_selection
99222
self.__pending_sort: Optional[_Sorting] = self.stored_sort
100223
self.dist_color = QColor(220, 220, 220, 255)
@@ -128,11 +251,8 @@ def __init__(self):
128251
attribute=Qt.WA_LayoutUsesWidgetRect)
129252
gui.auto_send(self.buttonsArea, self, "auto_commit")
130253

131-
view = DataTableView(
132-
sortingEnabled=True
133-
)
134-
view.setSortingEnabled(True)
135-
view.setItemDelegate(TableDataDelegate(view))
254+
view = DataTableView(sortingEnabled=True)
255+
view.setItemDelegate(SubsetTableDataDelegate(view))
136256
view.selectionFinished.connect(self.update_selection)
137257

138258
if self.select_rows:
@@ -164,27 +284,35 @@ def set_dataset(self, data: Optional[Table]):
164284
self.view.setModel(None)
165285
self.view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder)
166286
if data is not None:
287+
summary = tsummary.table_summary(data)
167288
self.input = InputData(
168289
table=data,
169-
summary=tsummary.table_summary(data),
170-
model=RichTableModel(data)
290+
summary=summary,
291+
model=_TableModel(data)
171292
)
172-
self._setup_table_view()
293+
if isinstance(summary.len, concurrent.futures.Future):
294+
def update(_):
295+
QMetaObject.invokeMethod(
296+
self, "_update_info", Qt.QueuedConnection)
297+
summary.len.add_done_callback(update)
173298
else:
174299
self.input = None
175300

301+
@Inputs.data_subset
302+
def set_subset_dataset(self, subset: Optional[Table]):
303+
"""Set the data subset"""
304+
if subset is not None and not isinstance(subset, SqlTable):
305+
ids = set(subset.ids)
306+
else:
307+
ids = None
308+
self._subset_ids = ids
309+
176310
def handleNewSignals(self):
177311
super().handleNewSignals()
178312
self.Warning.non_sortable_input.clear()
179313
self.Warning.missing_sort_columns.clear()
180314
data: Optional[Table] = self.input.table if self.input else None
181-
slot = self.input
182-
if slot is not None and isinstance(slot.summary.len, concurrent.futures.Future):
183-
def update(_):
184-
QMetaObject.invokeMethod(
185-
self, "_update_info", Qt.QueuedConnection)
186-
slot.summary.len.add_done_callback(update)
187-
315+
self._setup_table_view()
188316
self._update_input_summary()
189317

190318
if data is not None and self.__pending_sort is not None:
@@ -205,23 +333,12 @@ def _setup_table_view(self):
205333
return
206334

207335
datamodel = self.input.model
336+
datamodel.setSubsetRowIds(self._subset_ids or set())
337+
208338
view = self.view
209339
data = self.input.table
210340
rowcount = data.approx_len()
211341

212-
if self.color_by_class and data.domain.has_discrete_class:
213-
color_schema = [
214-
QColor(*c) for c in data.domain.class_var.colors]
215-
else:
216-
color_schema = None
217-
if self.show_distributions:
218-
view.setItemDelegate(
219-
TableBarItemDelegate(
220-
view, color=self.dist_color, color_schema=color_schema)
221-
)
222-
else:
223-
view.setItemDelegate(TableDataDelegate(view))
224-
225342
view.setModel(datamodel)
226343

227344
vheader = view.verticalHeader()
@@ -249,6 +366,7 @@ def _setup_table_view(self):
249366
assert view.model().rowCount() <= maxrows
250367
assert vheader.sectionSize(0) > 1 or datamodel.rowCount() == 0
251368

369+
self._setup_view_delegate()
252370
# update the header (attribute names)
253371
self._update_variable_labels()
254372

@@ -287,8 +405,11 @@ def _update_variable_labels(self):
287405

288406
def _on_distribution_color_changed(self):
289407
if self.input is None:
290-
return
291-
widget = self.view
408+
return # pragma: no cover
409+
self._setup_view_delegate()
410+
411+
def _setup_view_delegate(self):
412+
assert self.input is not None
292413
model = self.input.model
293414
data = model.source
294415
class_var = data.domain.class_var
@@ -297,11 +418,13 @@ def _on_distribution_color_changed(self):
297418
else:
298419
color_schema = None
299420
if self.show_distributions:
300-
delegate = TableBarItemDelegate(widget, color=self.dist_color,
301-
color_schema=color_schema)
421+
delegate = TableBarItemDelegate(
422+
self.view, color=self.dist_color, color_schema=color_schema
423+
)
302424
else:
303-
delegate = TableDataDelegate(widget)
304-
widget.setItemDelegate(delegate)
425+
delegate = SubsetTableDataDelegate(self.view)
426+
delegate.subset_opacity = 0.5 if self._subset_ids is not None else 1.0
427+
self.view.setItemDelegate(delegate)
305428

306429
def _on_select_rows_changed(self):
307430
if self.input is None:

Orange/widgets/data/tests/test_owtable.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,25 @@ def test_show_attribute_labels(self):
199199
w.controls.show_attribute_labels.toggle()
200200
self.assertFalse(w.show_attribute_labels)
201201

202+
def test_subset_input(self):
203+
w = self.widget
204+
self.send_signal(w.Inputs.data, self.data)
205+
self.send_signal(w.Inputs.data_subset, self.data[[0, 1, 5]])
206+
w.view.grab() # cover delegate painting methods
207+
208+
model = w.view.model()
209+
self.assertTrue(model.index(0, 0).data(model.SubsetRole))
210+
self.assertFalse(model.index(2, 0).data(model.SubsetRole))
211+
self.assertTrue(model.headerData(0, Qt.Vertical, model.SubsetRole))
212+
self.assertFalse(model.headerData(2, Qt.Vertical, model.SubsetRole))
213+
214+
self.send_signal(w.Inputs.data_subset, None)
215+
w.view.grab()
216+
217+
model = w.view.model()
218+
self.assertFalse(model.index(0, 0).data(model.SubsetRole))
219+
self.assertFalse(model.headerData(0, Qt.Vertical, model.SubsetRole))
220+
202221

203222
class TestOWTableSQL(TestOWTable, dbt):
204223
def setUpDB(self):

Orange/widgets/utils/itemdelegates.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import math
2-
from typing import Optional, Tuple
2+
from typing import Optional, Tuple, ClassVar
33

44
from AnyQt.QtCore import QModelIndex, QSize, Qt
55
from AnyQt.QtWidgets import QStyle, QStyleOptionViewItem, QApplication
@@ -101,7 +101,7 @@ class TableDataDelegate(DataDelegate):
101101
:class:`Orange.widgets.utils.itemmodels.TableModel`
102102
"""
103103
#: Roles supplied by TableModel we want DataDelegate to use.
104-
DefaultRoles = (
104+
DefaultRoles: ClassVar[Tuple[int, ...]] = (
105105
Qt.DisplayRole, Qt.TextAlignmentRole, Qt.BackgroundRole,
106106
Qt.ForegroundRole
107107
)

0 commit comments

Comments
 (0)