Repository: biolab/orange3

Issue:
Heat Map: change how colors are computed
Colors lose precision when you decrease the color range with High and Low. For a better dynamic range implement coloring as in the HyperSpectra widget from the spectroscopy add-on.


Discussion:
Just one comment on colors in general. When you make changes, would it be possible to keep color blind people in mind? I think this is fairly easy today with standard packages, but of course, requires some attention when implementing.

Resolving patch:
diff --git a/Orange/widgets/visualize/owheatmap.py b/Orange/widgets/visualize/owheatmap.py
--- a/Orange/widgets/visualize/owheatmap.py
+++ b/Orange/widgets/visualize/owheatmap.py
@@ -182,14 +182,11 @@ def color_palette_model(palettes, iconsize=QSize(64, 16)):
     return model
 
 
-def color_palette_table(colors, samples=255,
-                        threshold_low=0.0, threshold_high=1.0,
+def color_palette_table(colors,
                         underflow=None, overflow=None,
                         gamma=None):
-    N = len(colors)
     colors = np.array(colors, dtype=np.ubyte)
-    low, high = threshold_low * 255, threshold_high * 255
-    points = np.linspace(low, high, N)
+    points = np.linspace(0, 255, len(colors))
     space = np.linspace(0, 255, 255)
 
     if underflow is None:
@@ -215,6 +212,12 @@ def color_palette_table(colors, samples=255,
     return np.c_[r, g, b]
 
 
+def levels_with_thresholds(low, high, threshold_low, threshold_high):
+    lt = low + (high - low) * threshold_low
+    ht = low + (high - low) * threshold_high
+    return lt, ht
+
+
 def interp_exp(x, xp, fp, gamma=0.0, left=None, right=None,):
     assert np.all(np.diff(xp) > 0)
     x = np.asanyarray(x)
@@ -614,10 +617,7 @@ def color_palette(self):
             return []
         else:
             _, colors = max(data.items())
-            return color_palette_table(
-                colors, threshold_low=self.threshold_low,
-                threshold_high=self.threshold_high,
-                gamma=self.gamma)
+            return color_palette_table(colors, gamma=self.gamma)
 
     def clear(self):
         self.data = None
@@ -1044,6 +1044,7 @@ def setup_scene(self, parts, data):
                     X_part = X_part[:, sort_j[j]]
 
                 hw.set_levels(parts.levels)
+                hw.set_thresholds(self.threshold_low, self.threshold_high)
                 hw.set_color_table(palette)
                 hw.set_show_averages(self.averages)
                 hw.set_heatmap_data(X_part)
@@ -1116,7 +1117,7 @@ def setup_scene(self, parts, data):
             col_annotation_widgets_bottom.append(labelslist)
 
         legend = GradientLegendWidget(
-            parts.levels[0], parts.levels[1],
+            parts.levels[0], parts.levels[1], self.threshold_low, self.threshold_high,
             parent=widget)
 
         legend.set_color_table(palette)
@@ -1346,9 +1347,11 @@ def update_highslider(self):
     def update_color_schema(self):
         palette = self.color_palette()
         for heatmap in self.heatmap_widgets():
+            heatmap.set_thresholds(self.threshold_low, self.threshold_high)
             heatmap.set_color_table(palette)
 
         for legend in self.legend_widgets():
+            legend.set_thresholds(self.threshold_low, self.threshold_high)
             legend.set_color_table(palette)
 
     def update_sorting_examples(self):
@@ -1630,6 +1633,7 @@ def __init__(self, parent=None, data=None, **kwargs):
         self.setAcceptHoverEvents(True)
 
         self.__levels = None
+        self.__threshold_low, self.__threshold_high = 0., 1.
         self.__colortable = None
         self.__data = data
 
@@ -1711,6 +1715,12 @@ def set_color_table(self, table):
         self._update_pixmap()
         self.update()
 
+    def set_thresholds(self, threshold_low, threshold_high):
+        self.__threshold_low = threshold_low
+        self.__threshold_high = threshold_high
+        self._update_pixmap()
+        self.update()
+
     def _update_pixmap(self):
         """
         Update the pixmap if its construction arguments changed.
@@ -1720,15 +1730,19 @@ def _update_pixmap(self):
                 lut = self.__colortable
             else:
                 lut = None
+
+            ll, lh = self.__levels
+            ll, lh = levels_with_thresholds(ll, lh, self.__threshold_low, self.__threshold_high)
+
             argb, _ = pg.makeARGB(
-                self.__data, lut=lut, levels=self.__levels, scale=250)
+                self.__data, lut=lut, levels=(ll, lh))
             argb[np.isnan(self.__data)] = (100, 100, 100, 255)
 
             qimage = pg.makeQImage(argb, transpose=False)
             self.__pixmap = QPixmap.fromImage(qimage)
             avg = np.nanmean(self.__data, axis=1, keepdims=True)
             argb, _ = pg.makeARGB(
-                avg, lut=lut, levels=self.__levels, scale=250)
+                avg, lut=lut, levels=(ll, lh))
             qimage = pg.makeQImage(argb, transpose=False)
             self.__avgpixmap = QPixmap.fromImage(qimage)
         else:
@@ -2071,10 +2085,12 @@ def _pointSize(self, height):
 
 
 class GradientLegendWidget(QGraphicsWidget):
-    def __init__(self, low, high, parent=None):
+    def __init__(self, low, high, threshold_low, threshold_high, parent=None):
         super().__init__(parent)
         self.low = low
         self.high = high
+        self.threshold_low = threshold_low
+        self.threshold_high = threshold_high
         self.color_table = None
 
         layout = QGraphicsLinearLayout(Qt.Vertical)
@@ -2105,11 +2121,18 @@ def set_color_table(self, color_table):
         self.color_table = color_table
         self.__update()
 
+    def set_thresholds(self, threshold_low, threshold_high):
+        self.threshold_low = threshold_low
+        self.threshold_high = threshold_high
+        self.__update()
+
     def __update(self):
-        data = np.linspace(self.low, self.high, num=50, endpoint=True)
+        data = np.linspace(self.low, self.high, num=1000)
         data = data.reshape((1, -1))
+        ll, lh = levels_with_thresholds(self.low, self.high,
+                                        self.threshold_low, self.threshold_high)
         argb, _ = pg.makeARGB(data, lut=self.color_table,
-                              levels=(self.low, self.high))
+                              levels=(ll, lh))
         qimg = pg.makeQImage(argb, transpose=False)
         self.__pixitem.setPixmap(QPixmap.fromImage(qimg))
 


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owheatmap.py b/Orange/widgets/visualize/tests/test_owheatmap.py
--- a/Orange/widgets/visualize/tests/test_owheatmap.py
+++ b/Orange/widgets/visualize/tests/test_owheatmap.py
@@ -151,3 +151,20 @@ def test_empty_clusters(self):
 
         self.assertTrue(self.widget.Warning.empty_clusters.is_shown())
         self.assertEqual(len(self.widget.merge_indices), 3)
+
+    def test_use_enough_colors(self):
+        # Before 201906 thresholds modified the palette and decreased
+        # the number of colors used.
+        data = np.arange(1000).reshape(-1, 1)
+        table = Table.from_numpy(Domain([ContinuousVariable()]), data)
+        self.send_signal(self.widget.Inputs.data, table)
+        self.widget.threshold_high = 0.05
+        self.widget.update_color_schema()
+        heatmap_widget = self.widget.heatmap_widget_grid[0][0]
+        image = heatmap_widget.heatmap_item.pixmap().toImage()
+        colors = np.full((len(data), 3), np.nan)
+        for r in range(len(data)):
+            c = image.pixelColor(0, r)
+            colors[r] = c.red(), c.green(), c.blue()
+        unique_colors = len(np.unique(colors, axis=0))
+        self.assertLessEqual(len(data)*self.widget.threshold_low, unique_colors)