Repository: biolab/orange3

Issue:
Scatter Plot: crashes on sparse input change
<!--
This is an issue template. Please fill in the relevant details in the
sections below.

Wrap code and verbatim terminal window output into triple backticks, see:
https://help.github.com/articles/basic-writing-and-formatting-syntax/#quoting-code

If you're raising an issue about an add-on (e.g. installed via
Options > Add-ons), raise an issue on the relevant add-on's issue
tracker instead. See: https://github.com/biolab?q=orange3
-->

##### Orange version
<!-- From menu _Help→About→Version_ or code `Orange.version.full_version` -->
3.22.dev

##### Expected behavior
Scatter Plot doesn't crash.


##### Actual behavior
Scatter Plot crashes.


##### Steps to reproduce the behavior
Corpus - BoW - Scatter Plot. Now change the Term Frequency type to any other.


##### Additional info (worksheets, data, screenshots, ...)
`TypeError: descriptors must be instances of Variable, not 'NoneType'`




Discussion:
@VesnaT, this happens because `Orange.widgets.visualize.utils.widget.OWDataProjectionWidget.set_data` has

```
        data_existed = self.data is not None
        effective_data = self.effective_data if data_existed else None
```

`effective_data` is a property which constructs a `Domain` from `effective variables, and effective variables are in scatter plot defined as

```
    @property
    def effective_variables(self):
        return [self.attr_x, self.attr_y]
```

However, `self.attr_x` and `self.attr_y` are `None`, so `Domain` fails.

This is easily fixed by changing `effective_variables` to

```
    @property
    def effective_variables(self):
        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []
```

But the actual bug occurs earlier. First, if `self.attr_x` and `self.attr_y` are `None`, then `self.data` should be `None`, too; this is inconsistent. Second, in the schema that @ajdapretnar constructed, there are useful (albeit sparse) numeric attributes, so `attr_x` and `attr_y` should be non-`None`.
Upon some further investigation: all numeric attributes are hidden, so they don't appear in `xy_model`. `xy_model` is empty, so `self.attr_x = self.xy_model[0] if self.xy_model else None` (in `init_attr_values`) sets `attr_x` to `None` without setting `self.data` to `None`.

We should either allow `init_attr_values` to set `self.data` to `None` (and then `set_data` has to check `self.data` again after calling `init_attr_values`) **or** we should patch `effective_variables` as above.

Resolving patch:
diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py
--- a/Orange/widgets/visualize/owscatterplot.py
+++ b/Orange/widgets/visualize/owscatterplot.py
@@ -297,7 +297,7 @@ def _add_controls_sampling(self):
 
     @property
     def effective_variables(self):
-        return [self.attr_x, self.attr_y]
+        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []
 
     def _vizrank_color_change(self):
         self.vizrank.initialize()


Test patch:
diff --git a/Orange/widgets/tests/base.py b/Orange/widgets/tests/base.py
--- a/Orange/widgets/tests/base.py
+++ b/Orange/widgets/tests/base.py
@@ -730,6 +730,21 @@ def test_send_report(self, timeout=DEFAULT_TIMEOUT):
         self.send_signal(self.widget.Inputs.data, None)
         self.widget.report_button.click()
 
+    def test_hidden_effective_variables(self, timeout=DEFAULT_TIMEOUT):
+        hidden_var1 = ContinuousVariable("c1")
+        hidden_var1.attributes["hidden"] = True
+        hidden_var2 = ContinuousVariable("c2")
+        hidden_var2.attributes["hidden"] = True
+        class_vars = [DiscreteVariable("cls", values=["a", "b"])]
+        table = Table(Domain([hidden_var1, hidden_var2], class_vars),
+                      np.array([[0., 1.], [2., 3.]]),
+                      np.array([[0.], [1.]]))
+        self.send_signal(self.widget.Inputs.data, table)
+        if self.widget.isBlocking():
+            spy = QSignalSpy(self.widget.blockingStateChanged)
+            self.assertTrue(spy.wait(timeout))
+        self.send_signal(self.widget.Inputs.data, table)
+
 
 class AnchorProjectionWidgetTestMixin(ProjectionWidgetTestMixin):
     def test_embedding_missing_values(self):
diff --git a/Orange/widgets/visualize/tests/test_owscatterplot.py b/Orange/widgets/visualize/tests/test_owscatterplot.py
--- a/Orange/widgets/visualize/tests/test_owscatterplot.py
+++ b/Orange/widgets/visualize/tests/test_owscatterplot.py
@@ -634,7 +634,7 @@ def test_invalidated_features_first(self):
         # send features (same as default ones)
         self.send_signal(self.widget.Inputs.features,
                          AttributeList(self.data.domain.attributes[:2]))
-        self.assertListEqual(self.widget.effective_variables, [None, None])
+        self.assertListEqual(self.widget.effective_variables, [])
         self.widget.setup_plot.assert_called_once()
 
         # send data
@@ -685,7 +685,7 @@ def test_invalidated_diff_features_features_first(self):
         # send features (not the same as defaults)
         self.send_signal(self.widget.Inputs.features,
                          AttributeList(self.data.domain.attributes[2:4]))
-        self.assertListEqual(self.widget.effective_variables, [None, None])
+        self.assertListEqual(self.widget.effective_variables, [])
         self.widget.setup_plot.assert_called_once()
 
         # send data