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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions