Skip to content

Commit

Permalink
owtable: Add data subset input
Browse files Browse the repository at this point in the history
  • Loading branch information
ales-erjavec committed Apr 14, 2023
1 parent 3bea541 commit 89ac3bf
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 43 deletions.
205 changes: 164 additions & 41 deletions Orange/widgets/data/owtable.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import concurrent.futures
from dataclasses import dataclass
from typing import Optional, Union, Sequence, List, TypedDict, Tuple, Dict
from typing import (
Optional, Union, Sequence, List, TypedDict, Tuple, Dict, Any, Container
)

from scipy.sparse import issparse

from AnyQt.QtWidgets import QTableView, QHeaderView, QApplication, QStyle
from AnyQt.QtGui import QColor, QClipboard
from AnyQt.QtCore import Qt, QSize, QMetaObject, QItemSelectionModel
from AnyQt.QtWidgets import (
QTableView, QHeaderView, QApplication, QStyle, QStyleOptionHeader,
QStyleOptionViewItem
)
from AnyQt.QtGui import QColor, QClipboard, QPainter
from AnyQt.QtCore import (
Qt, QSize, QMetaObject, QItemSelectionModel, QModelIndex, QRect
)
from AnyQt.QtCore import Slot

from orangewidget.gui import OrangeUserRole

import Orange.data
from Orange.data import Variable
from Orange.data.table import Table
Expand All @@ -26,18 +35,130 @@
from Orange.widgets.utils.itemmodels import TableModel
from Orange.widgets.utils.state_summary import format_summary_details
from Orange.widgets.utils import disconnected
from Orange.widgets.utils.headerview import HeaderView
from Orange.widgets.data.utils.tableview import RichTableView
from Orange.widgets.data.utils import tablesummary as tsummary


SubsetRole = next(OrangeUserRole)


class HeaderViewWithSubsetIndicator(HeaderView):
_IndicatorChar = "\N{BULLET}"

def paintSection(
self, painter: QPainter, rect: QRect, logicalIndex: int
) -> None:
opt = QStyleOptionHeader()
self.initStyleOption(opt)
self.initStyleOptionForIndex(opt, logicalIndex)
model = self.model()
if model is None:
return # pragma: no cover
opt.rect = rect
issubset = model.headerData(logicalIndex, Qt.Vertical, SubsetRole)
style = self.style()
# draw background
style.drawControl(QStyle.CE_HeaderSection, opt, painter, self)
indicator_rect = QRect(rect)
text_rect = QRect(rect)
indicator_width = opt.fontMetrics.horizontalAdvance(
self._IndicatorChar + " "
)
indicator_rect.setWidth(indicator_width)
text_rect.setLeft(indicator_width)
if issubset:
optindicator = QStyleOptionHeader(opt)
optindicator.rect = indicator_rect
optindicator.textAlignment = Qt.AlignCenter
optindicator.text = self._IndicatorChar
# draw subset indicator
style.drawControl(QStyle.CE_HeaderLabel, optindicator, painter, self)
opt.rect = text_rect
# draw section label
style.drawControl(QStyle.CE_HeaderLabel, opt, painter, self)

def sectionSizeFromContents(self, logicalIndex: int) -> QSize:
opt = QStyleOptionHeader()
self.initStyleOption(opt)
super().initStyleOptionForIndex(opt, logicalIndex)
opt.text = self._IndicatorChar + " " + opt.text
return self.style().sizeFromContents(QStyle.CT_HeaderSection, opt, QSize(), self)


class DataTableView(gui.HScrollStepMixin, RichTableView):
pass
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vheader = HeaderViewWithSubsetIndicator(
Qt.Vertical, self, highlightSections=True
)
vheader.setSectionsClickable(True)
self.setVerticalHeader(vheader)


class _TableDataDelegate(TableDataDelegate):
DefaultRoles = TableDataDelegate.DefaultRoles + (SubsetRole,)


class SubsetTableDataDelegate(_TableDataDelegate):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.subset_opacity = 0.5

def paint(
self, painter: QPainter, option: QStyleOptionViewItem,
index: QModelIndex
) -> None:
issubset = self.cachedData(index, SubsetRole)
opacity = painter.opacity()
if not issubset:
painter.setOpacity(self.subset_opacity)
super().paint(painter, option, index)
if not issubset:
painter.setOpacity(opacity)

class TableBarItemDelegate(gui.TableBarItem, TableDataDelegate):

class TableBarItemDelegate(SubsetTableDataDelegate, gui.TableBarItem,
_TableDataDelegate):
pass


class _TableModel(RichTableModel):
SubsetRole = SubsetRole

def __init__(self, *args, subsets=None, **kwargs):
super().__init__(*args, **kwargs)
self._subset = subsets or set()

def setSubsetRowIds(self, subsetids: Container[int]):
self._subset = subsetids
if self.rowCount():
self.headerDataChanged.emit(Qt.Vertical, 0, self.rowCount() - 1)
self.dataChanged.emit(
self.index(0, 0),
self.index(self.rowCount() - 1, self.columnCount() - 1),
[SubsetRole],
)

def _is_subset(self, row):
row = self.mapToSourceRows(row)
try:
id_ = self.source.ids[row]
except (IndexError, AttributeError): # pragma: no cover
return False
return int(id_) in self._subset

