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

[ENH] Data Table: Optimize performance #2905

Merged
merged 3 commits into from
Feb 19, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
230 changes: 118 additions & 112 deletions Orange/widgets/data/owtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import concurrent.futures

from collections import OrderedDict, namedtuple
from typing import List, Tuple, Iterable # pylint: disable=unused-import

from math import isnan

import numpy
Expand Down Expand Up @@ -40,50 +42,26 @@
from Orange.widgets.utils.itemmodels import TableModel


class RichTableDecorator(QIdentityProxyModel):
"""A proxy model for a TableModel with some bells and whistles
class RichTableModel(TableModel):
"""A TableModel with some extra bells and whistles/

(adds support for gui.BarRole, include variable labels and icons
in the header)
"""
#: Rich header data flags.
Name, Labels, Icon = 1, 2, 4

def __init__(self, source, parent=None):
super().__init__(parent)

self._header_flags = RichTableDecorator.Name
self._labels = []
self._continuous = []

self.setSourceModel(source)
def __init__(self, sourcedata, parent=None):
super().__init__(sourcedata, parent)

@property
def source(self):
return getattr(self.sourceModel(), "source", None)

@property
def vars(self):
return getattr(self.sourceModel(), "vars", [])

def setSourceModel(self, source):
if source is not None and \
not isinstance(source, TableModel):
raise TypeError()

if source is not None:
self._continuous = [var.is_continuous for var in source.vars]
labels = []
for var in source.vars:
if isinstance(var, Orange.data.Variable):
labels.extend(var.attributes.keys())
self._labels = list(sorted(
{label for label in labels if not label.startswith("_")}))
else:
self._continuous = []
self._labels = []

super().setSourceModel(source)
self._header_flags = RichTableModel.Name
self._continuous = [var.is_continuous for var in self.vars]
labels = []
for var in self.vars:
if isinstance(var, Orange.data.Variable):
labels.extend(var.attributes.keys())
self._labels = list(sorted(
{label for label in labels if not label.startswith("_")}))

def data(self, index, role=Qt.DisplayRole,
# for faster local lookup
Expand All @@ -104,36 +82,30 @@ def data(self, index, role=Qt.DisplayRole,
return super().data(index, role)

def headerData(self, section, orientation, role):
if self.sourceModel() is None:
return None

# NOTE: Always use `self.sourceModel().heaerData(...)` and not
# super().headerData(...). The later does not work for zero length
# source models
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
var = self.sourceModel().headerData(
var = super().headerData(
section, orientation, TableModel.VariableRole)
if var is None:
return self.sourceModel().headerData(
return super().headerData(
section, orientation, Qt.DisplayRole)

lines = []
if self._header_flags & RichTableDecorator.Name:
if self._header_flags & RichTableModel.Name:
lines.append(var.name)
if self._header_flags & RichTableDecorator.Labels:
if self._header_flags & RichTableModel.Labels:
lines.extend(str(var.attributes.get(label, ""))
for label in self._labels)
return "\n".join(lines)
elif orientation == Qt.Horizontal and role == Qt.DecorationRole and \
self._header_flags & RichTableDecorator.Icon:
var = self.sourceModel().headerData(
self._header_flags & RichTableModel.Icon:
var = super().headerData(
section, orientation, TableModel.VariableRole)
if var is not None:
return gui.attributeIconDict[var]
else:
return None
else:
return self.sourceModel().headerData(section, orientation, role)
return super().headerData(section, orientation, role)

def setRichHeaderFlags(self, flags):
if flags != self._header_flags:
Expand All @@ -144,24 +116,6 @@ def setRichHeaderFlags(self, flags):
def richHeaderFlags(self):
return self._header_flags

if QT_VERSION < 0xFFFFFF: # TODO: change when QTBUG-44143 is fixed
def sort(self, column, order):
# Preempt the layout change notification
self.layoutAboutToBeChanged.emit()
# Block signals to suppress repeated layout[AboutToBe]Changed
# TODO: Are any other signals emitted during a sort?
self.blockSignals(True)
try:
rval = self.sourceModel().sort(column, order)
finally:
self.blockSignals(False)
# Tidy up.
self.layoutChanged.emit()
return rval
else:
def sort(self, column, order):
return self.sourceModel().sort(column, order)


class TableSliceProxy(QIdentityProxyModel):
def __init__(self, parent=None, rowSlice=slice(0, -1), **kwargs):
Expand Down Expand Up @@ -235,55 +189,54 @@ def select(self, selection, flags):
if isinstance(selection, QModelIndex):
selection = QItemSelection(selection, selection)

model = self.model()
indexes = self.selectedIndexes()

rows = set(ind.row() for ind in indexes)
cols = set(ind.column() for ind in indexes)

if flags & QItemSelectionModel.Select and \
not flags & QItemSelectionModel.Clear and self.__selectBlocks:
indexes = selection.indexes()
sel_rows = set(ind.row() for ind in indexes).union(rows)
sel_cols = set(ind.column() for ind in indexes).union(cols)

selection = QItemSelection()

for r_start, r_end in ranges(sorted(sel_rows)):
for c_start, c_end in ranges(sorted(sel_cols)):
top_left = model.index(r_start, c_start)
bottom_right = model.index(r_end - 1, c_end - 1)
selection.select(top_left, bottom_right)
elif self.__selectBlocks and flags & QItemSelectionModel.Deselect:
indexes = selection.indexes()
if not self.__selectBlocks:
super().select(selection, flags)
return

def to_ranges(indices):
return list(range(*r) for r in ranges(indices))
model = self.model()

selected_rows = to_ranges(sorted(rows))
selected_cols = to_ranges(sorted(cols))
def to_ranges(spans):
return list(range(*r) for r in spans)

desel_rows = to_ranges(set(ind.row() for ind in indexes))
desel_cols = to_ranges(set(ind.column() for ind in indexes))
if flags & QItemSelectionModel.Current: # no current selection support
flags &= ~QItemSelectionModel.Current
if flags & QItemSelectionModel.Toggle: # no toggle support either
flags &= ~QItemSelectionModel.Toggle
flags |= QItemSelectionModel.Select

if flags == QItemSelectionModel.ClearAndSelect:
# extend selection ranges in `selection` to span all row/columns
sel_rows = selection_rows(selection)
sel_cols = selection_columns(selection)
selection = QItemSelection()

# deselection extended vertically
for row_range, col_range in \
itertools.product(selected_rows, desel_cols):
itertools.product(to_ranges(sel_rows), to_ranges(sel_cols)):
selection.select(
model.index(row_range.start, col_range.start),
model.index(row_range.stop - 1, col_range.stop - 1)
)
# deselection extended horizontally
elif flags & (QItemSelectionModel.Select |
QItemSelectionModel.Deselect):
# extend all selection ranges in `selection` with the full current
# row/col spans
rows, cols = selection_blocks(self.selection())
sel_rows = selection_rows(selection)
sel_cols = selection_columns(selection)
ext_selection = QItemSelection()
for row_range, col_range in \
itertools.product(desel_rows, selected_cols):
selection.select(
itertools.product(to_ranges(rows), to_ranges(sel_cols)):
ext_selection.select(
model.index(row_range.start, col_range.start),
model.index(row_range.stop - 1, col_range.stop - 1)
)

QItemSelectionModel.select(self, selection, flags)
for row_range, col_range in \
itertools.product(to_ranges(sel_rows), to_ranges(cols)):
ext_selection.select(
model.index(row_range.start, col_range.start),
model.index(row_range.stop - 1, col_range.stop - 1)
)
selection.merge(ext_selection, QItemSelectionModel.Select)
super().select(selection, flags)

def selectBlocks(self):
"""Is the block selection in effect."""
Expand All @@ -299,7 +252,59 @@ def setSelectBlocks(self, state):
self.__selectBlocks = state


def selection_rows(selection):
# type: (QItemSelection) -> List[Tuple[int, int]]
"""
Return a list of ranges for all referenced rows contained in selection

Parameters
----------
selection : QItemSelection

Returns
-------
rows : List[Tuple[int, int]]
"""
spans = set(range(s.top(), s.bottom() + 1) for s in selection)
indices = sorted(set(itertools.chain(*spans)))
return list(ranges(indices))


def selection_columns(selection):
# type: (QItemSelection) -> List[Tuple[int, int]]
"""
Return a list of ranges for all referenced columns contained in selection

Parameters
----------
selection : QItemSelection

Returns
-------
rows : List[Tuple[int, int]]
"""
spans = {range(s.left(), s.right() + 1) for s in selection}
indices = sorted(set(itertools.chain(*spans)))
return list(ranges(indices))


def selection_blocks(selection):
# type: (QItemSelection) -> Tuple[List[Tuple[int, int]], List[Tuple[int, int]]]
if selection.count() > 0:
rowranges = {range(span.top(), span.bottom() + 1)
for span in selection}
colranges = {range(span.left(), span.right() + 1)
for span in selection}
else:
return [], []

rows = sorted(set(itertools.chain(*rowranges)))
cols = sorted(set(itertools.chain(*colranges)))
return list(ranges(rows)), list(ranges(cols))


def ranges(indices):
# type: (Iterable[int]) -> Iterable[Tuple[int, int]]
"""
Group consecutive indices into `(start, stop)` tuple 'ranges'.

