1from numbers import Number, Integral 2from math import isnan, isinf 3 4import operator 5from collections import namedtuple, defaultdict 6from collections.abc import Sequence 7from contextlib import contextmanager 8from functools import reduce, partial, lru_cache, wraps 9from itertools import chain 10from warnings import warn 11from xml.sax.saxutils import escape 12 13from AnyQt.QtCore import ( 14 Qt, QObject, QAbstractListModel, QModelIndex, 15 QItemSelectionModel, QItemSelection) 16from AnyQt.QtCore import pyqtSignal as Signal 17from AnyQt.QtGui import QColor, QBrush 18from AnyQt.QtWidgets import ( 19 QWidget, QBoxLayout, QToolButton, QAbstractButton, QAction 20) 21 22import numpy 23 24from orangewidget.utils.itemmodels import ( 25 PyListModel, AbstractSortTableModel as _AbstractSortTableModel 26) 27 28from Orange.widgets.utils.colorpalettes import ContinuousPalettes, ContinuousPalette 29from Orange.data import Variable, Storage, DiscreteVariable, ContinuousVariable 30from Orange.data.domain import filter_visible 31from Orange.widgets import gui 32from Orange.widgets.utils import datacaching 33from Orange.statistics import basic_stats 34from Orange.util import deprecated 35 36__all__ = [ 37 "PyListModel", "VariableListModel", "PyListModelTooltip", "DomainModel", 38 "AbstractSortTableModel", "PyTableModel", "TableModel", 39 "ModelActionsWidget", "ListSingleSelectionModel" 40] 41 42@contextmanager 43def signal_blocking(obj): 44 blocked = obj.signalsBlocked() 45 obj.blockSignals(True) 46 try: 47 yield 48 finally: 49 obj.blockSignals(blocked) 50 51 52def _as_contiguous_range(the_slice, length): 53 start, stop, step = the_slice.indices(length) 54 if step == -1: 55 # Equivalent range with positive step 56 start, stop, step = stop + 1, start + 1, 1 57 elif not (step == 1 or step is None): 58 raise IndexError("Non-contiguous range.") 59 return start, stop, step 60 61 62class AbstractSortTableModel(_AbstractSortTableModel): 63 # these were defined on TableModel below. When the AbstractSortTableModel 64 # was extracted and made a base of TableModel these deprecations were 65 # misplaced, they belong in TableModel. 66 @deprecated('Orange.widgets.utils.itemmodels.AbstractSortTableModel.mapFromSourceRows') 67 def mapFromTableRows(self, rows): 68 return self.mapFromSourceRows(rows) 69 70 @deprecated('Orange.widgets.utils.itemmodels.AbstractSortTableModel.mapToSourceRows') 71 def mapToTableRows(self, rows): 72 return self.mapToSourceRows(rows) 73 74 75class PyTableModel(AbstractSortTableModel): 76 """ A model for displaying python tables (sequences of sequences) in 77 QTableView objects. 78 79 Parameters 80 ---------- 81 sequence : list 82 The initial list to wrap. 83 parent : QObject 84 Parent QObject. 85 editable: bool or sequence 86 If True, all items are flagged editable. If sequence, the True-ish 87 fields mark their respective columns editable. 88 89 Notes 90 ----- 91 The model rounds numbers to human readable precision, e.g.: 92 1.23e-04, 1.234, 1234.5, 12345, 1.234e06. 93 94 To set additional item roles, use setData(). 95 """ 96 97 @staticmethod 98 def _RoleData(): 99 return defaultdict(lambda: defaultdict(dict)) 100 101 # All methods are either necessary overrides of super methods, or 102 # methods likened to the Python list's. Hence, docstrings aren't. 103 # pylint: disable=missing-docstring 104 def __init__(self, sequence=None, parent=None, editable=False): 105 super().__init__(parent) 106 self._rows = self._cols = 0 107 self._headers = {} 108 self._editable = editable 109 self._table = None 110 self._roleData = {} 111 if sequence is None: 112 sequence = [] 113 self.wrap(sequence) 114 115 def rowCount(self, parent=QModelIndex()): 116 return 0 if parent.isValid() else self._rows 117 118 def columnCount(self, parent=QModelIndex()): 119 return 0 if parent.isValid() else self._cols 120 121 def flags(self, index): 122 flags = super().flags(index) 123 if not self._editable or not index.isValid(): 124 return flags 125 if isinstance(self._editable, Sequence): 126 return flags | Qt.ItemIsEditable if self._editable[index.column()] else flags 127 return flags | Qt.ItemIsEditable 128 129 def setData(self, index, value, role): 130 row = self.mapFromSourceRows(index.row()) 131 if role == Qt.EditRole: 132 self[row][index.column()] = value 133 self.dataChanged.emit(index, index) 134 else: 135 self._roleData[row][index.column()][role] = value 136 return True 137 138 def data(self, index, role=Qt.DisplayRole): 139 if not index.isValid(): 140 return 141 142 row, column = self.mapToSourceRows(index.row()), index.column() 143 144 role_value = self._roleData.get(row, {}).get(column, {}).get(role) 145 if role_value is not None: 146 return role_value 147 148 try: 149 value = self[row][column] 150 except IndexError: 151 return 152 if role == Qt.EditRole: 153 return value 154 if role == Qt.DecorationRole and isinstance(value, Variable): 155 return gui.attributeIconDict[value] 156 if role == Qt.DisplayRole: 157 if (isinstance(value, Number) and 158 not (isnan(value) or isinf(value) or isinstance(value, Integral))): 159 absval = abs(value) 160 strlen = len(str(int(absval))) 161 value = '{:.{}{}}'.format(value, 162 2 if absval < .001 else 163 3 if strlen < 2 else 164 1 if strlen < 5 else 165 0 if strlen < 6 else 166 3, 167 'f' if (absval == 0 or 168 absval >= .001 and 169 strlen < 6) 170 else 'e') 171 return str(value) 172 if role == Qt.TextAlignmentRole and isinstance(value, Number): 173 return Qt.AlignRight | Qt.AlignVCenter 174 if role == Qt.ToolTipRole: 175 return str(value) 176 177 def sortColumnData(self, column): 178 return [row[column] for row in self._table] 179 180 def setHorizontalHeaderLabels(self, labels): 181 """ 182 Parameters 183 ---------- 184 labels : list of str or list of Variable 185 """ 186 self._headers[Qt.Horizontal] = tuple(labels) 187 188 def setVerticalHeaderLabels(self, labels): 189 """ 190 Parameters 191 ---------- 192 labels : list of str or list of Variable 193 """ 194 self._headers[Qt.Vertical] = tuple(labels) 195 196 def headerData(self, section, orientation, role=Qt.DisplayRole): 197 headers = self._headers.get(orientation) 198 199 if headers and section < len(headers): 200 section = self.mapToSourceRows(section) if orientation == Qt.Vertical else section 201 value = headers[section] 202 203 if role == Qt.ToolTipRole: 204 role = Qt.DisplayRole 205 206 if role == Qt.DisplayRole: 207 return value.name if isinstance(value, Variable) else value 208 209 if role == Qt.DecorationRole: 210 if isinstance(value, Variable): 211 return gui.attributeIconDict[value] 212 213 # Use QAbstractItemModel default for non-existent header/sections 214 return super().headerData(section, orientation, role) 215 216 def removeRows(self, row, count, parent=QModelIndex()): 217 if not parent.isValid(): 218 del self[row:row + count] 219 for rowidx in range(row, row + count): 220 self._roleData.pop(rowidx, None) 221 self._rows = self._table_dim()[0] 222 return True 223 return False 224 225 def removeColumns(self, column, count, parent=QModelIndex()): 226 self.beginRemoveColumns(parent, column, column + count - 1) 227 for row in self._table: 228 del row[column:column + count] 229 for cols in self._roleData.values(): 230 for col in range(column, column + count): 231 cols.pop(col, None) 232 del self._headers.get(Qt.Horizontal, [])[column:column + count] 233 self._cols = self._table_dim()[1] 234 self.endRemoveColumns() 235 return True 236 237 def _table_dim(self): 238 return len(self._table), max(map(len, self), default=0) 239 240 def insertRows(self, row, count, parent=QModelIndex()): 241 self.beginInsertRows(parent, row, row + count - 1) 242 self._table[row:row] = [[''] * self.columnCount() for _ in range(count)] 243 self._rows = self._table_dim()[0] 244 self.endInsertRows() 245 return True 246 247 def insertColumns(self, column, count, parent=QModelIndex()): 248 self.beginInsertColumns(parent, column, column + count - 1) 249 for row in self._table: 250 row[column:column] = [''] * count 251 self._rows = self._table_dim()[0] 252 self.endInsertColumns() 253 return True 254 255 def __len__(self): 256 return len(self._table) 257 258 def __bool__(self): 259 return len(self) != 0 260 261 def __iter__(self): 262 return iter(self._table) 263 264 def __getitem__(self, item): 265 return self._table[item] 266 267 def __delitem__(self, i): 268 if isinstance(i, slice): 269 start, stop, _ = _as_contiguous_range(i, len(self)) 270 stop -= 1 271 else: 272 start = stop = i = i if i >= 0 else len(self) + i 273 if stop < start: 274 return 275 self._check_sort_order() 276 self.beginRemoveRows(QModelIndex(), start, stop) 277 del self._table[i] 278 rows = self._table_dim()[0] 279 self._rows = rows 280 self.endRemoveRows() 281 self._update_column_count() 282 283 def __setitem__(self, i, value): 284 self._check_sort_order() 285 if isinstance(i, slice): 286 start, stop, _ = _as_contiguous_range(i, len(self)) 287 self.removeRows(start, stop - start) 288 if len(value) == 0: 289 return 290 self.beginInsertRows(QModelIndex(), start, start + len(value) - 1) 291 self._table[start:start] = value 292 self._rows = self._table_dim()[0] 293 self.endInsertRows() 294 self._update_column_count() 295 else: 296 self._table[i] = value 297 self.dataChanged.emit(self.index(i, 0), 298 self.index(i, self.columnCount() - 1)) 299 300 def _update_column_count(self): 301 cols_before = self._cols 302 cols_after = self._table_dim()[1] 303 if cols_before < cols_after: 304 self.beginInsertColumns(QModelIndex(), cols_before, cols_after - 1) 305 self._cols = cols_after 306 self.endInsertColumns() 307 elif cols_before > cols_after: 308 self.beginRemoveColumns(QModelIndex(), cols_after, cols_before - 1) 309 self._cols = cols_after 310 self.endRemoveColumns() 311 312 def _check_sort_order(self): 313 if self.mapToSourceRows(Ellipsis) is not Ellipsis: 314 warn("Can't modify PyTableModel when it's sorted", 315 RuntimeWarning, stacklevel=3) 316 raise RuntimeError("Can't modify PyTableModel when it's sorted") 317 318 def wrap(self, table): 319 self.beginResetModel() 320 self._table = table 321 self._roleData = self._RoleData() 322 self._rows, self._cols = self._table_dim() 323 self.resetSorting() 324 self.endResetModel() 325 326 def tolist(self): 327 return self._table 328 329 def clear(self): 330 self.beginResetModel() 331 self._table.clear() 332 self.resetSorting() 333 self._roleData.clear() 334 self._rows, self._cols = self._table_dim() 335 self.endResetModel() 336 337 def append(self, row): 338 self.extend([row]) 339 340 def _insertColumns(self, rows): 341 n_max = max(map(len, rows)) 342 if self.columnCount() < n_max: 343 self.insertColumns(self.columnCount(), n_max - self.columnCount()) 344 345 def extend(self, rows): 346 i, rows = len(self), list(rows) 347 self.insertRows(i, len(rows)) 348 self._insertColumns(rows) 349 self[i:] = rows 350 351 def insert(self, i, row): 352 self.insertRows(i, 1) 353 self._insertColumns((row,)) 354 self[i] = row 355 356 def remove(self, val): 357 del self[self._table.index(val)] 358 359 360class PyListModelTooltip(PyListModel): 361 def __init__(self): 362 super().__init__() 363 self.tooltips = [] 364 365 def data(self, index, role=Qt.DisplayRole): 366 if role == Qt.ToolTipRole: 367 return self.tooltips[index.row()] 368 else: 369 return super().data(index, role) 370 371 372class VariableListModel(PyListModel): 373 MIME_TYPE = "application/x-Orange-VariableList" 374 375 def __init__(self, *args, placeholder=None, **kwargs): 376 super().__init__(*args, **kwargs) 377 self.placeholder = placeholder 378 379 def data(self, index, role=Qt.DisplayRole): 380 if self._is_index_valid(index): 381 var = self[index.row()] 382 if var is None and role == Qt.DisplayRole: 383 return self.placeholder or "None" 384 if not isinstance(var, Variable): 385 return super().data(index, role) 386 elif role == Qt.DisplayRole: 387 return var.name 388 elif role == Qt.DecorationRole: 389 return gui.attributeIconDict[var] 390 elif role == Qt.ToolTipRole: 391 return self.variable_tooltip(var) 392 elif role == gui.TableVariable: 393 return var 394 else: 395 return PyListModel.data(self, index, role) 396 397 def variable_tooltip(self, var): 398 if var.is_discrete: 399 return self.discrete_variable_tooltip(var) 400 elif var.is_time: 401 return self.time_variable_toltip(var) 402 elif var.is_continuous: 403 return self.continuous_variable_toltip(var) 404 elif var.is_string: 405 return self.string_variable_tooltip(var) 406 407 def variable_labels_tooltip(self, var): 408 text = "" 409 if var.attributes: 410 items = [(safe_text(key), safe_text(value)) 411 for key, value in var.attributes.items()] 412 labels = list(map("%s = %s".__mod__, items)) 413 text += "<br/>Variable Labels:<br/>" 414 text += "<br/>".join(labels) 415 return text 416 417 def discrete_variable_tooltip(self, var): 418 text = "<b>%s</b><br/>Categorical with %i values: " %\ 419 (safe_text(var.name), len(var.values)) 420 text += ", ".join("%r" % safe_text(v) for v in var.values) 421 text += self.variable_labels_tooltip(var) 422 return text 423 424 def time_variable_toltip(self, var): 425 text = "<b>%s</b><br/>Time" % safe_text(var.name) 426 text += self.variable_labels_tooltip(var) 427 return text 428 429 def continuous_variable_toltip(self, var): 430 text = "<b>%s</b><br/>Numeric" % safe_text(var.name) 431 text += self.variable_labels_tooltip(var) 432 return text 433 434 def string_variable_tooltip(self, var): 435 text = "<b>%s</b><br/>Text" % safe_text(var.name) 436 text += self.variable_labels_tooltip(var) 437 return text 438 439 440class DomainModel(VariableListModel): 441 ATTRIBUTES, CLASSES, METAS = 1, 2, 4 442 MIXED = ATTRIBUTES | CLASSES | METAS 443 SEPARATED = (CLASSES, PyListModel.Separator, 444 METAS, PyListModel.Separator, 445 ATTRIBUTES) 446 PRIMITIVE = (DiscreteVariable, ContinuousVariable) 447 448 def __init__(self, order=SEPARATED, separators=True, placeholder=None, 449 valid_types=None, alphabetical=False, skip_hidden_vars=True, **kwargs): 450 """ 451 452 Parameters 453 ---------- 454 order: tuple or int 455 Order of attributes, metas, classes, separators and other options 456 separators: bool 457 If False, remove separators from `order`. 458 placeholder: str 459 The text that is shown when no variable is selected 460 valid_types: tuple 461 (Sub)types of `Variable` that are included in the model 462 alphabetical: bool 463 If true, variables are sorted alphabetically. 464 skip_hidden_vars: bool 465 If true, variables marked as "hidden" are skipped. 466 """ 467 super().__init__(placeholder=placeholder, **kwargs) 468 if isinstance(order, int): 469 order = (order,) 470 if placeholder is not None and None not in order: 471 # Add None for the placeholder if it's not already there 472 # Include separator if the current order uses them 473 order = (None,) + \ 474 (self.Separator, ) * (self.Separator in order) + \ 475 order 476 if not separators: 477 order = [e for e in order if e is not self.Separator] 478 self.order = order 479 self.valid_types = valid_types 480 self.alphabetical = alphabetical 481 self.skip_hidden_vars = skip_hidden_vars 482 self._within_set_domain = False 483 self.set_domain(None) 484 485 def set_domain(self, domain): 486 self.beginResetModel() 487 content = [] 488 # The logic related to separators is a bit complicated: it ensures that 489 # even when a section is empty we don't have two separators in a row 490 # or a separator at the end 491 add_separator = False 492 for section in self.order: 493 if section is self.Separator: 494 add_separator = True 495 continue 496 if isinstance(section, int): 497 if domain is None: 498 continue 499 to_add = list(chain( 500 *(vars for i, vars in enumerate( 501 (domain.attributes, domain.class_vars, domain.metas)) 502 if (1 << i) & section))) 503 if self.skip_hidden_vars: 504 to_add = list(filter_visible(to_add)) 505 if self.valid_types is not None: 506 to_add = [var for var in to_add 507 if isinstance(var, self.valid_types)] 508 if self.alphabetical: 509 to_add = sorted(to_add, key=lambda x: x.name) 510 elif isinstance(section, list): 511 to_add = section 512 else: 513 to_add = [section] 514 if to_add: 515 if add_separator and content: 516 content.append(self.Separator) 517 add_separator = False 518 content += to_add 519 try: 520 self._within_set_domain = True 521 self[:] = content 522 finally: 523 self._within_set_domain = False 524 self.endResetModel() 525 526 def prevent_modification(method): # pylint: disable=no-self-argument 527 @wraps(method) 528 # pylint: disable=protected-access 529 def e(self, *args, **kwargs): 530 if self._within_set_domain: 531 method(self, *args, **kwargs) 532 else: 533 raise TypeError( 534 "{} can be modified only by calling 'set_domain'". 535 format(type(self).__name__)) 536 return e 537 538 @prevent_modification 539 def extend(self, iterable): 540 return super().extend(iterable) 541 542 @prevent_modification 543 def append(self, item): 544 return super().append(item) 545 546 @prevent_modification 547 def insert(self, i, val): 548 return super().insert(i, val) 549 550 @prevent_modification 551 def remove(self, val): 552 return super().remove(val) 553 554 @prevent_modification 555 def pop(self, i): 556 return super().pop(i) 557 558 @prevent_modification 559 def clear(self): 560 return super().clear() 561 562 @prevent_modification 563 def __delitem__(self, s): 564 return super().__delitem__(s) 565 566 @prevent_modification 567 def __setitem__(self, s, value): 568 return super().__setitem__(s, value) 569 570 @prevent_modification 571 def reverse(self): 572 return super().reverse() 573 574 @prevent_modification 575 def sort(self, *args, **kwargs): 576 return super().sort(*args, **kwargs) 577 578 def setData(self, index, value, role=Qt.EditRole): 579 # reimplemented 580 if role == Qt.EditRole: 581 return False 582 else: 583 return super().setData(index, value, role) 584 585 def setItemData(self, index, data): 586 # reimplemented 587 if Qt.EditRole in data: 588 return False 589 else: 590 return super().setItemData(index, data) 591 592 def insertRows(self, row, count, parent=QModelIndex()): 593 # reimplemented 594 return False 595 596 def removeRows(self, row, count, parent=QModelIndex()): 597 # reimplemented 598 return False 599 600_html_replace = [("<", "<"), (">", ">")] 601 602 603def safe_text(text): 604 for old, new in _html_replace: 605 text = str(text).replace(old, new) 606 return text 607 608 609class ContinuousPalettesModel(QAbstractListModel): 610 """ 611 Model for combo boxes 612 """ 613 KeyRole = Qt.UserRole + 1 614 def __init__(self, parent=None, categories=None, icon_width=64): 615 super().__init__(parent) 616 self.icon_width = icon_width 617 618 palettes = list(ContinuousPalettes.values()) 619 if categories is None: 620 # Use dict, not set, to keep order of categories 621 categories = dict.fromkeys(palette.category for palette in palettes) 622 623 self.items = [] 624 for category in categories: 625 self.items.append(category) 626 self.items += [palette for palette in palettes 627 if palette.category == category] 628 if len(categories) == 1: 629 del self.items[0] 630 631 def rowCount(self, parent): 632 return 0 if parent.isValid() else len(self.items) 633 634 @staticmethod 635 def columnCount(parent): 636 return 0 if parent.isValid() else 1 637 638 def data(self, index, role): 639 item = self.items[index.row()] 640 if isinstance(item, str): 641 if role in [Qt.EditRole, Qt.DisplayRole]: 642 return item 643 else: 644 if role in [Qt.EditRole, Qt.DisplayRole]: 645 return item.friendly_name 646 if role == Qt.DecorationRole: 647 return item.color_strip(self.icon_width, 16) 648 if role == Qt.UserRole: 649 return item 650 if role == self.KeyRole: 651 return item.name 652 return None 653 654 def flags(self, index): 655 item = self.items[index.row()] 656 if isinstance(item, ContinuousPalette): 657 return Qt.ItemIsEnabled | Qt.ItemIsSelectable 658 else: 659 return Qt.NoItemFlags 660 661 def indexOf(self, x): 662 if isinstance(x, str): 663 for i, item in enumerate(self.items): 664 if not isinstance(item, str) \ 665 and x in (item.name, item.friendly_name): 666 return i 667 elif isinstance(x, ContinuousPalette): 668 return self.items.index(x) 669 return None 670 671 672class ListSingleSelectionModel(QItemSelectionModel): 673 """ Item selection model for list item models with single selection. 674 675 Defines signal: 676 - selectedIndexChanged(QModelIndex) 677 678 """ 679 selectedIndexChanged = Signal(QModelIndex) 680 681 def __init__(self, model, parent=None): 682 QItemSelectionModel.__init__(self, model, parent) 683 self.selectionChanged.connect(self.onSelectionChanged) 684 685 def onSelectionChanged(self, new, _): 686 index = list(new.indexes()) 687 if index: 688 index = index.pop() 689 else: 690 index = QModelIndex() 691 692 self.selectedIndexChanged.emit(index) 693 694 def selectedRow(self): 695 """ Return QModelIndex of the selected row or invalid if no selection. 696 """ 697 rows = self.selectedRows() 698 if rows: 699 return rows[0] 700 else: 701 return QModelIndex() 702 703 def select(self, index, flags=QItemSelectionModel.ClearAndSelect): 704 if isinstance(index, int): 705 index = self.model().index(index) 706 return QItemSelectionModel.select(self, index, flags) 707 708 709def select_row(view, row): 710 """ 711 Select a `row` in an item view. 712 """ 713 selmodel = view.selectionModel() 714 selmodel.select(view.model().index(row, 0), 715 QItemSelectionModel.ClearAndSelect | 716 QItemSelectionModel.Rows) 717 718 719def select_rows(view, row_indices, command=QItemSelectionModel.ClearAndSelect): 720 """ 721 Select several rows in view. 722 723 :param QAbstractItemView view: 724 :param row_indices: Integer indices of rows to select. 725 :param command: QItemSelectionModel.SelectionFlags 726 """ 727 selmodel = view.selectionModel() 728 model = view.model() 729 selection = QItemSelection() 730 for row in row_indices: 731 index = model.index(row, 0) 732 selection.select(index, index) 733 selmodel.select(selection, command | QItemSelectionModel.Rows) 734 735 736class ModelActionsWidget(QWidget): 737 def __init__(self, actions=None, parent=None, 738 direction=QBoxLayout.LeftToRight): 739 QWidget.__init__(self, parent) 740 self.actions = [] 741 self.buttons = [] 742 layout = QBoxLayout(direction) 743 layout.setContentsMargins(0, 0, 0, 0) 744 self.setContentsMargins(0, 0, 0, 0) 745 self.setLayout(layout) 746 if actions is not None: 747 for action in actions: 748 self.addAction(action) 749 self.setLayout(layout) 750 751 def actionButton(self, action): 752 if isinstance(action, QAction): 753 button = QToolButton(self) 754 button.setDefaultAction(action) 755 return button 756 elif isinstance(action, QAbstractButton): 757 return action 758 759 def insertAction(self, ind, action, *args): 760 button = self.actionButton(action) 761 self.layout().insertWidget(ind, button, *args) 762 self.buttons.insert(ind, button) 763 self.actions.insert(ind, action) 764 return button 765 766 def addAction(self, action, *args): 767 return self.insertAction(-1, action, *args) 768 769 770class TableModel(AbstractSortTableModel): 771 """ 772 An adapter for using Orange.data.Table within Qt's Item View Framework. 773 774 :param Orange.data.Table sourcedata: Source data table. 775 :param QObject parent: 776 """ 777 #: Orange.data.Value for the index. 778 ValueRole = gui.TableValueRole # next(gui.OrangeUserRole) 779 #: Orange.data.Value of the row's class. 780 ClassValueRole = gui.TableClassValueRole # next(gui.OrangeUserRole) 781 #: Orange.data.Variable of the column. 782 VariableRole = gui.TableVariable # next(gui.OrangeUserRole) 783 #: Basic statistics of the column 784 VariableStatsRole = next(gui.OrangeUserRole) 785 #: The column's role (position) in the domain. 786 #: One of Attribute, ClassVar or Meta 787 DomainRole = next(gui.OrangeUserRole) 788 789 #: Column domain roles 790 ClassVar, Meta, Attribute = range(3) 791 792 #: Default background color for domain roles 793 ColorForRole = { 794 ClassVar: QColor(160, 160, 160), 795 Meta: QColor(220, 220, 200), 796 Attribute: None, 797 } 798 799 #: Standard column descriptor 800 Column = namedtuple( 801 "Column", ["var", "role", "background", "format"]) 802 #: Basket column descriptor (i.e. sparse X/Y/metas/ compressed into 803 #: a single column). 804 Basket = namedtuple( 805 "Basket", ["vars", "role", "background", "density", "format"]) 806 807 # The class uses the same names (X_density etc) as Table 808 # pylint: disable=invalid-name 809 def __init__(self, sourcedata, parent=None): 810 super().__init__(parent) 811 self.source = sourcedata 812 self.domain = domain = sourcedata.domain 813 814 self.X_density = sourcedata.X_density() 815 self.Y_density = sourcedata.Y_density() 816 self.M_density = sourcedata.metas_density() 817 818 brush_for_role = { 819 role: QBrush(c) if c is not None else None 820 for role, c in self.ColorForRole.items() 821 } 822 823 def format_sparse(vars, datagetter, instance): 824 data = datagetter(instance) 825 return ", ".join("{}={}".format(vars[i].name, vars[i].repr_val(v)) 826 for i, v in zip(data.indices, data.data)) 827 828 def format_sparse_bool(vars, datagetter, instance): 829 data = datagetter(instance) 830 return ", ".join(vars[i].name for i in data.indices) 831 832 def format_dense(var, instance): 833 return str(instance[var]) 834 835 def make_basket_formater(vars, density, role): 836 formater = (format_sparse if density == Storage.SPARSE 837 else format_sparse_bool) 838 if role == TableModel.Attribute: 839 getter = operator.attrgetter("sparse_x") 840 elif role == TableModel.ClassVar: 841 getter = operator.attrgetter("sparse_y") 842 elif role == TableModel.Meta: 843 getter = operator.attrgetter("sparse_metas") 844 return partial(formater, vars, getter) 845 846 def make_basket(vars, density, role): 847 return TableModel.Basket( 848 vars, TableModel.Attribute, brush_for_role[role], density, 849 make_basket_formater(vars, density, role) 850 ) 851 852 def make_column(var, role): 853 return TableModel.Column( 854 var, role, brush_for_role[role], 855 partial(format_dense, var) 856 ) 857 858 columns = [] 859 860 if self.Y_density != Storage.DENSE and domain.class_vars: 861 coldesc = make_basket(domain.class_vars, self.Y_density, 862 TableModel.ClassVar) 863 columns.append(coldesc) 864 else: 865 columns += [make_column(var, TableModel.ClassVar) 866 for var in domain.class_vars] 867 868 if self.M_density != Storage.DENSE and domain.metas: 869 coldesc = make_basket(domain.metas, self.M_density, 870 TableModel.Meta) 871 columns.append(coldesc) 872 else: 873 columns += [make_column(var, TableModel.Meta) 874 for var in domain.metas] 875 876 if self.X_density != Storage.DENSE and domain.attributes: 877 coldesc = make_basket(domain.attributes, self.X_density, 878 TableModel.Attribute) 879 columns.append(coldesc) 880 else: 881 columns += [make_column(var, TableModel.Attribute) 882 for var in domain.attributes] 883 884 #: list of all domain variables (class_vars + metas + attrs) 885 self.vars = domain.class_vars + domain.metas + domain.attributes 886 self.columns = columns 887 888 #: A list of all unique attribute labels (in all variables) 889 self._labels = sorted( 890 reduce(operator.ior, 891 [set(var.attributes) for var in self.vars], 892 set())) 893 894 @lru_cache(maxsize=1000) 895 def row_instance(index): 896 return self.source[int(index)] 897 self._row_instance = row_instance 898 899 # column basic statistics (VariableStatsRole), computed when 900 # first needed. 901 self.__stats = None 902 self.__rowCount = sourcedata.approx_len() 903 self.__columnCount = len(self.columns) 904 905 if self.__rowCount > (2 ** 31 - 1): 906 raise ValueError("len(sourcedata) > 2 ** 31 - 1") 907 908 def sortColumnData(self, column): 909 return self._columnSortKeyData(column, TableModel.ValueRole) 910 911 @deprecated('Orange.widgets.utils.itemmodels.TableModel.sortColumnData') 912 def columnSortKeyData(self, column, role): 913 return self._columnSortKeyData(column, role) 914 915 def _columnSortKeyData(self, column, role): 916 """ 917 Return a sequence of source table objects which can be used as 918 `keys` for sorting. 919 920 :param int column: Sort column. 921 :param Qt.ItemRole role: Sort item role. 922 923 """ 924 coldesc = self.columns[column] 925 if isinstance(coldesc, TableModel.Column) \ 926 and role == TableModel.ValueRole: 927 col_data = numpy.asarray(self.source.get_column_view(coldesc.var)[0]) 928 929 if coldesc.var.is_continuous: 930 # continuous from metas have dtype object; cast it to float 931 col_data = col_data.astype(float) 932 return col_data 933 else: 934 return numpy.asarray([self.index(i, column).data(role) 935 for i in range(self.rowCount())]) 936 937 def data(self, index, role, 938 # For optimizing out LOAD_GLOBAL byte code instructions in 939 # the item role tests. 940 _str=str, 941 _Qt_DisplayRole=Qt.DisplayRole, 942 _Qt_EditRole=Qt.EditRole, 943 _Qt_BackgroundRole=Qt.BackgroundRole, 944 _ValueRole=ValueRole, 945 _ClassValueRole=ClassValueRole, 946 _VariableRole=VariableRole, 947 _DomainRole=DomainRole, 948 _VariableStatsRole=VariableStatsRole, 949 # Some cached local precomputed values. 950 # All of the above roles we respond to 951 _recognizedRoles=frozenset([Qt.DisplayRole, 952 Qt.EditRole, 953 Qt.BackgroundRole, 954 ValueRole, 955 ClassValueRole, 956 VariableRole, 957 DomainRole, 958 VariableStatsRole])): 959 """ 960 Reimplemented from `QAbstractItemModel.data` 961 """ 962 if role not in _recognizedRoles: 963 return None 964 965 row, col = index.row(), index.column() 966 if not 0 <= row <= self.__rowCount: 967 return None 968 969 row = self.mapToSourceRows(row) 970 971 try: 972 instance = self._row_instance(row) 973 except IndexError: 974 self.layoutAboutToBeChanged.emit() 975 self.beginRemoveRows(self.parent(), row, max(self.rowCount(), row)) 976 self.__rowCount = min(row, self.__rowCount) 977 self.endRemoveRows() 978 self.layoutChanged.emit() 979 return None 980 coldesc = self.columns[col] 981 982 if role == _Qt_DisplayRole: 983 return coldesc.format(instance) 984 elif role == _Qt_EditRole and isinstance(coldesc, TableModel.Column): 985 return instance[coldesc.var] 986 elif role == _Qt_BackgroundRole: 987 return coldesc.background 988 elif role == _ValueRole and isinstance(coldesc, TableModel.Column): 989 return instance[coldesc.var] 990 elif role == _ClassValueRole: 991 try: 992 return instance.get_class() 993 except TypeError: 994 return None 995 elif role == _VariableRole and isinstance(coldesc, TableModel.Column): 996 return coldesc.var 997 elif role == _DomainRole: 998 return coldesc.role 999 elif role == _VariableStatsRole: 1000 return self._stats_for_column(col) 1001 else: 1002 return None 1003 1004 def setData(self, index, value, role): 1005 row = self.mapFromSourceRows(index.row()) 1006 if role == Qt.EditRole: 1007 try: 1008 self.source[row, index.column()] = value 1009 except (TypeError, IndexError): 1010 return False 1011 else: 1012 self.dataChanged.emit(index, index) 1013 return True 1014 else: 1015 return False 1016 1017 def parent(self, index=QModelIndex()): 1018 """Reimplemented from `QAbstractTableModel.parent`.""" 1019 return QModelIndex() 1020 1021 def rowCount(self, parent=QModelIndex()): 1022 """Reimplemented from `QAbstractTableModel.rowCount`.""" 1023 return 0 if parent.isValid() else self.__rowCount 1024 1025 def columnCount(self, parent=QModelIndex()): 1026 """Reimplemented from `QAbstractTableModel.columnCount`.""" 1027 return 0 if parent.isValid() else self.__columnCount 1028 1029 def headerData(self, section, orientation, role): 1030 """Reimplemented from `QAbstractTableModel.headerData`.""" 1031 if orientation == Qt.Vertical: 1032 if role == Qt.DisplayRole: 1033 return int(self.mapToSourceRows(section) + 1) 1034 return None 1035 1036 coldesc = self.columns[section] 1037 if role == Qt.DisplayRole: 1038 if isinstance(coldesc, TableModel.Basket): 1039 return "{...}" 1040 else: 1041 return coldesc.var.name 1042 elif role == Qt.ToolTipRole: 1043 return self._tooltip(coldesc) 1044 elif role == TableModel.VariableRole \ 1045 and isinstance(coldesc, TableModel.Column): 1046 return coldesc.var 1047 elif role == TableModel.VariableStatsRole: 1048 return self._stats_for_column(section) 1049 elif role == TableModel.DomainRole: 1050 return coldesc.role 1051 else: 1052 return None 1053 1054 def _tooltip(self, coldesc): 1055 """ 1056 Return an header tool tip text for an `column` descriptor. 1057 """ 1058 if isinstance(coldesc, TableModel.Basket): 1059 return None 1060 1061 labels = self._labels 1062 variable = coldesc.var 1063 pairs = [(escape(key), escape(str(variable.attributes[key]))) 1064 for key in labels if key in variable.attributes] 1065 tip = "<b>%s</b>" % escape(variable.name) 1066 tip = "<br/>".join([tip] + ["%s = %s" % pair for pair in pairs]) 1067 return tip 1068 1069 def _stats_for_column(self, column): 1070 """ 1071 Return BasicStats for `column` index. 1072 """ 1073 coldesc = self.columns[column] 1074 if isinstance(coldesc, TableModel.Basket): 1075 return None 1076 1077 if self.__stats is None: 1078 self.__stats = datacaching.getCached( 1079 self.source, basic_stats.DomainBasicStats, 1080 (self.source, True) 1081 ) 1082 1083 return self.__stats[coldesc.var] 1084