Repository: biolab/orange3

Issue:
ValueError:  zero-size array to reduction operation minimum which has no identity
https://sentry.io/biolab/orange3/issues/812964821/?referrer=github_plugin

```
ValueError:  zero-size array to reduction operation minimum which has no identity
(5 additional frame(s) were not displayed)
...
  Module "Orange.widgets.visualize.owscatterplotgraph", line 528, in reset_graph

    self.update_coordinates()
  Module "Orange.widgets.visualize.owscatterplot", line 122, in update_coordinates

    self.update_regression_line()
  Module "Orange.widgets.visualize.owscatterplot", line 160, in update_regression_line

    min_x, max_x = np.min(x), np.max(x)
  Module "numpy.core.fromnumeric", line 2442, in amin

    initial=initial)
  Module "numpy.core.fromnumeric", line 83, in _wrapreduction

    return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
```


Discussion:

Resolving patch:
diff --git a/Orange/data/util.py b/Orange/data/util.py
--- a/Orange/data/util.py
+++ b/Orange/data/util.py
@@ -95,6 +95,22 @@ def hstack(arrays):
         return np.hstack(arrays)
 
 
+def array_equal(a1, a2):
+    """array_equal that supports sparse and dense arrays with missing values"""
+    if a1.shape != a2.shape:
+        return False
+
+    if not (sp.issparse(a1) or sp.issparse(a2)):  # Both dense: just compare
+        return np.allclose(a1, a2, equal_nan=True)
+
+    v1 = np.vstack(sp.find(a1)).T
+    v2 = np.vstack(sp.find(a2)).T
+    if not (sp.issparse(a1) and sp.issparse(a2)):  # Any dense: order indices
+        v1.sort(axis=0)
+        v2.sort(axis=0)
+    return np.allclose(v1, v2, equal_nan=True)
+
+
 def assure_array_dense(a):
     if sp.issparse(a):
         a = a.toarray()
diff --git a/Orange/widgets/visualize/utils/widget.py b/Orange/widgets/visualize/utils/widget.py
--- a/Orange/widgets/visualize/utils/widget.py
+++ b/Orange/widgets/visualize/utils/widget.py
@@ -8,7 +8,7 @@
 from Orange.data import (
     Table, ContinuousVariable, Domain, Variable, StringVariable
 )
-from Orange.data.util import get_unique_names
+from Orange.data.util import get_unique_names, array_equal
 from Orange.data.sql.table import SqlTable
 from Orange.preprocess.preprocess import Preprocess, ApplyDomain
 from Orange.statistics.util import bincount
@@ -437,12 +437,10 @@ def set_data(self, data):
         self.use_context()
         self._invalidated = not (
             data_existed and self.data is not None and
-            effective_data.X.shape == self.effective_data.X.shape and
-            np.allclose(effective_data.X,
-                        self.effective_data.X, equal_nan=True))
+            array_equal(effective_data.X, self.effective_data.X))
         if self._invalidated:
             self.clear()
-        self.cb_class_density.setEnabled(self.can_draw_density())
+        self.enable_controls()
 
     def check_data(self):
         self.valid_data = None
@@ -451,6 +449,9 @@ def check_data(self):
     def use_context(self):
         pass
 
+    def enable_controls(self):
+        self.cb_class_density.setEnabled(self.can_draw_density())
+
     @Inputs.data_subset
     @check_sql_input
     def set_subset_data(self, subset):
@@ -500,8 +501,9 @@ def get_embedding(self):
 
     def get_coordinates_data(self):
         embedding = self.get_embedding()
-        return embedding[self.valid_data].T[:2] if embedding is not None \
-            else (None, None)
+        if embedding is not None and len(embedding[self.valid_data]):
+            return embedding[self.valid_data].T
+        return None, None
 
     def setup_plot(self):
         self.graph.reset_graph()


Test patch:
diff --git a/Orange/tests/test_util.py b/Orange/tests/test_util.py
--- a/Orange/tests/test_util.py
+++ b/Orange/tests/test_util.py
@@ -8,8 +8,9 @@
 from Orange.util import export_globals, flatten, deprecated, try_, deepgetattr, \
     OrangeDeprecationWarning
 from Orange.data import Table
-from Orange.data.util import vstack, hstack
+from Orange.data.util import vstack, hstack, array_equal
 from Orange.statistics.util import stats
+from Orange.tests.test_statistics import dense_sparse
 
 SOMETHING = 0xf00babe
 
@@ -118,3 +119,42 @@ def test_stats_sparse(self):
         data = Table("iris")
         sparse_x = sp.csr_matrix(data.X)
         self.assertTrue(stats(data.X).all() == stats(sparse_x).all())
