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