Skip to content

[BUG]: hex_to_rgb misparses 3-digit shorthand hex colors (#FFF -> (15,15,15)) #5661

Description

@eeshsaxena

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:

defhex_to_rgb(value):
value=value.lstrip("#")
hex_total_length=len(value)
rgb_section_length=hex_total_length//3returntuple(
int(value[i : i+rgb_section_length], 16)
foriinrange(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-blackhex_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:

colorscaleresulting 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

importplotly.figure_factoryasfffromplotly.colorsimporthex_to_rgbprint(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.colorforainfig_short.layout.annotations}) # {'#FFFFFF'} -> white on whitefig_long=ff.create_annotated_heatmap(z, colorscale=[[0, "#FFFFFF"], [1, "#FFFFFF"]])
print({a.font.colorforainfig_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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions