1import sys 2import threading 3import itertools 4import concurrent.futures 5 6from collections import OrderedDict, namedtuple 7 8from math import isnan 9 10import numpy 11from scipy.sparse import issparse 12 13from AnyQt.QtWidgets import ( 14 QTableView, QHeaderView, QAbstractButton, QApplication, QStyleOptionHeader, 15 QStyle, QStylePainter 16) 17from AnyQt.QtGui import QColor, QClipboard 18from AnyQt.QtCore import ( 19 Qt, QSize, QEvent, QObject, QMetaObject, 20 QAbstractProxyModel, QIdentityProxyModel, QModelIndex, 21 QItemSelectionModel, QItemSelection, QItemSelectionRange, 22) 23from AnyQt.QtCore import pyqtSlot as Slot 24 25import Orange.data 26from Orange.data.storage import Storage 27from Orange.data.table import Table 28from Orange.data.sql.table import SqlTable 29from Orange.statistics import basic_stats 30 31from Orange.widgets import gui 32from Orange.widgets.settings import Setting 33from Orange.widgets.utils.itemdelegates import TableDataDelegate 34from Orange.widgets.utils.itemselectionmodel import ( 35 BlockSelectionModel, ranges, selection_blocks 36) 37from Orange.widgets.utils.tableview import TableView, \ 38 table_selection_to_mime_data 39from Orange.widgets.utils.widgetpreview import WidgetPreview 40from Orange.widgets.widget import OWWidget, Input, Output 41from Orange.widgets.utils import datacaching 42from Orange.widgets.utils.annotated_data import (create_annotated_table, 43 ANNOTATED_DATA_SIGNAL_NAME) 44from Orange.widgets.utils.itemmodels import TableModel 45from Orange.widgets.utils.state_summary import format_summary_details 46 47 48class RichTableModel(TableModel): 49 """A TableModel with some extra bells and whistles/ 50 51 (adds support for gui.BarRole, include variable labels and icons 52 in the header) 53 """ 54 #: Rich header data flags. 55 Name, Labels, Icon = 1, 2, 4 56 57 def __init__(self, sourcedata, parent=None): 58 super().__init__(sourcedata, parent) 59 60 self._header_flags = RichTableModel.Name 61 self._continuous = [var.is_continuous for var in self.vars] 62 labels = [] 63 for var in self.vars: 64 if isinstance(var, Orange.data.Variable): 65 labels.extend(var.attributes.keys()) 66 self._labels = list(sorted( 67 {label for label in labels if not label.startswith("_")})) 68 69 def data(self, index, role=Qt.DisplayRole, 70 # for faster local lookup 71 _BarRole=gui.TableBarItem.BarRole): 72 # pylint: disable=arguments-differ 73 if role == _BarRole and self._continuous[index.column()]: 74 val = super().data(index, TableModel.ValueRole) 75 if val is None or isnan(val): 76 return None 77 78 dist = super().data(index, TableModel.VariableStatsRole) 79 if dist is not None and dist.max > dist.min: 80 return (val - dist.min) / (dist.max - dist.min) 81 else: 82 return None 83 elif role == Qt.TextAlignmentRole and self._continuous[index.column()]: 84 return Qt.AlignRight | Qt.AlignVCenter 85 else: 86 return super().data(index, role) 87 88 def headerData(self, section, orientation, role): 89 if orientation == Qt.Horizontal and role == Qt.DisplayRole: 90 var = super().headerData( 91 section, orientation, TableModel.VariableRole) 92 if var is None: 93 return super().headerData( 94 section, orientation, Qt.DisplayRole) 95 96 lines = [] 97 if self._header_flags & RichTableModel.Name: 98 lines.append(var.name) 99 if self._header_flags & RichTableModel.Labels: 100 lines.extend(str(var.attributes.get(label, "")) 101 for label in self._labels) 102 return "\n".join(lines) 103 elif orientation == Qt.Horizontal and role == Qt.DecorationRole and \ 104 self._header_flags & RichTableModel.Icon: 105 var = super().headerData( 106 section, orientation, TableModel.VariableRole) 107 if var is not None: 108 return gui.attributeIconDict[var] 109 else: 110 return None 111 else: 112 return super().headerData(section, orientation, role) 113 114 def setRichHeaderFlags(self, flags): 115 if flags != self._header_flags: 116 self._header_flags = flags 117 self.headerDataChanged.emit( 118 Qt.Horizontal, 0, self.columnCount() - 1) 119 120 def richHeaderFlags(self): 121 return self._header_flags 122 123 124class TableSliceProxy(QIdentityProxyModel): 125 def __init__(self, parent=None, rowSlice=slice(0, -1), **kwargs): 126 super().__init__(parent, **kwargs) 127 self.__rowslice = rowSlice 128 129 def setRowSlice(self, rowslice): 130 if rowslice.step is not None and rowslice.step != 1: 131 raise ValueError("invalid stride") 132 133 if self.__rowslice != rowslice: 134 self.beginResetModel() 135 self.__rowslice = rowslice 136 self.endResetModel() 137 138 def mapToSource(self, proxyindex): 139 model = self.sourceModel() 140 if model is None or not proxyindex.isValid(): 141 return QModelIndex() 142 143 row, col = proxyindex.row(), proxyindex.column() 144 row = row + self.__rowslice.start 145 assert 0 <= row < model.rowCount() 146 return model.createIndex(row, col, proxyindex.internalPointer()) 147 148 def mapFromSource(self, sourceindex): 149 model = self.sourceModel() 150 if model is None or not sourceindex.isValid(): 151 return QModelIndex() 152 row, col = sourceindex.row(), sourceindex.column() 153 row = row - self.__rowslice.start 154 assert 0 <= row < self.rowCount() 155 return self.createIndex(row, col, sourceindex.internalPointer()) 156 157 def rowCount(self, parent=QModelIndex()): 158 if parent.isValid(): 159 return 0 160 count = super().rowCount() 161 start, stop, step = self.__rowslice.indices(count) 162 assert step == 1 163 return stop - start 164 165 166TableSlot = namedtuple("TableSlot", ["input_id", "table", "summary", "view"]) 167 168 169class DataTableView(gui.HScrollStepMixin, TableView): 170 dataset: Table 171 input_slot: TableSlot 172 173 174class TableBarItemDelegate(gui.TableBarItem, TableDataDelegate): 175 pass 176 177 178class OWDataTable(OWWidget): 179 name = "Data Table" 180 description = "View the dataset in a spreadsheet." 181 icon = "icons/Table.svg" 182 priority = 50 183 keywords = [] 184 185 class Inputs: 186 data = Input("Data", Table, multiple=True, auto_summary=False) 187 188 class Outputs: 189 selected_data = Output("Selected Data", Table, default=True) 190 annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) 191 192 buttons_area_orientation = Qt.Vertical 193 194 show_distributions = Setting(False) 195 dist_color_RGB = Setting((220, 220, 220, 255)) 196 show_attribute_labels = Setting(True) 197 select_rows = Setting(True) 198 auto_commit = Setting(True) 199 200 color_by_class = Setting(True) 201 selected_rows = Setting([], schema_only=True) 202 selected_cols = Setting([], schema_only=True) 203 204 settings_version = 2 205 206 def __init__(self): 207 super().__init__() 208 209 self._inputs = OrderedDict() 210 211 self.__pending_selected_rows = self.selected_rows 212 self.selected_rows = None 213 self.__pending_selected_cols = self.selected_cols 214 self.selected_cols = None 215 216 self.dist_color = QColor(*self.dist_color_RGB) 217 218 info_box = gui.vBox(self.controlArea, "Info") 219 self.info_text = gui.widgetLabel(info_box) 220 self._set_input_summary(None) 221 222 box = gui.vBox(self.controlArea, "Variables") 223 self.c_show_attribute_labels = gui.checkBox( 224 box, self, "show_attribute_labels", 225 "Show variable labels (if present)", 226 callback=self._on_show_variable_labels_changed) 227 228 gui.checkBox(box, self, "show_distributions", 229 'Visualize numeric values', 230 callback=self._on_distribution_color_changed) 231 gui.checkBox(box, self, "color_by_class", 'Color by instance classes', 232 callback=self._on_distribution_color_changed) 233 234 box = gui.vBox(self.controlArea, "Selection") 235 236 gui.checkBox(box, self, "select_rows", "Select full rows", 237 callback=self._on_select_rows_changed) 238 239 gui.rubber(self.controlArea) 240 241 gui.button(self.buttonsArea, self, "Restore Original Order", 242 callback=self.restore_order, 243 tooltip="Show rows in the original order", 244 autoDefault=False, 245 attribute=Qt.WA_LayoutUsesWidgetRect) 246 gui.auto_send(self.buttonsArea, self, "auto_commit") 247 248 # GUI with tabs 249 self.tabs = gui.tabWidget(self.mainArea) 250 self.tabs.currentChanged.connect(self._on_current_tab_changed) 251 252 def copy_to_clipboard(self): 253 self.copy() 254 255 @staticmethod 256 def sizeHint(): 257 return QSize(800, 500) 258 259 @Inputs.data 260 def set_dataset(self, data, tid=None): 261 """Set the input dataset.""" 262 if data is not None: 263 datasetname = getattr(data, "name", "Data") 264 if tid in self._inputs: 265 # update existing input slot 266 slot = self._inputs[tid] 267 view = slot.view 268 # reset the (header) view state. 269 view.setModel(None) 270 view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder) 271 assert self.tabs.indexOf(view) != -1 272 self.tabs.setTabText(self.tabs.indexOf(view), datasetname) 273 else: 274 view = DataTableView() 275 view.setSortingEnabled(True) 276 view.setItemDelegate(TableDataDelegate(view)) 277 278 if self.select_rows: 279 view.setSelectionBehavior(QTableView.SelectRows) 280 281 header = view.horizontalHeader() 282 header.setSectionsMovable(True) 283 header.setSectionsClickable(True) 284 header.setSortIndicatorShown(True) 285 header.setSortIndicator(-1, Qt.AscendingOrder) 286 287 # QHeaderView does not 'reset' the model sort column, 288 # because there is no guaranty (requirement) that the 289 # models understand the -1 sort column. 290 def sort_reset(index, order): 291 if view.model() is not None and index == -1: 292 view.model().sort(index, order) 293 294 header.sortIndicatorChanged.connect(sort_reset) 295 self.tabs.addTab(view, datasetname) 296 297 view.dataset = data 298 self.tabs.setCurrentWidget(view) 299 300 self._setup_table_view(view, data) 301 slot = TableSlot(tid, data, table_summary(data), view) 302 view.input_slot = slot 303 self._inputs[tid] = slot 304 305 self.tabs.setCurrentIndex(self.tabs.indexOf(view)) 306 307 self._set_input_summary(slot) 308 309 if isinstance(slot.summary.len, concurrent.futures.Future): 310 def update(_): 311 QMetaObject.invokeMethod( 312 self, "_update_info", Qt.QueuedConnection) 313 314 slot.summary.len.add_done_callback(update) 315 316 elif tid in self._inputs: 317 slot = self._inputs.pop(tid) 318 view = slot.view 319 view.hide() 320 view.deleteLater() 321 self.tabs.removeTab(self.tabs.indexOf(view)) 322 323 current = self.tabs.currentWidget() 324 if current is not None: 325 self._set_input_summary(current.input_slot) 326 else: 327 self._set_input_summary(None) 328 329 self.tabs.tabBar().setVisible(self.tabs.count() > 1) 330 331 if data and self.__pending_selected_rows is not None: 332 self.selected_rows = self.__pending_selected_rows 333 self.__pending_selected_rows = None 334 else: 335 self.selected_rows = [] 336 337 if data and self.__pending_selected_cols is not None: 338 self.selected_cols = self.__pending_selected_cols 339 self.__pending_selected_cols = None 340 else: 341 self.selected_cols = [] 342 343 self.set_selection() 344 self.unconditional_commit() 345 346 def _setup_table_view(self, view, data): 347 """Setup the `view` (QTableView) with `data` (Orange.data.Table) 348 """ 349 if data is None: 350 view.setModel(None) 351 return 352 353 datamodel = RichTableModel(data) 354 355 rowcount = data.approx_len() 356 357 if self.color_by_class and data.domain.has_discrete_class: 358 color_schema = [ 359 QColor(*c) for c in data.domain.class_var.colors] 360 else: 361 color_schema = None 362 if self.show_distributions: 363 view.setItemDelegate( 364 TableBarItemDelegate( 365 view, color=self.dist_color, color_schema=color_schema) 366 ) 367 else: 368 view.setItemDelegate(TableDataDelegate(view)) 369 370 # Enable/disable view sorting based on data's type 371 view.setSortingEnabled(is_sortable(data)) 372 header = view.horizontalHeader() 373 header.setSectionsClickable(is_sortable(data)) 374 header.setSortIndicatorShown(is_sortable(data)) 375 header.sortIndicatorChanged.connect(self.update_selection) 376 377 view.setModel(datamodel) 378 379 vheader = view.verticalHeader() 380 option = view.viewOptions() 381 size = view.style().sizeFromContents( 382 QStyle.CT_ItemViewItem, option, 383 QSize(20, 20), view) 384 385 vheader.setDefaultSectionSize(size.height() + 2) 386 vheader.setMinimumSectionSize(5) 387 vheader.setSectionResizeMode(QHeaderView.Fixed) 388 389 # Limit the number of rows displayed in the QTableView 390 # (workaround for QTBUG-18490 / QTBUG-28631) 391 maxrows = (2 ** 31 - 1) // (vheader.defaultSectionSize() + 2) 392 if rowcount > maxrows: 393 sliceproxy = TableSliceProxy( 394 parent=view, rowSlice=slice(0, maxrows)) 395 sliceproxy.setSourceModel(datamodel) 396 # First reset the view (without this the header view retains 397 # it's state - at this point invalid/broken) 398 view.setModel(None) 399 view.setModel(sliceproxy) 400 401 assert view.model().rowCount() <= maxrows 402 assert vheader.sectionSize(0) > 1 or datamodel.rowCount() == 0 403 404 # update the header (attribute names) 405 self._update_variable_labels(view) 406 407 selmodel = BlockSelectionModel( 408 view.model(), parent=view, selectBlocks=not self.select_rows) 409 view.setSelectionModel(selmodel) 410 view.selectionFinished.connect(self.update_selection) 411 412 #noinspection PyBroadException 413 def set_corner_text(self, table, text): 414 """Set table corner text.""" 415 # As this is an ugly hack, do everything in 416 # try - except blocks, as it may stop working in newer Qt. 417 # pylint: disable=broad-except 418 if not hasattr(table, "btn") and not hasattr(table, "btnfailed"): 419 try: 420 btn = table.findChild(QAbstractButton) 421 422 class Efc(QObject): 423 @staticmethod 424 def eventFilter(o, e): 425 if (isinstance(o, QAbstractButton) and 426 e.type() == QEvent.Paint): 427 # paint by hand (borrowed from QTableCornerButton) 428 btn = o 429 opt = QStyleOptionHeader() 430 opt.initFrom(btn) 431 state = QStyle.State_None 432 if btn.isEnabled(): 433 state |= QStyle.State_Enabled 434 if btn.isActiveWindow(): 435 state |= QStyle.State_Active 436 if btn.isDown(): 437 state |= QStyle.State_Sunken 438 opt.state = state 439 opt.rect = btn.rect() 440 opt.text = btn.text() 441 opt.position = QStyleOptionHeader.OnlyOneSection 442 painter = QStylePainter(btn) 443 painter.drawControl(QStyle.CE_Header, opt) 444 return True # eat event 445 return False 446 table.efc = Efc() 447 # disconnect default handler for clicks and connect a new one, which supports 448 # both selection and deselection of all data 449 btn.clicked.disconnect() 450 btn.installEventFilter(table.efc) 451 btn.clicked.connect(self._on_select_all) 452 table.btn = btn 453 454 if sys.platform == "darwin": 455 btn.setAttribute(Qt.WA_MacSmallSize) 456 457 except Exception: 458 table.btnfailed = True 459 460 if hasattr(table, "btn"): 461 try: 462 btn = table.btn 463 btn.setText(text) 464 opt = QStyleOptionHeader() 465 opt.text = btn.text() 466 s = btn.style().sizeFromContents( 467 QStyle.CT_HeaderSection, 468 opt, QSize(), 469 btn) 470 if s.isValid(): 471 table.verticalHeader().setMinimumWidth(s.width()) 472 except Exception: 473 pass 474 475 def _set_input_summary(self, slot): 476 def format_summary(summary): 477 if isinstance(summary, ApproxSummary): 478 length = summary.len.result() if summary.len.done() else \ 479 summary.approx_len 480 elif isinstance(summary, Summary): 481 length = summary.len 482 return length 483 484 summary, details = self.info.NoInput, "" 485 if slot: 486 summary = format_summary(slot.summary) 487 details = format_summary_details(slot.table) 488 self.info.set_input_summary(summary, details) 489 490 self.info_text.setText("\n".join(self._info_box_text(slot))) 491 492 @staticmethod 493 def _info_box_text(slot): 494 def format_part(part): 495 if isinstance(part, DenseArray): 496 if not part.nans: 497 return "" 498 perc = 100 * part.nans / (part.nans + part.non_nans) 499 return f" ({perc:.1f} % missing data)" 500 501 if isinstance(part, SparseArray): 502 tag = "sparse" 503 elif isinstance(part, SparseBoolArray): 504 tag = "tags" 505 else: # isinstance(part, NotAvailable) 506 return "" 507 dens = 100 * part.non_nans / (part.nans + part.non_nans) 508 return f" ({tag}, density {dens:.2f} %)" 509 510 def desc(n, part): 511 if n == 0: 512 return f"No {part}s" 513 elif n == 1: 514 return f"1 {part}" 515 else: 516 return f"{n} {part}s" 517 518 if slot is None: 519 return ["No data."] 520 summary = slot.summary 521 text = [] 522 if isinstance(summary, ApproxSummary): 523 if summary.len.done(): 524 text.append(f"{summary.len.result()} instances") 525 else: 526 text.append(f"~{summary.approx_len} instances") 527 elif isinstance(summary, Summary): 528 text.append(f"{summary.len} instances") 529 if sum(p.nans for p in [summary.X, summary.Y, summary.M]) == 0: 530 text[-1] += " (no missing data)" 531 532 text.append(desc(len(summary.domain.attributes), "feature") 533 + format_part(summary.X)) 534 535 if not summary.domain.class_vars: 536 text.append("No target variable.") 537 else: 538 if len(summary.domain.class_vars) > 1: 539 c_text = desc(len(summary.domain.class_vars), "outcome") 540 elif summary.domain.has_continuous_class: 541 c_text = "Numeric outcome" 542 else: 543 c_text = "Target with " \ 544 + desc(len(summary.domain.class_var.values), "value") 545 text.append(c_text + format_part(summary.Y)) 546 547 text.append(desc(len(summary.domain.metas), "meta attribute") 548 + format_part(summary.M)) 549 return text 550 551 def _on_select_all(self, _): 552 data_info = self.tabs.currentWidget().input_slot.summary 553 if len(self.selected_rows) == data_info.len \ 554 and len(self.selected_cols) == len(data_info.domain.variables): 555 self.tabs.currentWidget().clearSelection() 556 else: 557 self.tabs.currentWidget().selectAll() 558 559 def _on_current_tab_changed(self, index): 560 """Update the status bar on current tab change""" 561 view = self.tabs.widget(index) 562 if view is not None and view.model() is not None: 563 self._set_input_summary(view.input_slot) 564 self.update_selection() 565 else: 566 self._set_input_summary(None) 567 568 def _update_variable_labels(self, view): 569 "Update the variable labels visibility for `view`" 570 model = view.model() 571 if isinstance(model, TableSliceProxy): 572 model = model.sourceModel() 573 574 if self.show_attribute_labels: 575 model.setRichHeaderFlags( 576 RichTableModel.Labels | RichTableModel.Name) 577 578 labelnames = set() 579 domain = model.source.domain 580 for a in itertools.chain(domain.metas, domain.variables): 581 labelnames.update(a.attributes.keys()) 582 labelnames = sorted( 583 [label for label in labelnames if not label.startswith("_")]) 584 self.set_corner_text(view, "\n".join([""] + labelnames)) 585 else: 586 model.setRichHeaderFlags(RichTableModel.Name) 587 self.set_corner_text(view, "") 588 589 def _on_show_variable_labels_changed(self): 590 """The variable labels (var.attribues) visibility was changed.""" 591 for slot in self._inputs.values(): 592 self._update_variable_labels(slot.view) 593 594 def _on_distribution_color_changed(self): 595 for ti in range(self.tabs.count()): 596 widget = self.tabs.widget(ti) 597 model = widget.model() 598 while isinstance(model, QAbstractProxyModel): 599 model = model.sourceModel() 600 data = model.source 601 class_var = data.domain.class_var 602 if self.color_by_class and class_var and class_var.is_discrete: 603 color_schema = [QColor(*c) for c in class_var.colors] 604 else: 605 color_schema = None 606 if self.show_distributions: 607 delegate = TableBarItemDelegate(widget, color=self.dist_color, 608 color_schema=color_schema) 609 else: 610 delegate = TableDataDelegate(widget) 611 widget.setItemDelegate(delegate) 612 tab = self.tabs.currentWidget() 613 if tab: 614 tab.reset() 615 616 def _on_select_rows_changed(self): 617 for slot in self._inputs.values(): 618 selection_model = slot.view.selectionModel() 619 selection_model.setSelectBlocks(not self.select_rows) 620 if self.select_rows: 621 slot.view.setSelectionBehavior(QTableView.SelectRows) 622 # Expand the current selection to full row selection. 623 selection_model.select( 624 selection_model.selection(), 625 QItemSelectionModel.Select | QItemSelectionModel.Rows 626 ) 627 else: 628 slot.view.setSelectionBehavior(QTableView.SelectItems) 629 630 def restore_order(self): 631 """Restore the original data order of the current view.""" 632 table = self.tabs.currentWidget() 633 if table is not None: 634 table.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder) 635 636 @Slot() 637 def _update_info(self): 638 current = self.tabs.currentWidget() 639 if current is not None and current.model() is not None: 640 self._set_input_summary(current.input_slot) 641 642 def update_selection(self, *_): 643 self.commit() 644 645 def set_selection(self): 646 if self.selected_rows and self.selected_cols: 647 view = self.tabs.currentWidget() 648 model = view.model() 649 if model.rowCount() <= self.selected_rows[-1] or \ 650 model.columnCount() <= self.selected_cols[-1]: 651 return 652 653 selection = QItemSelection() 654 rowranges = list(ranges(self.selected_rows)) 655 colranges = list(ranges(self.selected_cols)) 656 657 for rowstart, rowend in rowranges: 658 for colstart, colend in colranges: 659 selection.append( 660 QItemSelectionRange( 661 view.model().index(rowstart, colstart), 662 view.model().index(rowend - 1, colend - 1) 663 ) 664 ) 665 view.selectionModel().select( 666 selection, QItemSelectionModel.ClearAndSelect) 667 668 @staticmethod 669 def get_selection(view): 670 """ 671 Return the selected row and column indices of the selection in view. 672 """ 673 selmodel = view.selectionModel() 674 675 selection = selmodel.selection() 676 model = view.model() 677 # map through the proxies into input table. 678 while isinstance(model, QAbstractProxyModel): 679 selection = model.mapSelectionToSource(selection) 680 model = model.sourceModel() 681 682 assert isinstance(selmodel, BlockSelectionModel) 683 assert isinstance(model, TableModel) 684 685 row_spans, col_spans = selection_blocks(selection) 686 rows = list(itertools.chain.from_iterable(itertools.starmap(range, row_spans))) 687 cols = list(itertools.chain.from_iterable(itertools.starmap(range, col_spans))) 688 rows = numpy.array(rows, dtype=numpy.intp) 689 # map the rows through the applied sorting (if any) 690 rows = model.mapToSourceRows(rows) 691 rows = rows.tolist() 692 return rows, cols 693 694 @staticmethod 695 def _get_model(view): 696 model = view.model() 697 while isinstance(model, QAbstractProxyModel): 698 model = model.sourceModel() 699 return model 700 701 def commit(self): 702 """ 703 Commit/send the current selected row/column selection. 704 """ 705 selected_data = table = rowsel = None 706 view = self.tabs.currentWidget() 707 if view and view.model() is not None: 708 model = self._get_model(view) 709 table = model.source # The input data table 710 711 # Selections of individual instances are not implemented 712 # for SqlTables 713 if isinstance(table, SqlTable): 714 self.Outputs.selected_data.send(selected_data) 715 self.Outputs.annotated_data.send(None) 716 return 717 718 rowsel, colsel = self.get_selection(view) 719 self.selected_rows, self.selected_cols = rowsel, colsel 720 721 domain = table.domain 722 723 if len(colsel) < len(domain.variables) + len(domain.metas): 724 # only a subset of the columns is selected 725 allvars = domain.class_vars + domain.metas + domain.attributes 726 columns = [(c, model.headerData(c, Qt.Horizontal, 727 TableModel.DomainRole)) 728 for c in colsel] 729 assert all(role is not None for _, role in columns) 730 731 def select_vars(role): 732 """select variables for role (TableModel.DomainRole)""" 733 return [allvars[c] for c, r in columns if r == role] 734 735 attrs = select_vars(TableModel.Attribute) 736 if attrs and issparse(table.X): 737 # for sparse data you can only select all attributes 738 attrs = table.domain.attributes 739 class_vars = select_vars(TableModel.ClassVar) 740 metas = select_vars(TableModel.Meta) 741 domain = Orange.data.Domain(attrs, class_vars, metas) 742 743 # Send all data by default 744 if not rowsel: 745 selected_data = table 746 else: 747 selected_data = table.from_table(domain, table, rowsel) 748 749 self.Outputs.selected_data.send(selected_data) 750 self.Outputs.annotated_data.send(create_annotated_table(table, rowsel)) 751 752 def copy(self): 753 """ 754 Copy current table selection to the clipboard. 755 """ 756 view = self.tabs.currentWidget() 757 if view is not None: 758 mime = table_selection_to_mime_data(view) 759 QApplication.clipboard().setMimeData( 760 mime, QClipboard.Clipboard 761 ) 762 763 def send_report(self): 764 view = self.tabs.currentWidget() 765 if not view or not view.model(): 766 return 767 model = self._get_model(view) 768 self.report_data_brief(model.source) 769 self.report_table(view) 770 771 772# Table Summary 773 774# Basic statistics for X/Y/metas arrays 775DenseArray = namedtuple( 776 "DenseArray", ["nans", "non_nans", "stats"]) 777SparseArray = namedtuple( 778 "SparseArray", ["nans", "non_nans", "stats"]) 779SparseBoolArray = namedtuple( 780 "SparseBoolArray", ["nans", "non_nans", "stats"]) 781NotAvailable = namedtuple("NotAvailable", []) 782 783#: Orange.data.Table summary 784Summary = namedtuple( 785 "Summary", 786 ["len", "domain", "X", "Y", "M"]) 787 788#: Orange.data.sql.table.SqlTable summary 789ApproxSummary = namedtuple( 790 "ApproxSummary", 791 ["approx_len", "len", "domain", "X", "Y", "M"]) 792 793 794def table_summary(table): 795 if isinstance(table, SqlTable): 796 approx_len = table.approx_len() 797 len_future = concurrent.futures.Future() 798 799 def _len(): 800 len_future.set_result(len(table)) 801 threading.Thread(target=_len).start() # KILL ME !!! 802 803 return ApproxSummary(approx_len, len_future, table.domain, 804 NotAvailable(), NotAvailable(), NotAvailable()) 805 else: 806 domain = table.domain 807 n_instances = len(table) 808 # dist = basic_stats.DomainBasicStats(table, include_metas=True) 809 bstats = datacaching.getCached( 810 table, basic_stats.DomainBasicStats, (table, True) 811 ) 812 813 dist = bstats.stats 814 # pylint: disable=unbalanced-tuple-unpacking 815 X_dist, Y_dist, M_dist = numpy.split( 816 dist, numpy.cumsum([len(domain.attributes), 817 len(domain.class_vars)])) 818 819 def parts(array, density, col_dist): 820 array = numpy.atleast_2d(array) 821 nans = sum([dist.nans for dist in col_dist]) 822 non_nans = sum([dist.non_nans for dist in col_dist]) 823 if density == Storage.DENSE: 824 return DenseArray(nans, non_nans, col_dist) 825 elif density == Storage.SPARSE: 826 return SparseArray(nans, non_nans, col_dist) 827 elif density == Storage.SPARSE_BOOL: 828 return SparseBoolArray(nans, non_nans, col_dist) 829 elif density == Storage.MISSING: 830 return NotAvailable() 831 else: 832 assert False 833 return None 834 835 X_part = parts(table.X, table.X_density(), X_dist) 836 Y_part = parts(table.Y, table.Y_density(), Y_dist) 837 M_part = parts(table.metas, table.metas_density(), M_dist) 838 return Summary(n_instances, domain, X_part, Y_part, M_part) 839 840 841def is_sortable(table): 842 if isinstance(table, SqlTable): 843 return False 844 elif isinstance(table, Orange.data.Table): 845 return True 846 else: 847 return False 848 849 850if __name__ == "__main__": # pragma: no cover 851 WidgetPreview(OWDataTable).run( 852 [(Table("iris"), "iris"), 853 (Table("brown-selected"), "brown-selected"), 854 (Table("housing"), "housing")]) 855