Repository: biolab/orange3

Issue:
Colors for more than 256 values crash
**Describe the bug**

Several widgets crash if given Discrete Variable with more than 256 values.

**To Reproduce**

Make a Discrete Variable with more than 256 values. Pass it to Color or Line Chart (Line Chart will crash only if this variable is selected for Split by).

**Orange version:**
3.26.0.dev

**Expected behavior**

Values that are out of range are colored gray. A Warning appears.



Discussion:
> Values that are out of range are colored gray. A Warning appears.

Gray, or do we cycle through the palette again, repeating the colors? If we do this, we don't need a warning. Nobody will notice recycling since many of the 256 Glasbey colors are indistinhuishable anyway. 

Warning would be difficult to implement - we would have to do it for every widget separately. The change I describe below is a simple change in a single place that would solve problems for all widgets at once. 

Note to self of whomever may be concerned: `LimitedDiscretePalette` returns `Glasbey` if `number_of_colors > len(DefaultRGBColors)`. It doesn't check whether `number_of_colors > len(Glasbey)`. Shame on me.

`LimitedDiscretePalette` should return another palette, e.g. `ExtendedGlasbey` that won't redefine only `values_to_qcolors` and similar, but also properties `qcolors` and `qcolors_w_nan` so that they would return an array of appropriate length. Alternatively, `Glasbey` can behave like this by default (e.g. `Glasbey` wouldn't be an instance of `DiscretePalette` but of `GlasbeyPalette`).
> Nobody will notice recycling since many of the 256 Glasbey colors are indistinhuishable anyway.

😆 True. This is not meant to be saying I need more than 256 colors. Nobody does. Just so the widget doesn't break. I'm fine with this solution, if it is the most straightforward. 👍 

Resolving patch:
diff --git a/Orange/widgets/utils/colorpalettes.py b/Orange/widgets/utils/colorpalettes.py
--- a/Orange/widgets/utils/colorpalettes.py
+++ b/Orange/widgets/utils/colorpalettes.py
@@ -176,11 +176,15 @@ def __init__(self, number_of_colors, nan_color=NAN_COLOR,
                 "use 'force_glasbey' instead", DeprecationWarning)
         if force_glasbey or force_hsv \
                 or number_of_colors > len(DefaultRGBColors):
-            palette = Glasbey
+            colors = Glasbey.palette[:number_of_colors]
+            if number_of_colors > len(colors):
+                colors = np.vstack(
+                    (colors, ) * (number_of_colors // len(colors))
+                    + (colors[:number_of_colors % len(colors)], ))
         else:
-            palette = DefaultRGBColors
+            colors = DefaultRGBColors.palette[:number_of_colors]
         super().__init__("custom", "custom",
-                         palette.palette[:number_of_colors], nan_color,
+                         colors, nan_color,
                          category=category, flags=flags)
 
 


Test patch:
diff --git a/Orange/widgets/utils/tests/test_colorpalettes.py b/Orange/widgets/utils/tests/test_colorpalettes.py
--- a/Orange/widgets/utils/tests/test_colorpalettes.py
+++ b/Orange/widgets/utils/tests/test_colorpalettes.py
@@ -159,6 +159,27 @@ def test_small_palettes():
         palette = LimitedDiscretePalette(100)
         np.testing.assert_equal(palette.palette, Glasbey.palette[:100])
 
+    def test_large_palettes(self):
+        palette = LimitedDiscretePalette(257)
+        qcolors = palette.qcolors
+        qcolors_w_nan = palette.qcolors_w_nan
+        c256 = qcolors[256].getRgb()
+
+        self.assertEqual(len(palette), 257)
+        self.assertEqual(len(palette.palette), 257)
+        self.assertEqual(len(qcolors), 257)
+        self.assertEqual(len(qcolors_w_nan), 258)
+        self.assertEqual([c.getRgb() for c in qcolors],
+                         [c.getRgb() for c in qcolors_w_nan[:-1]])
+        self.assertEqual(palette[256].getRgb(), c256)
+        np.testing.assert_equal(palette.value_to_color(256), c256[:3])
+        self.assertEqual(palette.value_to_qcolor(256).getRgb(), c256)
+        np.testing.assert_equal(palette.values_to_colors([256])[0], c256[:3])
+        self.assertEqual(palette.values_to_qcolors([256])[0].getRgb(), c256)
+
+        for size in range(1020, 1030):
+            self.assertEqual(len(LimitedDiscretePalette(size)), size)
+
     @staticmethod
     def test_forced_glasbey_palettes():
         palette = LimitedDiscretePalette(5, force_glasbey=True)
@@ -598,6 +619,10 @@ def test_colors(self):
         np.testing.assert_almost_equal(var.colors,
                                        [palette[1], [13, 14, 15], palette[0]])
 
+        # Variable with many values
+        var = DiscreteVariable("x", values=tuple(f"v{i}" for i in range(1020)))
+        self.assertEqual(len(var.colors), 1020)
+
     def test_colors_fallback_to_palette(self):
         var = DiscreteVariable.make("a", values=("F", "M"))
         var.palette = Dark2Colors