def data(self, index: QModelIndex, role=Qt.DisplayRole) -> Any:
if role == _TableModel.SubsetRole:
return self._is_subset(index.row())
return super().data(index, role)

def headerData(self, section, orientation, role):
if orientation == Qt.Vertical and role == _TableModel.SubsetRole:
return self._is_subset(section)
return super().headerData(section, orientation, role)


@dataclass
class InputData:
table: Table
Expand All @@ -61,7 +182,8 @@ class OWTable(OWWidget):
keywords = "data table, view"

class Inputs:
data = Input("Data", Table)
data = Input("Data", Table, default=True)
data_subset = Input("Data Subset", Table)

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

view = DataTableView(
sortingEnabled=True
)
view.setSortingEnabled(True)
view.setItemDelegate(TableDataDelegate(view))
view = DataTableView(sortingEnabled=True)
view.setItemDelegate(SubsetTableDataDelegate(view))
view.selectionFinished.connect(self.update_selection)

if self.select_rows:
Expand Down Expand Up @@ -164,27 +284,35 @@ def set_dataset(self, data: Optional[Table]):
self.view.setModel(None)
self.view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder)
if data is not None:
summary = tsummary.table_summary(data)
self.input = InputData(
table=data,
summary=tsummary.table_summary(data),
model=RichTableModel(data)
summary=summary,
model=_TableModel(data)
)
self._setup_table_view()
if isinstance(summary.len, concurrent.futures.Future):
def update(_):
QMetaObject.invokeMethod(
self, "_update_info", Qt.QueuedConnection)
summary.len.add_done_callback(update)
else:
self.input = None

@Inputs.data_subset
def set_subset_dataset(self, subset: Optional[Table]):
"""Set the data subset"""
if subset is not None and not isinstance(subset, SqlTable):
ids = set(subset.ids)
else:
ids = None
self._subset_ids = ids

def handleNewSignals(self):
super().handleNewSignals()
self.Warning.non_sortable_input.clear()
self.Warning.missing_sort_columns.clear()
data: Optional[Table] = self.input.table if self.input else None
slot = self.input
if slot is not None and isinstance(slot.summary.len, concurrent.futures.Future):
def update(_):
QMetaObject.invokeMethod(
self, "_update_info", Qt.QueuedConnection)
slot.summary.len.add_done_callback(update)

self._setup_table_view()
self._update_input_summary()

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

datamodel = self.input.model
datamodel.setSubsetRowIds(self._subset_ids or set())

view = self.view
data = self.input.table
rowcount = data.approx_len()

if self.color_by_class and data.domain.has_discrete_class:
color_schema = [
QColor(*c) for c in data.domain.class_var.colors]
else:
color_schema = None
if self.show_distributions:
view.setItemDelegate(
TableBarItemDelegate(
view, color=self.dist_color, color_schema=color_schema)
)
else:
view.setItemDelegate(TableDataDelegate(view))

view.setModel(datamodel)

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

self._setup_view_delegate()
# update the header (attribute names)
self._update_variable_labels()

Expand Down Expand Up @@ -287,8 +405,11 @@ def _update_variable_labels(self):

def _on_distribution_color_changed(self):
if self.input is None:
return
widget = self.view
return # pragma: no cover
self._setup_view_delegate()

def _setup_view_delegate(self):
assert self.input is not None
model = self.input.model
data = model.source
class_var = data.domain.class_var
Expand All @@ -297,11 +418,13 @@ def _on_distribution_color_changed(self):
else:
color_schema = None
if self.show_distributions:
delegate = TableBarItemDelegate(widget, color=self.dist_color,
color_schema=color_schema)
delegate = TableBarItemDelegate(
self.view, color=self.dist_color, color_schema=color_schema
)
else:
delegate = TableDataDelegate(widget)
widget.setItemDelegate(delegate)
delegate = SubsetTableDataDelegate(self.view)
delegate.subset_opacity = 0.5 if self._subset_ids is not None else 1.0
self.view.setItemDelegate(delegate)

def _on_select_rows_changed(self):
if self.input is None:
Expand Down
19 changes: 19 additions & 0 deletions Orange/widgets/data/tests/test_owtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,25 @@ def test_show_attribute_labels(self):
w.controls.show_attribute_labels.toggle()
self.assertFalse(w.show_attribute_labels)

def test_subset_input(self):
w = self.widget
self.send_signal(w.Inputs.data, self.data)
self.send_signal(w.Inputs.data_subset, self.data[[0, 1, 5]])
w.view.grab() # cover delegate painting methods

model = w.view.model()
self.assertTrue(model.index(0, 0).data(model.SubsetRole))
self.assertFalse(model.index(2, 0).data(model.SubsetRole))
self.assertTrue(model.headerData(0, Qt.Vertical, model.SubsetRole))
self.assertFalse(model.headerData(2, Qt.Vertical, model.SubsetRole))

self.send_signal(w.Inputs.data_subset, None)
w.view.grab()

model = w.view.model()
self.assertFalse(model.index(0, 0).data(model.SubsetRole))
self.assertFalse(model.headerData(0, Qt.Vertical, model.SubsetRole))


class TestOWTableSQL(TestOWTable, dbt):
def setUpDB(self):
Expand Down
4 changes: 2 additions & 2 deletions Orange/widgets/utils/itemdelegates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import math
from typing import Optional, Tuple
from typing import Optional, Tuple, ClassVar

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

0 comments on commit 89ac3bf

Please sign in to comment.