Repository: biolab/orange3

Issue:
"Color Density" should ignore category "Other"
I suppose this is unwanted. Data instances belonging to "Other" should probably be ignored.

To replicate, use any data with large number of categories, in which those belonging to the less frequent ones are in the majority. The below example is the network for last_fm.

![x](https://user-images.githubusercontent.com/2387315/105985604-f580fa00-609b-11eb-81cc-4466c7ff2c6e.png)



Discussion:
Ah, you a right! Can confirm, it is undesired.

Resolving patch:
diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py
--- a/Orange/widgets/visualize/owscatterplotgraph.py
+++ b/Orange/widgets/visualize/owscatterplotgraph.py
@@ -43,6 +43,9 @@
 SELECTION_WIDTH = 5
 MAX_N_VALID_SIZE_ANIMATE = 1000
 
+# maximum number of colors (including Other)
+MAX_COLORS = 11
+
 
 class LegendItem(PgLegendItem):
     def __init__(self, size=None, offset=None, pen=None, brush=None):
@@ -1189,7 +1192,9 @@ def update_density(self):
             c_data = self.master.get_color_data()
             if c_data is None:
                 return
-            mask = np.isfinite(self._filter_visible(c_data))
+            visible_c_data = self._filter_visible(c_data)
+            mask = np.bitwise_and(np.isfinite(visible_c_data),
+                                  visible_c_data < MAX_COLORS - 1)
             pens = self.scatterplot_item.data['pen']
             rgb_data = [
                 pen.color().getRgb()[:3] if pen is not None else (255, 255, 255)
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
@@ -25,12 +25,12 @@
 from Orange.widgets.utils.plot import OWPlotGUI
 from Orange.widgets.utils.sql import check_sql_input
 from Orange.widgets.utils.state_summary import format_summary_details
-from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase
+from Orange.widgets.visualize.owscatterplotgraph import (
+    OWScatterPlotBase, MAX_COLORS
+)
 from Orange.widgets.visualize.utils.component import OWGraphWithAnchors
 from Orange.widgets.widget import OWWidget, Input, Output, Msg
 
-# maximum number of colors (including Other)
-MAX_COLORS = 11
 
 # maximum number of shapes (including Other)
 MAX_SHAPES = len(OWScatterPlotBase.CurveSymbols) - 1


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owscatterplotbase.py b/Orange/widgets/visualize/tests/test_owscatterplotbase.py
--- a/Orange/widgets/visualize/tests/test_owscatterplotbase.py
+++ b/Orange/widgets/visualize/tests/test_owscatterplotbase.py
@@ -940,6 +940,46 @@ def test_density_with_missing(self, class_density_image):
         np.testing.assert_equal(y_data, y_data0)
         np.testing.assert_equal(colors, colors0)
 
+    @patch("Orange.widgets.visualize.owscatterplotgraph.MAX_COLORS", 3)
+    @patch("Orange.widgets.utils.classdensity.class_density_image")
+    def test_density_with_max_colors(self, class_density_image):
+        graph = self.graph
+        graph.reset_graph()
+        graph.plot_widget.addItem = Mock()
+        graph.plot_widget.removeItem = Mock()
+
+        graph.class_density = True
+        d = np.arange(10, dtype=float) % 3
+        self.master.get_color_data = lambda: d
+
+        # All colors known
+        graph.update_colors()
+        x_data, y_data, colors = class_density_image.call_args[0][5:]
+        np.testing.assert_equal(x_data, np.arange(10)[d < 2])
+        np.testing.assert_equal(y_data, np.arange(10)[d < 2])
+        self.assertEqual(len(set(colors)), 2)
+
+        # Missing colors
+        d[:3] = np.nan
+        graph.update_colors()
+        x_data, y_data, colors = class_density_image.call_args[0][5:]
+        np.testing.assert_equal(x_data, np.arange(3, 10)[d[3:] < 2])
+        np.testing.assert_equal(y_data, np.arange(3, 10)[d[3:] < 2])
+        self.assertEqual(len(set(colors)), 2)
+
+        # Missing colors + only subsample plotted
+        graph.set_sample_size(8)
+        graph.reset_graph()
+        x_data, y_data, colors = class_density_image.call_args[0][5:]
+        visible_data = graph._filter_visible(d)
+        d_known = np.bitwise_and(np.isfinite(visible_data),
+                                  visible_data < 2)
+        x_data0 = graph._filter_visible(np.arange(10))[d_known]
+        y_data0 = graph._filter_visible(np.arange(10))[d_known]
+        np.testing.assert_equal(x_data, x_data0)
+        np.testing.assert_equal(y_data, y_data0)
+        self.assertLessEqual(len(set(colors)), 2)
+
     def test_labels(self):
         graph = self.graph
         graph.reset_graph()