Repository: biolab/orange3

Issue:
tSNE widget raises an error on data with only one variable
**Describe the bug**

tSNE widget raises an error on data with only one variable in X or when only one variable has non-nan data.

**To Reproduce**
Steps to reproduce the behavior:
1. In File widget select iris
2. Connect it to Select columns where you leave only one variable in features.
3. Connect to tSNE and it raises an error.

**Orange version:**
3.24


**Additional context**
```
Traceback (most recent call last):
  File "/Users/primoz/Documents/orange3/Orange/widgets/utils/concurrent.py", line 596, in _on_task_done
    super()._on_task_done(future)
  File "/Users/primoz/Documents/orange3/Orange/widgets/utils/concurrent.py", line 552, in _on_task_done
    self.on_exception(ex)
  File "/Users/primoz/Documents/orange3/Orange/widgets/utils/concurrent.py", line 492, in on_exception
    raise ex
  File "/Users/primoz/miniconda3/envs/orange/lib/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/Users/primoz/Documents/orange3/Orange/widgets/unsupervised/owtsne.py", line 217, in run
    job(progress_callback=_progress_callback)
  File "/Users/primoz/Documents/orange3/Orange/widgets/unsupervised/owtsne.py", line 107, in compute_initialization
    task.initialization = task.tsne.compute_initialization(task.pca_projection.X)
  File "/Users/primoz/Documents/orange3/Orange/projection/manifold.py", line 454, in compute_initialization
    X, self.n_components, random_state=self.random_state
  File "/Users/primoz/miniconda3/envs/orange/lib/python3.7/site-packages/openTSNE/initialization.py", line 58, in pca
    embedding = pca_.fit_transform(X)
  File "/Users/primoz/miniconda3/envs/orange/lib/python3.7/site-packages/sklearn/decomposition/_pca.py", line 369, in fit_transform
    U, S, V = self._fit(X)
  File "/Users/primoz/miniconda3/envs/orange/lib/python3.7/site-packages/sklearn/decomposition/_pca.py", line 416, in _fit
    return self._fit_full(X, n_components)
  File "/Users/primoz/miniconda3/envs/orange/lib/python3.7/site-packages/sklearn/decomposition/_pca.py", line 435, in _fit_full
    % (n_components, min(n_samples, n_features)))
ValueError: n_components=2 must be between 0 and min(n_samples, n_features)=1 with svd_solver='full'
```





Discussion:

Resolving patch:
diff --git a/Orange/widgets/unsupervised/owtsne.py b/Orange/widgets/unsupervised/owtsne.py
--- a/Orange/widgets/unsupervised/owtsne.py
+++ b/Orange/widgets/unsupervised/owtsne.py
@@ -278,8 +278,8 @@ class Information(OWDataProjectionWidget.Information):
 
     class Error(OWDataProjectionWidget.Error):
         not_enough_rows = Msg("Input data needs at least 2 rows")
+        not_enough_cols = Msg("Input data needs at least 2 attributes")
         constant_data = Msg("Input data is constant")
-        no_attributes = Msg("Data has no attributes")
         no_valid_data = Msg("No projection due to no valid data")
 
     def __init__(self):
@@ -387,8 +387,8 @@ def error(err):
         if len(self.data) < 2:
             error(self.Error.not_enough_rows)
 
-        elif not self.data.domain.attributes:
-            error(self.Error.no_attributes)
+        elif len(self.data.domain.attributes) < 2:
+            error(self.Error.not_enough_cols)
 
         elif not self.data.is_sparse():
             if np.all(~np.isfinite(self.data.X)):


Test patch:
diff --git a/Orange/widgets/unsupervised/tests/test_owtsne.py b/Orange/widgets/unsupervised/tests/test_owtsne.py
--- a/Orange/widgets/unsupervised/tests/test_owtsne.py
+++ b/Orange/widgets/unsupervised/tests/test_owtsne.py
@@ -70,49 +70,57 @@ def restore_mocked_functions(self):
 
     def test_wrong_input(self):
         # no data
