Repository: biolab/orange3

Issue:
Mosaic: crash on data and subset data
##### Orange version
From at least 3.3.8 onward


##### Expected behavior
Should not crash. Subset data properly interpreted.


##### Actual behavior
![screenshot_20170803_122307](https://user-images.githubusercontent.com/22157215/28917621-8c5d37b4-7846-11e7-9110-e13070d451c5.png)

https://sentry.io/biolab/orange3/issues/252105951/






Discussion:

Resolving patch:
diff --git a/Orange/widgets/visualize/owmosaic.py b/Orange/widgets/visualize/owmosaic.py
--- a/Orange/widgets/visualize/owmosaic.py
+++ b/Orange/widgets/visualize/owmosaic.py
@@ -4,6 +4,7 @@
 from math import sqrt, log
 from operator import mul, attrgetter
 
+import numpy as np
 from scipy.stats import distributions
 from scipy.misc import comb
 from AnyQt.QtCore import Qt, QSize, pyqtSignal as Signal
@@ -303,8 +304,8 @@ def __init__(self):
 
         self.data = None
         self.discrete_data = None
-        self.unprocessed_subset_data = None
         self.subset_data = None
+        self.subset_indices = None
 
         self.color_data = None
 
@@ -379,6 +380,7 @@ def init_combos(self, data):
         if data is None:
             self.color_model.set_domain(None)
             return
+        self.color_model.set_domain(self.data.domain)
         for combo in self.attr_combos[1:]:
             combo.addItem("(None)")
 
@@ -438,33 +440,28 @@ def set_data(self, data):
             self.init_combos(None)
             return
 
-        self.color_model.set_domain(self.data.domain)
         self.init_combos(self.data)
 
         self.openContext(self.data)
 
-        # if we first received subset we now call setSubsetData to process it
-        if self.unprocessed_subset_data:
-            self.set_subset_data(self.unprocessed_subset_data)
-            self.unprocessed_subset_data = None
-
-        self.set_color_data()
-
     @Inputs.data_subset
     def set_subset_data(self, data):
-        self.Warning.incompatible_subset.clear()
-        if self.data is None:
-            self.unprocessed_subset_data = data
-            return
-        try:
-            self.subset_data = data.transform(self.data.domain)
-        except:
-            self.subset_data = None
-            self.Warning.incompatible_subset(shown=data is not None)
+        self.subset_data = data
 
     # this is called by widget after setData and setSubsetData are called.
     # this way the graph is updated only once
     def handleNewSignals(self):
+        self.Warning.incompatible_subset.clear()
+        self.subset_indices = indices = None
+        if self.data is not None and self.subset_data:
+            transformed = self.subset_data.transform(self.data.domain)
+            if np.all(np.isnan(transformed.X)) and np.all(np.isnan(transformed.Y)):
+                self.Warning.incompatible_subset()
+            else:
+                indices = {e.id for e in transformed}
+                self.subset_indices = [ex.id in indices for ex in self.data]
+
+        self.set_color_data()
         self.reset_graph()
 
     def clear_selection(self):
@@ -832,13 +829,7 @@ def line(x1, y1, x2, y2):
 
                 if conditionalsubsetdict:
                     if conditionalsubsetdict[attr_vals]:
-                        counts = [conditionalsubsetdict[attr_vals + "-" + val]
-                                  for val in cls_values]
-                        if sum(counts) == 1:
-                            rect(x0 - 2, y0 - 2, x1 - x0 + 5, y1 - y0 + 5, -550,
-                                 colors[counts.index(1)], Qt.white,
-                                 penWidth=2, penStyle=Qt.DashLine)
-                        if self.subset_data is not None:
+                        if self.subset_indices is not None:
                             line(x1 - bar_width, y0, x1 - bar_width, y1)
                             total = 0
                             n = conditionalsubsetdict[attr_vals]
@@ -918,7 +909,6 @@ def draw_legend(x0_x1, y0_y1):
         data = self.discrete_data
         if data is None:
             return
-        subset = self.subset_data
         attr_list = self.get_attr_list()
         class_var = data.domain.class_var
         if class_var:
@@ -991,9 +981,9 @@ def get_max_label_width(attr):
         conditionaldict, distributiondict = \
             get_conditional_distribution(data, attr_list)
         conditionalsubsetdict = None
-        if subset:
+        if self.subset_indices:
             conditionalsubsetdict, _ = \
-                get_conditional_distribution(subset, attr_list)
+                get_conditional_distribution(self.discrete_data[self.subset_indices], attr_list)
 
         # draw rectangles
         draw_data(
@@ -1050,6 +1040,7 @@ def main():
     ow.show()
     data = Table("zoo.tab")
     ow.set_data(data)
+    ow.set_subset_data(data[::10])
     ow.handleNewSignals()
     a.exec_()
 


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owmosaic.py b/Orange/widgets/visualize/tests/test_owmosaic.py
--- a/Orange/widgets/visualize/tests/test_owmosaic.py
+++ b/Orange/widgets/visualize/tests/test_owmosaic.py
@@ -124,10 +124,10 @@ def test_count(self):
         metas = [data.domain.attributes[0]]
         domain = Domain(attributes, data.domain.class_var, metas)
         new_data = data.from_table(domain, data)
-        widget.set_data(new_data)
+        self.send_signal(self.widget.Inputs.data, new_data)
 
         for data in [self.iris, new_data]:
-            widget.set_data(data)
+            self.send_signal(self.widget.Inputs.data, data)
 
             simulate.combobox_activate_index(self.widget.controls.variable_color, 0, 0)
             self.assertTrue(widget.interior_coloring == widget.PEARSON)
@@ -147,7 +147,7 @@ def test_count(self):
             vizrank.max_attrs = 4
             self.assertEqual(vizrank.state_count(), 15)  # above + 4x3x2x1 / 2x3x4
 
-            widget.set_data(self.iris_no_class)
+            self.send_signal(self.widget.Inputs.data, self.iris_no_class)
             simulate.combobox_activate_index(self.widget.controls.variable_color, 0, 0)
             vizrank.max_attrs = 2
             self.assertEqual(vizrank.state_count(), 6)  # 4x3 / 2
@@ -160,7 +160,7 @@ def test_iteration(self):
         """MosaicVizrank correctly iterates through states"""
         widget = self.widget
         vizrank = self.vizrank
-        widget.set_data(self.iris)
+        self.send_signal(self.widget.Inputs.data, self.iris)
         vizrank.compute_attr_order()
 
         widget.interior_coloring = widget.CLASS_DISTRIBUTION
@@ -209,7 +209,7 @@ def test_iteration(self):
 
     def test_row_for_state(self):
         """MosaicVizrank returns table row corresponding to the state"""
-        self.widget.set_data(self.iris)
+        self.send_signal(self.widget.Inputs.data, self.iris)
         self.vizrank.attr_ordering = [DiscreteVariable(n) for n in "abcd"]
         items = self.vizrank.row_for_state(0, [1, 3, 0])
         self.assertEqual(len(items), 1)
@@ -224,7 +224,7 @@ def test_does_not_crash(self):
         """MosaicVizrank computes rankings without crashing"""
         widget = self.widget
         vizrank = self.vizrank
-        widget.set_data(self.iris)
+        self.send_signal(self.widget.Inputs.data, self.iris)
         vizrank.max_attrs = 2
 
         widget.interior_coloring = widget.PEARSON
@@ -236,18 +236,18 @@ def test_does_not_crash(self):
         time.sleep(0.5)
         self.assertEqual(vizrank.rank_model.rowCount(), 10)  # 4 + 4x5 / 2
 
-        widget.set_data(self.iris_no_class)
+        self.send_signal(self.widget.Inputs.data, self.iris_no_class)
         vizrank.toggle()
 
     def test_does_not_crash_cont_class(self):
         """MosaicVizrank computes rankings without crashing"""
         data = Table("housing.tab")
-        self.widget.set_data(data)
+        self.send_signal(self.widget.Inputs.data, data)
         self.vizrank.toggle()
 
     def test_pause_continue(self):
         data = Table("housing.tab")
-        self.widget.set_data(data)
+        self.send_signal(self.widget.Inputs.data, data)
         self.vizrank.toggle()  # start
         self.process_events(until=lambda: self.vizrank.saved_progress > 5)
         self.vizrank.toggle()  # stop
@@ -257,7 +257,7 @@ def test_pause_continue(self):
 
     def test_finished(self):
         data = Table("iris.tab")
-        self.widget.set_data(data)
+        self.send_signal(self.widget.Inputs.data, data)
         self.vizrank.toggle()
         self.process_events(until=lambda: not self.vizrank.keep_running)
         self.assertEqual(len(self.vizrank.scores), self.vizrank.state_count())
@@ -336,3 +336,29 @@ def test_scores(self):
             attrlist = tuple(sorted(self.vizrank.attr_ordering[i].name for i in state))
             sc = self.vizrank.compute_score(state)
             self.assertTrue(np.allclose(sc, SCORES[attrlist], rtol=0.003, atol=0))
+
+    def test_subset_data(self):
+        """
+        Mosaic should not crash on subset data and should properly interpret it.
+        GH-2515
+        GH-2528
+        """
+        table_titanic = Table("titanic")
+        self.send_signal(self.widget.Inputs.data, table_titanic)
+        self.send_signal(self.widget.Inputs.data_subset, table_titanic[::20])
+        table_housing = Table("housing")
+        self.send_signal(self.widget.Inputs.data, table_housing)
+        self.send_signal(self.widget.Inputs.data_subset, table_housing[::20])
+
+    def test_incompatible_subset(self):
+        """
+        Show warning when subset data is not compatible with data.
+        GH-2528
+        """
+        table_titanic = Table("titanic")
+        self.assertFalse(self.widget.Warning.incompatible_subset.is_shown())
+        self.send_signal(self.widget.Inputs.data, self.iris)
+        self.send_signal(self.widget.Inputs.data_subset, table_titanic)
+        self.assertTrue(self.widget.Warning.incompatible_subset.is_shown())
+        self.send_signal(self.widget.Inputs.data_subset, self.iris)
+        self.assertFalse(self.widget.Warning.incompatible_subset.is_shown())