Repository: biolab/orange3

Issue:
Box Plot widget: if a feature have no data, widget produce error
##### Orange version

3.3.7
##### Expected bahavior

If a selected feature has no data, box plot widget should return warning. 
##### Actual behavior

If a feature with no data is selected in the Variable box, Orange returns error.
##### Additional info (error data)

ValueError                                    Traceback (most recent call last):
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/Orange/widgets/gui.py", line 2440, in **call**
    self.func(**kwds)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/Orange/widgets/visualize/owboxplot.py", line 247, in attr_changed
    self.layout_changed()
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/Orange/widgets/visualize/owboxplot.py", line 318, in layout_changed
    self.draw_axis()
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/Orange/widgets/visualize/owboxplot.py", line 512, in draw_axis
    bottom = min(stat.a_min for stat in self.stats)
ValueError: min() arg is an empty sequence



Discussion:

Resolving patch:
diff --git a/Orange/widgets/visualize/owboxplot.py b/Orange/widgets/visualize/owboxplot.py
--- a/Orange/widgets/visualize/owboxplot.py
+++ b/Orange/widgets/visualize/owboxplot.py
@@ -497,8 +497,11 @@ def mean_label(self, stat, attr, val_name):
 
     def draw_axis(self):
         """Draw the horizontal axis and sets self.scale_x"""
-        bottom = min(stat.a_min for stat in self.stats)
-        top = max(stat.a_max for stat in self.stats)
+        misssing_stats = not self.stats
+        stats = self.stats or [BoxData(np.array([[0.], [1.]]))]
+        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, "")]
+        bottom = min(stat.a_min for stat in stats)
+        top = max(stat.a_max for stat in stats)
 
         first_val, step = compute_scale(bottom, top)
         while bottom <= first_val:
@@ -507,8 +510,8 @@ def draw_axis(self):
         no_ticks = math.ceil((top - first_val) / step) + 1
         top = max(top, first_val + no_ticks * step)
 
-        gbottom = min(bottom, min(stat.mean - stat.dev for stat in self.stats))
-        gtop = max(top, max(stat.mean + stat.dev for stat in self.stats))
+        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))
+        gtop = max(top, max(stat.mean + stat.dev for stat in stats))
 
         bv = self.box_view
         viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)
@@ -517,7 +520,7 @@ def draw_axis(self):
         # In principle we should repeat this until convergence since the new
         # scaling is too conservative. (No chance am I doing this.)
         mlb = min(stat.mean + mean_lab.min_x / scale_x
-                  for stat, mean_lab in zip(self.stats, self.mean_labels))
+                  for stat, mean_lab in zip(stats, mean_labels))
         if mlb < gbottom:
             gbottom = mlb
             self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)
@@ -530,8 +533,10 @@ def draw_axis(self):
             l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,
                                        self._pen_axis_tick)
             l.setZValue(100)
+
             t = self.box_scene.addSimpleText(
-                self.attribute.repr_val(val), self._axis_font)
+                self.attribute.repr_val(val) if not misssing_stats else "?",
+                self._axis_font)
             t.setFlags(
                 t.flags() | QtGui.QGraphicsItem.ItemIgnoresTransformations)
             r = t.boundingRect()


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owboxplot.py b/Orange/widgets/visualize/tests/test_owboxplot.py
new file mode 100644
--- /dev/null
+++ b/Orange/widgets/visualize/tests/test_owboxplot.py
@@ -0,0 +1,55 @@
+# Test methods with long descriptive names can omit docstrings
+# pylint: disable=missing-docstring
+from unittest import skip
+
+import numpy as np
+from Orange.data import Table, ContinuousVariable
+from Orange.widgets.visualize.owboxplot import OWBoxPlot
+from Orange.widgets.tests.base import WidgetTest
+
+
+class TestOWBoxPlot(WidgetTest):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.iris = Table("iris")
+        cls.zoo = Table("zoo")
+        cls.housing = Table("housing")
+
+    def setUp(self):
+        self.widget = self.create_widget(OWBoxPlot)
+
+    @skip("Known bug, FIXME!")
+    def test_input_data(self):
+        """Check widget's data"""
+        self.send_signal("Data", self.iris)
+        self.assertGreater(len(self.widget.attrs), 0)
+        self.send_signal("Data", None)
+        self.assertEqual(len(self.widget.attrs), 0)
+
+    def test_input_data_missings_cont_group_var(self):
+        """Check widget with continuous data with missing values and group variable"""
+        data = self.iris
+        data.X[:, 0] = np.nan
+        self.send_signal("Data", data)
+        # used to crash, see #1568
+
+    def test_input_data_missings_cont_no_group_var(self):
+        """Check widget with continuous data with missing values and no group variable"""
+        data = self.housing
+        data.X[:, 0] = np.nan
+        self.send_signal("Data", data)
+        # used to crash, see #1568
+
+    def test_input_data_missings_disc_group_var(self):
+        """Check widget with discrete data with missing values and group variable"""
+        data = self.zoo
+        data.X[:, 0] = np.nan
+        self.send_signal("Data", data)
+
+    def test_input_data_missings_disc_no_group_var(self):
+        """Check widget discrete data with missing values and no group variable"""
+        data = self.zoo
+        data.domain.class_var = ContinuousVariable("cls")
+        data.X[:, 0] = np.nan
+        self.send_signal("Data", data)