-        self.data = None
-        self.send_signal(self.widget.Inputs.data, self.data)
+        data = None
+        self.send_signal(self.widget.Inputs.data, data)
         self.wait_until_stop_blocking()
         self.assertIsNone(self.widget.data)
 
         # <2 rows
-        self.data = Table.from_list(self.domain, [[1, 2, 3, 4, 5, 'STG1']])
-        self.send_signal(self.widget.Inputs.data, self.data)
+        data = Table.from_list(self.domain, [[1, 2, 3, 4, 5, 'STG1']])
+        self.send_signal(self.widget.Inputs.data, data)
         self.wait_until_stop_blocking()
         self.assertIsNone(self.widget.data)
         self.assertTrue(self.widget.Error.not_enough_rows.is_shown())
 
         # no attributes
-        self.data = Table.from_list(self.empty_domain, [['STG1']] * 2)
-        self.send_signal(self.widget.Inputs.data, self.data)
+        data = Table.from_list(self.empty_domain, [['STG1']] * 2)
+        self.send_signal(self.widget.Inputs.data, data)
         self.wait_until_stop_blocking()
         self.assertIsNone(self.widget.data)
-        self.assertTrue(self.widget.Error.no_attributes.is_shown())
+        self.assertTrue(self.widget.Error.not_enough_cols.is_shown())
+
+        # one attributes
+        data = Table.from_list(self.empty_domain, [[1, 'STG1'],
+                                                   [2, 'STG1']])
+        self.send_signal(self.widget.Inputs.data, data)
+        self.wait_until_stop_blocking()
+        self.assertIsNone(self.widget.data)
+        self.assertTrue(self.widget.Error.not_enough_cols.is_shown())
 
         # constant data
-        self.data = Table.from_list(self.domain, [[1, 2, 3, 4, 5, 'STG1']] * 2)
-        self.send_signal(self.widget.Inputs.data, self.data)
+        data = Table.from_list(self.domain, [[1, 2, 3, 4, 5, 'STG1']] * 2)
+        self.send_signal(self.widget.Inputs.data, data)
         self.wait_until_stop_blocking()
         self.assertIsNone(self.widget.data)
         self.assertTrue(self.widget.Error.constant_data.is_shown())
 
         # correct input
-        self.data = Table.from_list(self.domain, [[1, 2, 3, 4, 5, 'STG1'],
-                                                  [5, 4, 3, 2, 1, 'STG1']])
-        self.send_signal(self.widget.Inputs.data, self.data)
+        data = Table.from_list(self.domain, [[1, 2, 3, 4, 5, 'STG1'],
+                                             [5, 4, 3, 2, 1, 'STG1']])
+        self.send_signal(self.widget.Inputs.data, data)
         self.wait_until_stop_blocking()
         self.assertIsNotNone(self.widget.data)
         self.assertFalse(self.widget.Error.not_enough_rows.is_shown())
-        self.assertFalse(self.widget.Error.no_attributes.is_shown())
+        self.assertFalse(self.widget.Error.not_enough_cols.is_shown())
         self.assertFalse(self.widget.Error.constant_data.is_shown())
 
     def test_input(self):
-        self.data = Table.from_list(self.domain, [[1, 1, 1, 1, 1, 'STG1'],
-                                                  [2, 2, 2, 2, 2, 'STG1'],
-                                                  [4, 4, 4, 4, 4, 'STG2'],
-                                                  [5, 5, 5, 5, 5, 'STG2']])
+        data = Table.from_list(self.domain, [[1, 1, 1, 1, 1, 'STG1'],
+                                             [2, 2, 2, 2, 2, 'STG1'],
+                                             [4, 4, 4, 4, 4, 'STG2'],
+                                             [5, 5, 5, 5, 5, 'STG2']])
 
-        self.send_signal(self.widget.Inputs.data, self.data)
+        self.send_signal(self.widget.Inputs.data, data)
         self.wait_until_stop_blocking()
 
     def test_attr_models(self):