Repository: biolab/orange3

Issue:
Pivot Table: handle empty data
<!-- 
Thanks for taking the time to report a bug!
If you're raising an issue about an add-on (i.e., installed via Options > Add-ons), raise an issue in the relevant add-on's issue tracker instead. See: https://github.com/biolab?q=orange3
To fix the bug, we need to be able to reproduce it. Please answer the following questions to the best of your ability.
-->


- [ ] **What's wrong?**
<!-- Be specific, clear, and concise. Include screenshots if relevant. -->
<!-- If you're getting an error message, copy it, and enclose it with three backticks (```). -->

Pivot Table crashes when sent empty data table.

```
---------------------------- ValueError Exception -----------------------------
Traceback (most recent call last):
  File "/Users/ajda/orange/orange-canvas-core/orangecanvas/scheme/signalmanager.py", line 1024, in __process_next
    if self.__process_next_helper(use_max_active=True):
  File "/Users/ajda/orange/orange-canvas-core/orangecanvas/scheme/signalmanager.py", line 1062, in __process_next_helper
    self.process_node(selected_node)
  File "/Users/ajda/orange/orange-canvas-core/orangecanvas/scheme/signalmanager.py", line 690, in process_node
    self.send_to_node(node, signals_in)
  File "/Users/ajda/orange/orange-widget-base/orangewidget/workflow/widgetsscheme.py", line 806, in send_to_node
    self.process_signals_for_widget(node, widget, signals)
  File "/Users/ajda/orange/orange-widget-base/orangewidget/workflow/widgetsscheme.py", line 820, in process_signals_for_widget
    process_signals_for_widget(widget, signals, workflow)
  File "/Users/ajda/opt/miniconda3/envs/o3/lib/python3.8/functools.py", line 875, in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
  File "/Users/ajda/orange/orange-widget-base/orangewidget/workflow/widgetsscheme.py", line 921, in process_signals_for_widget
    process_signal_input(input_meta, widget, signal, workflow)
  File "/Users/ajda/opt/miniconda3/envs/o3/lib/python3.8/functools.py", line 875, in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
  File "/Users/ajda/orange/orange-widget-base/orangewidget/workflow/widgetsscheme.py", line 884, in process_signal_input_default
    notify_input_helper(
  File "/Users/ajda/opt/miniconda3/envs/o3/lib/python3.8/functools.py", line 875, in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
  File "/Users/ajda/orange/orange-widget-base/orangewidget/utils/signals.py", line 639, in set_input_helper
    handler(*args)
  File "/Users/ajda/orange/orange-widget-base/orangewidget/utils/signals.py", line 197, in summarize_wrapper
    method(widget, value)
  File "/Users/ajda/orange/orange3/Orange/widgets/utils/sql.py", line 31, in new_f
    return f(widget, data, *args, **kwargs)
  File "/Users/ajda/orange/orange3/Orange/widgets/data/owpivot.py", line 900, in set_data
    self.unconditional_commit()
  File "/Users/ajda/orange/orange3/Orange/widgets/data/owpivot.py", line 945, in commit
    col = self.data.get_column_view(col_var)[0].astype(np.float)
  File "/Users/ajda/orange/orange3/Orange/data/table.py", line 1331, in get_column_view
    col_index = self.domain.index(index)
  File "/Users/ajda/orange/orange3/Orange/data/domain.py", line 352, in index
    raise ValueError("'%s' is not in domain" % var)
ValueError: 'None' is not in domain

```

- [ ] **How can we reproduce the problem?** 
<!-- Upload a zip with the .ows file and data. -->
<!-- Describe the steps (open this widget, click there, then add this...) -->

File - Select Columns (move everything to Ignored) - Pivot Table.



- [ ] **What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: Big Sur
- Orange version: 3.30.dev
- How you installed Orange: git



Discussion:

Resolving patch:
diff --git a/Orange/widgets/data/owpivot.py b/Orange/widgets/data/owpivot.py
--- a/Orange/widgets/data/owpivot.py
+++ b/Orange/widgets/data/owpivot.py
@@ -744,6 +744,7 @@ class Warning(OWWidget.Warning):
         renamed_vars = Msg("Some variables have been renamed in some tables"
                            "to avoid duplicates.\n{}")
         too_many_values = Msg("Selected variable has too many values.")
+        no_variables = Msg("At least 1 primitive variable is required.")
 
     settingsHandler = DomainContextHandler()
     row_feature = ContextSetting(None)
@@ -854,12 +855,21 @@ def no_col_feature(self):
     def skipped_aggs(self):
         def add(fun):
             data, var = self.data, self.val_feature
+            primitive_funcs = Pivot.ContVarFunctions + Pivot.DiscVarFunctions
             return data and not var and fun not in Pivot.AutonomousFunctions \
                 or var and var.is_discrete and fun in Pivot.ContVarFunctions \
-                or var and var.is_continuous and fun in Pivot.DiscVarFunctions
+                or var and var.is_continuous and fun in Pivot.DiscVarFunctions \
+                or var and not var.is_primitive() and fun in primitive_funcs
         skipped = [str(fun) for fun in self.sel_agg_functions if add(fun)]
         return ", ".join(sorted(skipped))
 
+    @property
+    def data_has_primitives(self):
+        if not self.data:
+            return False
+        domain = self.data.domain
+        return any(v.is_primitive() for v in domain.variables + domain.metas)
+
     def __feature_changed(self):
         self.selection = set()
         self.pivot = None
@@ -867,7 +877,7 @@ def __feature_changed(self):
 
     def __val_feature_changed(self):
         self.selection = set()
-        if self.no_col_feature:
+        if self.no_col_feature or not self.pivot:
             return
         self.pivot.update_pivot_table(self.val_feature)
         self.commit()
@@ -896,7 +906,8 @@ def set_data(self, data):
         self.pivot = None
         self.check_data()
         self.init_attr_values()
-        self.openContext(self.data)
+        if self.data_has_primitives:
+            self.openContext(self.data)
         self.unconditional_commit()
 
     def check_data(self):
@@ -912,8 +923,8 @@ def init_attr_values(self):
             self.row_feature = model[0]
         model = self.controls.val_feature.model()
         if model and len(model) > 2:
-            self.val_feature = domain.variables[0] \
-                if domain.variables[0] in model else model[2]
+            allvars = domain.variables + domain.metas
+            self.val_feature = allvars[0] if allvars[0] in model else model[2]
 
     def commit(self):
         def send_outputs(pivot_table, filtered_data, grouped_data):
@@ -933,9 +944,16 @@ def send_outputs(pivot_table, filtered_data, grouped_data):
         self.Warning.cannot_aggregate.clear()
         self.Warning.no_col_feature.clear()
 
+        self.table_view.clear()
+
         if self.pivot is None:
+            if self.data:
+                if not self.data_has_primitives:
+                    self.Warning.no_variables()
+                    send_outputs(None, None, None)
+                    return
+
             if self.no_col_feature:
-                self.table_view.clear()
                 self.Warning.no_col_feature()
                 send_outputs(None, None, None)
                 return
@@ -965,7 +983,6 @@ def send_outputs(pivot_table, filtered_data, grouped_data):
             self.Warning.renamed_vars(self.pivot.renamed)
 
     def _update_graph(self):
-        self.table_view.clear()
         if self.pivot.pivot_table:
             col_feature = self.col_feature or self.row_feature
             self.table_view.update_table(col_feature.name,


Test patch:
diff --git a/Orange/widgets/data/tests/test_owpivot.py b/Orange/widgets/data/tests/test_owpivot.py
--- a/Orange/widgets/data/tests/test_owpivot.py
+++ b/Orange/widgets/data/tests/test_owpivot.py
@@ -278,6 +278,32 @@ def test_table_values(self):
         self.assertEqual(model.data(model.index(4, 4)), "114.0")
         self.assertEqual(model.data(model.index(5, 4)), "reversable defect")
 
+    def test_only_metas_table(self):
+        self.send_signal(self.widget.Inputs.data, self.zoo[:, 17:])
+        self.assertTrue(self.widget.Warning.no_variables.is_shown())
+
+        data = self.zoo.transform(Domain([], metas=self.zoo.domain.attributes))
+        self.send_signal(self.widget.Inputs.data, data)
+        self.assertFalse(self.widget.Warning.no_variables.is_shown())
+
+    def test_empty_table(self):
+        data = self.heart_disease[:, :0]
+        self.send_signal(self.widget.Inputs.data, data)
+        self.assertTrue(self.widget.Warning.no_variables.is_shown())
+        self.send_signal(self.widget.Inputs.data, None)
+        self.assertFalse(self.widget.Warning.no_variables.is_shown())
+
+        data = self.heart_disease
+        self.send_signal(self.widget.Inputs.data, data)
+
+        zoo_domain = self.zoo.domain
+        data = self.zoo.transform(Domain([], metas=zoo_domain.metas))
+        self.send_signal(self.widget.Inputs.data, data)
+
+        domain = Domain([], zoo_domain.class_vars, metas=zoo_domain.metas)
+        data = self.zoo.transform(domain)
+        self.send_signal(self.widget.Inputs.data, data)
+
 
 class TestAggregationFunctionsEnum(unittest.TestCase):
     def test_pickle(self):