Repository: biolab/orange3

Issue:
BoxPlot: Hide groups with no instances
When BoxPlot displays filtered data, grouped by a variable that has many different values (state, country, ...), but many of them are not present in the dataset, an empty row is created for each of them.

This can be worked around by including a purge domain widget before the boxplot, but I would prefer if boxplot could do this (semi) automatically. The same problem was present in scatterplot where the legend displayed all possible values but has since been fixed to include only the values present in data.

In the following workflow (using `cyber-security-breaches` documentation dataset), the first boxplot is used to select only instances with state AR, which are then displayed in the second boxplot. The suggested change would result in only the AR row being drawn in the second boxplot

<img width="1398" alt="screen shot 2018-06-26 at 23 34 46" src="https://user-images.githubusercontent.com/588601/41940616-9cba7798-7999-11e8-9cd0-108ebc45f1c8.png">


Discussion:
+1 for this issue!

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
@@ -407,8 +407,11 @@ def compute_box_data(self):
                 dataset, attr, self.group_var)
             if self.is_continuous:
                 self.stats = [BoxData(cont, attr, i, self.group_var)
-                              for i, cont in enumerate(self.conts)]
-            self.label_txts_all = self.group_var.values
+                              for i, cont in enumerate(self.conts)
+                              if np.sum(cont) > 0]
+            self.label_txts_all = \
+                [v for v, c in zip(self.group_var.values, self.conts)
+                 if np.sum(c) > 0]
         else:
             self.dist = distribution.get_distribution(dataset, attr)
             self.conts = []
@@ -528,14 +531,17 @@ def display_changed_disc(self):
             if self.group_var:
                 self.labels = [
                     QGraphicsTextItem("{}".format(int(sum(cont))))
-                    for cont in self.conts]
+                    for cont in self.conts if np.sum(cont) > 0]
             else:
                 self.labels = [
                     QGraphicsTextItem(str(int(sum(self.dist))))]
 
         self.draw_axis_disc()
         if self.group_var:
-            self.boxes = [self.strudel(cont, i) for i, cont in enumerate(self.conts)]
+            self.boxes = \
+                [self.strudel(cont, i) for i, cont in enumerate(self.conts)
+                 if np.sum(cont) > 0]
+            self.conts = self.conts[np.sum(np.array(self.conts), axis=1) > 0]
         else:
             self.boxes = [self.strudel(self.dist)]
 
@@ -767,6 +773,8 @@ def draw_axis_disc(self):
         Draw the horizontal axis and sets self.scale_x for discrete attributes
         """
         if self.stretched:
+            if not self.attr_labels:
+                return
             step = steps = 10
         else:
             if self.group_var:


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owboxplot.py b/Orange/widgets/visualize/tests/test_owboxplot.py
--- a/Orange/widgets/visualize/tests/test_owboxplot.py
+++ b/Orange/widgets/visualize/tests/test_owboxplot.py
@@ -162,6 +162,20 @@ def test_label_overlap(self):
         QTest.qWait(3000)
         self.widget.hide()
 
+    def test_empty_groups(self):
+        """Test if groups with zero elements are not shown"""
+        table = Table("cyber-security-breaches")
+        self.send_signal(self.widget.Inputs.data, table)
+        self.__select_variable("US State")
+        self.__select_group("US State")
+        self.assertEqual(52, len(self.widget.boxes))
+
+        # select rows with US State equal to TX or MO
+        use_indexes = np.array([0, 1, 25, 26, 27])
+        table.X = table.X[use_indexes]
+        self.send_signal(self.widget.Inputs.data, table)
+        self.assertEqual(2, len(self.widget.boxes))
+
     def _select_data(self):
         items = [item for item in self.widget.box_scene.items()
                  if isinstance(item, FilterGraphicsRectItem)]