Expand Down Expand Up @@ -521,8 +526,7 @@ def _setup_table_view(self, view, data):
view.setModel(None)
return

datamodel = TableModel(data)
datamodel = RichTableDecorator(datamodel)
datamodel = RichTableModel(data)

rowcount = data.approx_len()

Expand Down Expand Up @@ -654,7 +658,7 @@ def _update_variable_labels(self, view):

if self.show_attribute_labels:
model.setRichHeaderFlags(
RichTableDecorator.Labels | RichTableDecorator.Name)
RichTableModel.Labels | RichTableModel.Name)

labelnames = set()
for a in model.source.domain.variables:
Expand All @@ -663,7 +667,7 @@ def _update_variable_labels(self, view):
[label for label in labelnames if not label.startswith("_")])
self.set_corner_text(view, "\n".join([""] + labelnames))
else:
model.setRichHeaderFlags(RichTableDecorator.Name)
model.setRichHeaderFlags(RichTableModel.Name)
self.set_corner_text(view, "")

def _on_show_variable_labels_changed(self):
Expand Down Expand Up @@ -764,24 +768,26 @@ def get_selection(self, view):
"""
Return the selected row and column indices of the selection in view.
"""
selection = view.selectionModel().selection()
selmodel = view.selectionModel()

selection = selmodel.selection()
model = view.model()
# map through the proxies into input table.
while isinstance(model, QAbstractProxyModel):
selection = model.mapSelectionToSource(selection)
model = model.sourceModel()

assert isinstance(selmodel, BlockSelectionModel)
assert isinstance(model, TableModel)

indexes = selection.indexes()

rows = numpy.unique([ind.row() for ind in indexes])
row_spans, col_spans = selection_blocks(selection)
rows = list(itertools.chain.from_iterable(itertools.starmap(range, row_spans)))
cols = list(itertools.chain.from_iterable(itertools.starmap(range, col_spans)))
rows = numpy.array(rows, dtype=numpy.intp)
# map the rows through the applied sorting (if any)
rows = model.mapToSourceRows(rows)
rows.sort()
rows = rows.tolist()

cols = sorted(set(ind.column() for ind in indexes))
return rows, cols

@staticmethod
Expand Down
14 changes: 11 additions & 3 deletions Orange/widgets/utils/itemmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from AnyQt.QtCore import (
Qt, QObject, QAbstractListModel, QAbstractTableModel, QModelIndex,
QItemSelectionModel
QItemSelectionModel, QT_VERSION
)
from AnyQt.QtCore import pyqtSignal as Signal
from AnyQt.QtGui import QColor
Expand Down Expand Up @@ -197,7 +197,12 @@ def sort(self, column: int, order: Qt.SortOrder = Qt.AscendingOrder):
data table is left unmodified. Use mapToSourceRows()/mapFromSourceRows()
when accessing data by row indexes.
"""
self.layoutAboutToBeChanged.emit()
if QT_VERSION >= 0x50000:
self.layoutAboutToBeChanged.emit(
[], QAbstractTableModel.VerticalSortHint
)
else:
self.layoutAboutToBeChanged.emit()

# Store persistent indices as well as their (actual) rows in the
# source data table.
Expand Down Expand Up @@ -230,7 +235,10 @@ def sort(self, column: int, order: Qt.SortOrder = Qt.AscendingOrder):
persistent,
[self.index(row, pind.column())
for row, pind in zip(persistent_rows, persistent)])
self.layoutChanged.emit()
if QT_VERSION >= 0x50000:
self.layoutChanged.emit([], QAbstractTableModel.VerticalSortHint)
else:
self.layoutChanged.emit()


class PyTableModel(AbstractSortTableModel):
Expand Down