11import concurrent .futures
22from dataclasses import dataclass
3- from typing import Optional , Union , Sequence , List , TypedDict , Tuple
3+ from typing import (
4+ Optional , Union , Sequence , List , TypedDict , Tuple , Any , Container
5+ )
46
57from 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+ )
1017from AnyQt .QtCore import Slot
1118
19+ from orangewidget .gui import OrangeUserRole
20+
1221import Orange .data
1322from Orange .data .table import Table
1423from Orange .data .sql .table import SqlTable
2534from Orange .widgets .utils .itemmodels import TableModel
2635from Orange .widgets .utils .state_summary import format_summary_details
2736from Orange .widgets .utils import disconnected
37+ from Orange .widgets .utils .headerview import HeaderView
2838from Orange .widgets .data .utils .tableview import RichTableView
2939from Orange .widgets .data .utils import tablesummary as tsummary
3040
3141
42+ SubsetRole = next (OrangeUserRole )
43+
44+
45+ class HeaderViewWithSubsetIndicator (HeaderView ):
46+ _IndicatorChar = "\N{BULLET} "
47+
48+ def paintSection (
49+ self , painter : QPainter , rect : QRect , logicalIndex : int
50+ ) -> None :
51+ opt = QStyleOptionHeader ()
52+ self .initStyleOption (opt )
53+ self .initStyleOptionForIndex (opt , logicalIndex )
54+ model = self .model ()
55+ if model is None :
56+ return # pragma: no cover
57+ opt .rect = rect
58+ issubset = model .headerData (logicalIndex , Qt .Vertical , SubsetRole )
59+ style = self .style ()
60+ # draw background
61+ style .drawControl (QStyle .CE_HeaderSection , opt , painter , self )
62+ indicator_rect = QRect (rect )
63+ text_rect = QRect (rect )
64+ indicator_width = opt .fontMetrics .horizontalAdvance (
65+ self ._IndicatorChar + " "
66+ )
67+ indicator_rect .setWidth (indicator_width )
68+ text_rect .setLeft (indicator_width )
69+ if issubset :
70+ optindicator = QStyleOptionHeader (opt )
71+ optindicator .rect = indicator_rect
72+ optindicator .textAlignment = Qt .AlignCenter
73+ optindicator .text = self ._IndicatorChar
74+ # draw subset indicator
75+ style .drawControl (QStyle .CE_HeaderLabel , optindicator , painter , self )
76+ opt .rect = text_rect
77+ # draw section label
78+ style .drawControl (QStyle .CE_HeaderLabel , opt , painter , self )
79+
80+ def sectionSizeFromContents (self , logicalIndex : int ) -> QSize :
81+ opt = QStyleOptionHeader ()
82+ self .initStyleOption (opt )
83+ super ().initStyleOptionForIndex (opt , logicalIndex )
84+ opt .text = self ._IndicatorChar + " " + opt .text
85+ return self .style ().sizeFromContents (QStyle .CT_HeaderSection , opt , QSize (), self )
86+
87+
3288class DataTableView (gui .HScrollStepMixin , RichTableView ):
33- pass
89+ def __init__ (self , * args , ** kwargs ):
90+ super ().__init__ (* args , ** kwargs )
91+ vheader = HeaderViewWithSubsetIndicator (
92+ Qt .Vertical , self , highlightSections = True
93+ )
94+ vheader .setSectionsClickable (True )
95+ self .setVerticalHeader (vheader )
96+
3497
98+ class _TableDataDelegate (TableDataDelegate ):
99+ DefaultRoles = TableDataDelegate .DefaultRoles + (SubsetRole ,)
35100
36- class TableBarItemDelegate (gui .TableBarItem , TableDataDelegate ):
101+
102+ class SubsetTableDataDelegate (_TableDataDelegate ):
103+ def __init__ (self , * args , ** kwargs ):
104+ super ().__init__ (* args , ** kwargs )
105+ self .subset_opacity = 0.5
106+
107+ def paint (
108+ self , painter : QPainter , option : QStyleOptionViewItem ,
109+ index : QModelIndex
110+ ) -> None :
111+ issubset = self .cachedData (index , SubsetRole )
112+ opacity = painter .opacity ()
113+ if not issubset :
114+ painter .setOpacity (self .subset_opacity )
115+ super ().paint (painter , option , index )
116+ if not issubset :
117+ painter .setOpacity (opacity )
118+
119+
120+ class TableBarItemDelegate (SubsetTableDataDelegate , gui .TableBarItem ,
121+ _TableDataDelegate ):
37122 pass
38123
39124
125+ class _TableModel (RichTableModel ):
126+ SubsetRole = SubsetRole
127+
128+ def __init__ (self , * args , subsets = None , ** kwargs ):
129+ super ().__init__ (* args , ** kwargs )
130+ self ._subset = subsets or set ()
131+
132+ def setSubsetRowIds (self , subsetids : Container [int ]):
133+ self ._subset = subsetids
134+ if self .rowCount ():
135+ self .headerDataChanged .emit (Qt .Vertical , 0 , self .rowCount () - 1 )
136+ self .dataChanged .emit (
137+ self .index (0 , 0 ),
138+ self .index (self .rowCount () - 1 , self .columnCount () - 1 ),
139+ [SubsetRole ],
140+ )
141+
142+ def _is_subset (self , row ):
143+ row = self .mapToSourceRows (row )
144+ try :
145+ id_ = self .source .ids [row ]
146+ except (IndexError , AttributeError ): # pragma: no cover
147+ return False
148+ return int (id_ ) in self ._subset
149+
150+ def data (self , index : QModelIndex , role = Qt .DisplayRole ) -> Any :
151+ if role == _TableModel .SubsetRole :
152+ return self ._is_subset (index .row ())
153+ return super ().data (index , role )
154+
155+ def headerData (self , section , orientation , role ):
156+ if orientation == Qt .Vertical and role == _TableModel .SubsetRole :
157+ return self ._is_subset (section )
158+ return super ().headerData (section , orientation , role )
159+
160+
40161@dataclass
41162class InputData :
42163 table : Table
@@ -60,7 +181,8 @@ class OWTable(OWWidget):
60181 keywords = "data table, view"
61182
62183 class Inputs :
63- data = Input ("Data" , Table )
184+ data = Input ("Data" , Table , default = True )
185+ data_subset = Input ("Data Subset" , Table )
64186
65187 class Outputs :
66188 selected_data = Output ("Selected Data" , Table , default = True )
@@ -94,8 +216,11 @@ class Warning(OWWidget.Warning):
94216 def __init__ (self ):
95217 super ().__init__ ()
96218 self .input : Optional [InputData ] = None
219+ self ._subset_ids : Optional [set ] = None
97220 self .__pending_selection : Optional [_Selection ] = self .stored_selection
98221 self .__pending_sort : Optional [_Sorting ] = self .stored_sort
222+ self .__have_new_data = False
223+ self .__have_new_subset = False
99224 self .dist_color = QColor (220 , 220 , 220 , 255 )
100225
101226 info_box = gui .vBox (self .controlArea , "Info" )
@@ -127,11 +252,8 @@ def __init__(self):
127252 attribute = Qt .WA_LayoutUsesWidgetRect )
128253 gui .auto_send (self .buttonsArea , self , "auto_commit" )
129254
130- view = DataTableView (
131- sortingEnabled = True
132- )
133- view .setSortingEnabled (True )
134- view .setItemDelegate (TableDataDelegate (view ))
255+ view = DataTableView (sortingEnabled = True )
256+ view .setItemDelegate (SubsetTableDataDelegate (view ))
135257 view .selectionFinished .connect (self .update_selection )
136258
137259 if self .select_rows :
@@ -163,39 +285,61 @@ def set_dataset(self, data: Optional[Table]):
163285 self .view .setModel (None )
164286 self .view .horizontalHeader ().setSortIndicator (- 1 , Qt .AscendingOrder )
165287 if data is not None :
288+ summary = tsummary .table_summary (data )
166289 self .input = InputData (
167290 table = data ,
168- summary = tsummary . table_summary ( data ) ,
169- model = RichTableModel (data )
291+ summary = summary ,
292+ model = _TableModel (data )
170293 )
171- self ._setup_table_view ()
294+ if isinstance (summary .len , concurrent .futures .Future ):
295+ def update (_ ):
296+ QMetaObject .invokeMethod (
297+ self , "_update_info" , Qt .QueuedConnection )
298+ summary .len .add_done_callback (update )
172299 else :
173300 self .input = None
301+ self .__have_new_data = True
302+
303+ @Inputs .data_subset
304+ def set_subset_dataset (self , subset : Optional [Table ]):
305+ """Set the data subset"""
306+ if subset is not None and not isinstance (subset , SqlTable ):
307+ ids = set (subset .ids )
308+ else :
309+ ids = None
310+ self ._subset_ids = ids
311+ self .__have_new_subset = True
174312
175313 def handleNewSignals (self ):
176314 super ().handleNewSignals ()
177315 self .Warning .non_sortable_input .clear ()
178316 self .Warning .missing_sort_columns .clear ()
179317 data : Optional [Table ] = self .input .table if self .input else None
180- slot = self .input
181- if slot is not None and isinstance (slot .summary .len , concurrent .futures .Future ):
182- def update (_ ):
183- QMetaObject .invokeMethod (
184- self , "_update_info" , Qt .QueuedConnection )
185- slot .summary .len .add_done_callback (update )
318+ model = self .input .model if self .input else None
186319
187- self ._update_input_summary ()
320+ if self .__have_new_data :
321+ self ._setup_table_view ()
322+ self ._update_input_summary ()
323+
324+ if data is not None and self .__pending_sort is not None :
325+ self .__restore_sort ()
326+
327+ if data is not None and self .__pending_selection is not None :
328+ selection = self .__pending_selection
329+ self .__pending_selection = None
330+ rows = selection ["rows" ]
331+ columns = selection ["columns" ]
332+ self .set_selection (rows , columns )
188333
189- if data is not None and self .__pending_sort is not None :
190- self .__restore_sort ()
334+ if self .__have_new_subset and model is not None :
335+ model .setSubsetRowIds (self ._subset_ids or set ())
336+ self .__have_new_subset = False
191337
192- if data is not None and self .__pending_selection is not None :
193- selection = self .__pending_selection
194- self .__pending_selection = None
195- rows = selection ["rows" ]
196- columns = selection ["columns" ]
197- self .set_selection (rows , columns )
198- self .commit .now ()
338+ self ._setup_view_delegate ()
339+
340+ if self .__have_new_data :
341+ self .commit .now ()
342+ self .__have_new_data = False
199343
200344 def _setup_table_view (self ):
201345 """Setup the view with current input data."""
@@ -204,23 +348,12 @@ def _setup_table_view(self):
204348 return
205349
206350 datamodel = self .input .model
351+ datamodel .setSubsetRowIds (self ._subset_ids or set ())
352+
207353 view = self .view
208354 data = self .input .table
209355 rowcount = data .approx_len ()
210356
211- if self .color_by_class and data .domain .has_discrete_class :
212- color_schema = [
213- QColor (* c ) for c in data .domain .class_var .colors ]
214- else :
215- color_schema = None
216- if self .show_distributions :
217- view .setItemDelegate (
218- TableBarItemDelegate (
219- view , color = self .dist_color , color_schema = color_schema )
220- )
221- else :
222- view .setItemDelegate (TableDataDelegate (view ))
223-
224357 view .setModel (datamodel )
225358
226359 vheader = view .verticalHeader ()
@@ -248,6 +381,7 @@ def _setup_table_view(self):
248381 assert view .model ().rowCount () <= maxrows
249382 assert vheader .sectionSize (0 ) > 1 or datamodel .rowCount () == 0
250383
384+ self ._setup_view_delegate ()
251385 # update the header (attribute names)
252386 self ._update_variable_labels ()
253387
@@ -285,9 +419,13 @@ def _update_variable_labels(self):
285419 model .setRichHeaderFlags (RichTableModel .Name )
286420
287421 def _on_distribution_color_changed (self ):
422+ if self .input is None :
423+ return # pragma: no cover
424+ self ._setup_view_delegate ()
425+
426+ def _setup_view_delegate (self ):
288427 if self .input is None :
289428 return
290- widget = self .view
291429 model = self .input .model
292430 data = model .source
293431 class_var = data .domain .class_var
@@ -296,11 +434,13 @@ def _on_distribution_color_changed(self):
296434 else :
297435 color_schema = None
298436 if self .show_distributions :
299- delegate = TableBarItemDelegate (widget , color = self .dist_color ,
300- color_schema = color_schema )
437+ delegate = TableBarItemDelegate (
438+ self .view , color = self .dist_color , color_schema = color_schema
439+ )
301440 else :
302- delegate = TableDataDelegate (widget )
303- widget .setItemDelegate (delegate )
441+ delegate = SubsetTableDataDelegate (self .view )
442+ delegate .subset_opacity = 0.5 if self ._subset_ids is not None else 1.0
443+ self .view .setItemDelegate (delegate )
304444
305445 def _on_select_rows_changed (self ):
306446 if self .input is None :
0 commit comments