RGB.to_hsl rounds hue to whole degrees, so RGB -> HSL -> RGB is lossy for ~41% of colours
Software versions
bokeh 3.10.0, Python 3.14.7, macOS. The same code is on main in src/bokeh/colors/color.py.
Browser name and version
N/A, pure Python.
Jupyter notebook / Jupyter Lab version
N/A.
Expected behavior
RGB(...).to_hsl().to_rgb() returns the colour it started from.
Observed behavior
It returns a different colour for about 41% of inputs.
RGB.to_hsl rounds the hue to whole degrees:
h, l, s = colorsys.rgb_to_hls(self.r/255., self.g/255., self.b/255.)
return HSL(round(h*360), s, l, self.a)s and l keep full float precision, and HSL.__init__ annotates h as float, so only the hue is quantised. One degree of hue is worth up to about 2/255 per channel at high saturation.
Sweeping every 5th value per channel, 140,608 colours:
| colours that changed | worst channel error | |
|---|---|---|
| as-is | 57,858 of 140,608 (41.1%) | 2 |
with h*360 instead of round(h*360) |
0 of 140,608 | 0 |
Example code
from bokeh.colors import RGB
c = RGB(0, 5, 200)
print(c.to_hsl().to_rgb()) # RGB(0, 7, 200, 1.0)Stack traceback or browser console output
None, no error is raised.
Dropping the rounding would change to_hsl().h from an int to a float, so it is a behaviour change rather than a pure fix. tests/unit/bokeh/colors/test_color__colors.py pins the rounded value (assert c2.h == 24) and would need updating. Raising it as an issue first per the contributing guidelines rather than sending a PR. Happy to send one if you want it changed.
AI assistance was used to find this and to run the sweep above.
Source: bokeh/bokeh