+
+    @dense_sparse
+    def test_array_equal(self, array):
+        a1 = array([[0., 2.], [3., np.nan]])
+        a2 = array([[0., 2.], [3., np.nan]])
+        self.assertTrue(array_equal(a1, a2))
+
+        a3 = np.array([[0., 2.], [3., np.nan]])
+        self.assertTrue(array_equal(a1, a3))
+        self.assertTrue(array_equal(a3, a1))
+
+    @dense_sparse
+    def test_array_not_equal(self, array):
+        a1 = array([[0., 2.], [3., np.nan]])
+        a2 = array([[0., 2.], [4., np.nan]])
+        self.assertFalse(array_equal(a1, a2))
+
+        a3 = array([[0., 2.], [3., np.nan], [4., 5.]])
+        self.assertFalse(array_equal(a1, a3))
+
+    def test_csc_array_equal(self):
+        a1 = sp.csc_matrix(([1, 4, 5], ([0, 0, 1], [0, 2, 2])), shape=(2, 3))
+        a2 = sp.csc_matrix(([5, 1, 4], ([1, 0, 0], [2, 0, 2])), shape=(2, 3))
+        a2[0, 1] = 0  # explicitly setting to 0
+        self.assertTrue(array_equal(a1, a2))
+
+    def test_csc_scr_equal(self):
+        a1 = sp.csc_matrix(([1, 4, 5], ([0, 0, 1], [0, 2, 2])), shape=(2, 3))
+        a2 = sp.csr_matrix(([5, 1, 4], ([1, 0, 0], [2, 0, 2])), shape=(2, 3))
+        self.assertTrue(array_equal(a1, a2))
+
+        a1 = sp.csc_matrix(([1, 4, 5], ([0, 0, 1], [0, 2, 2])), shape=(2, 3))
+        a2 = sp.csr_matrix(([1, 4, 5], ([0, 0, 1], [0, 2, 2])), shape=(2, 3))
+        self.assertTrue(array_equal(a1, a2))
+
+    def test_csc_unordered_array_equal(self):
+        a1 = sp.csc_matrix(([1, 4, 5], [0, 0, 1], [0, 1, 1, 3]), shape=(2, 3))
+        a2 = sp.csc_matrix(([1, 5, 4], [0, 1, 0], [0, 1, 1, 3]), shape=(2, 3))
+        self.assertTrue(array_equal(a1, a2))
diff --git a/Orange/widgets/visualize/tests/test_owprojectionwidget.py b/Orange/widgets/visualize/tests/test_owprojectionwidget.py
--- a/Orange/widgets/visualize/tests/test_owprojectionwidget.py
+++ b/Orange/widgets/visualize/tests/test_owprojectionwidget.py
@@ -1,7 +1,7 @@
 # Test methods with long descriptive names can omit docstrings
 # pylint: disable=missing-docstring
 import unittest
-from unittest.mock import patch
+from unittest.mock import patch, Mock
 
 import numpy as np
 
@@ -199,5 +199,22 @@ def test_invalid_subset(self):
         self.assertFalse(widget.Warning.subset_independent.is_shown())
         self.assertFalse(widget.Warning.subset_not_subset.is_shown())
 
+    def test_get_coordinates_data(self):
+        self.widget.get_embedding = Mock(return_value=np.ones((10, 2)))
+        self.widget.valid_data = np.ones((10,), dtype=bool)
+        self.widget.valid_data[0] = False
+        self.assertIsNotNone(self.widget.get_coordinates_data())
+        self.assertEqual(len(self.widget.get_coordinates_data()[0]), 9)
+        self.widget.valid_data = np.zeros((10,), dtype=bool)
+        self.assertIsNone(self.widget.get_coordinates_data()[0])
+
+    def test_sparse_data_reload(self):
+        table = Table("heart_disease").to_sparse()
+        self.widget.setup_plot = Mock()
+        self.send_signal(self.widget.Inputs.data, table)
+        self.send_signal(self.widget.Inputs.data, table)
+        self.widget.setup_plot.assert_called_once()
+
+
 if __name__ == "__main__":
     unittest.main()
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
@@ -735,6 +735,13 @@ def test_on_manual_change(self):
         selection = vizrank.rank_table.selectedIndexes()
         self.assertEqual(len(selection), 0)
 
+    def test_regression_line(self):
+        self.widget.graph.controls.show_reg_line.setChecked(True)
+        self.send_signal(self.widget.Inputs.data, self.data)
+        data = self.data.copy()
+        data[:, 0] = np.nan
+        self.send_signal(self.widget.Inputs.data, data)
+
 
 if __name__ == "__main__":
     import unittest