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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions