Description
hex_to_rgb() assumes the hex string is always 6 digits. It computes the channel width as len(value) // 3 and slices the string into equal thirds:
def hex_to_rgb(value):
value = value.lstrip("#")
hex_total_length = len(value)
rgb_section_length = hex_total_length // 3
return tuple(
int(value[i : i + rgb_section_length], 16)
for i in range(0, hex_total_length, rgb_section_length)
)
For the 3-digit CSS shorthand form this takes each digit as a whole channel value instead of expanding it (CSS says #FFF == #FFFFFF, i.e. each digit is doubled). So:
hex_to_rgb("#FFF") # -> (15, 15, 15) near-black
hex_to_rgb("#FFFFFF") # -> (255, 255, 255) white
Those are the same colour in CSS, but produce wildly different results. Nothing raises - the wrong colour is returned silently.
This matters because plotly itself treats #FFF as a perfectly valid colour:
go.Figure(go.Scatter(x=[1], y=[1], marker=dict(color="#FFF"))) # accepted, renders white
so users can reasonably pass shorthand hex, and the figure factories that route colours through hex_to_rgb then misinterpret them (figure_factory/_annotated_heatmap.py:155, _ternary_contour.py:248, _trisurf.py:120, matplotlylib/mpltools.py:136).
Visible consequence: create_annotated_heatmap picks the annotation font colour from the background luminance via should_use_black_text(). With shorthand hex, a white background is read as (15, 15, 15), so it chooses white text on a white background and the annotations become invisible:
| colorscale |
resulting annotation font colour |
[[0, "#FFF"], [1, "#FFF"]] |
#FFFFFF (invisible on white) |
[[0, "#FFFFFF"], [1, "#FFFFFF"]] |
#000000 (correct) |
A fix would be to expand the 3-digit form before parsing (and probably reject lengths that are neither 3 nor 6, rather than silently splitting into thirds - e.g. a 4 or 5 character string currently produces nonsense too).
Steps to reproduce
import plotly.figure_factory as ff
from plotly.colors import hex_to_rgb
print(hex_to_rgb("#FFF")) # (15, 15, 15) <- expected (255, 255, 255)
print(hex_to_rgb("#FFFFFF")) # (255, 255, 255)
z = [[1, 2], [3, 4]]
fig_short = ff.create_annotated_heatmap(z, colorscale=[[0, "#FFF"], [1, "#FFF"]])
print({a.font.color for a in fig_short.layout.annotations}) # {'#FFFFFF'} -> white on white
fig_long = ff.create_annotated_heatmap(z, colorscale=[[0, "#FFFFFF"], [1, "#FFFFFF"]])
print({a.font.color for a in fig_long.layout.annotations}) # {'#000000'} -> correct
Both colorscales describe the same white background, but only the 6-digit form produces readable annotations.
Happy to open a PR for this if it's welcome.
Versions
- plotly 6.7.0
- Python 3.14.3
- Windows 11
Description
hex_to_rgb()assumes the hex string is always 6 digits. It computes the channel width aslen(value) // 3and slices the string into equal thirds:For the 3-digit CSS shorthand form this takes each digit as a whole channel value instead of expanding it (CSS says
#FFF==#FFFFFF, i.e. each digit is doubled). So:Those are the same colour in CSS, but produce wildly different results. Nothing raises - the wrong colour is returned silently.
This matters because plotly itself treats
#FFFas a perfectly valid colour:so users can reasonably pass shorthand hex, and the figure factories that route colours through
hex_to_rgbthen misinterpret them (figure_factory/_annotated_heatmap.py:155,_ternary_contour.py:248,_trisurf.py:120,matplotlylib/mpltools.py:136).Visible consequence:
create_annotated_heatmappicks the annotation font colour from the background luminance viashould_use_black_text(). With shorthand hex, a white background is read as(15, 15, 15), so it chooses white text on a white background and the annotations become invisible:[[0, "#FFF"], [1, "#FFF"]]#FFFFFF(invisible on white)[[0, "#FFFFFF"], [1, "#FFFFFF"]]#000000(correct)A fix would be to expand the 3-digit form before parsing (and probably reject lengths that are neither 3 nor 6, rather than silently splitting into thirds - e.g. a 4 or 5 character string currently produces nonsense too).
Steps to reproduce
Both colorscales describe the same white background, but only the 6-digit form produces readable annotations.
Happy to open a PR for this if it's welcome.
Versions