Skip to content

Accept CSS color formats supported by plotly.js in the color validator - #5868

Open
ShawnNotFound wants to merge 5 commits into
plotly:mainfrom
ShawnNotFound:fix-css-color-validation
Open

ShawnNotFound wants to merge 5 commits into
plotly:mainfrom
ShawnNotFound:fix-css-color-validation

Conversation

@ShawnNotFound

@ShawnNotFound ShawnNotFound commented Nov 3, 2026 •

Copy link
Copy Markdown

Link to issue

Closes #5867

Description of change

The color validator now accepts the CSS color syntax that plotly.js parses: signed and exponent numbers, angle units other than deg, the none keyword, any color() color space, and transparent. Before, values like lab(50% -40 59.5) or color(srgb 1 0 0) raised a ValueError even though plotly.js renders them.

Demo

import plotly.graph_objects as go

go.Scatter(marker_color="lab(50% -40 59.5)")
# before: ValueError: Invalid value of type 'builtins.str' received for the 'color' property of scatter.marker
# after: accepted

Testing strategy

Added valid and invalid cases to test_color_validator.py. I checked each of them against culori, the parser plotly.js uses, so the validator accepts and rejects the same strings.

Additional information (optional)

The old code removed spaces before matching so the regex didn't have to handle them. The new regex handles whitespace itself, so that step is gone. Strings that don't form a valid color function, like rgb(255, 0) or rgb(1-2 3 4), are still rejected.

Guidelines

Allow signed and exponent numbers, angle units other than deg, the none
keyword, any color() color space and the transparent keyword. These are
all accepted by plotly.js 4 but were rejected by the Python validator.

@chrikrah chrikrah left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShawnNotFound blocking: the number pattern in _re_css_value can split a digit run anywhere, so an unclosed rgb( backtracks quadratically. One token fixes it:

-        r"(?:[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?(?:%|deg|grad|rad|turn)?|none)"
+        r"(?:[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?(?:%|deg|grad|rad|turn)?|none)"
# plotly 7.1.0 from PyPI, _plotly_utils/basevalidators.py overlaid from each ref, Python 3.12
# ColorValidator.perform_validate_coerce, seconds
input                       base 84498cc   head 527cd14   head + fix
"rgb(" + "1"*5000           < 0.05         3.03           < 0.05
"rgb(" + "1"*20000          < 0.05         41.96          < 0.05
"rgb(1 1 " + "1"*20000+"x"  < 0.05         45.95          < 0.05

$ python -m pytest test_color_validator.py -q -p no:cacheprovider
head 145 passed | basevalidators.py at base: 24 failed, 121 passed | head + fix: 145 passed
# 731 spacing and case variants of rgb/hsl/hwb/lab/lch/oklab/oklch and color(): head and head + fix accept the same set

Can you swap that line in? Nothing else blocks.

\d+\.?\d* could split a run of digits in many ways, so an unclosed
color function with many digits took quadratic time to reject.
@ShawnNotFound

Copy link
Copy Markdown
Author

Thanks, I have swapped that line.

@chrikrah chrikrah left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShawnNotFound unclosed rgb( fails in milliseconds. Nothing here blocks a merge from my side.

# fd6f3ac exported with git archive, plotly 7.1.0 from PyPI with basevalidators.py overlaid; Python 3.12
$ python t.py     # perform_validate_coerce on "rgb(" + 20000 ones, seconds
0.002
$ python -m pytest tests/test_plotly_utils/validators/test_color_validator.py -q -p no:cacheprovider
145 passed in 0.28s
# basevalidators.py from main 3c1aeba, tests kept
24 failed, 121 passed in 0.90s

# _plotly_utils from git archive at each ref; Python 3.12.14
$ python -I probe.py <ref>     # ColorValidator("color", "scatter.marker").perform_validate_coerce
== 3c1aeba
'light blue' -> 'light blue'
'# ff0000' -> '# ff0000'
'hsl(0, 100 %, 50%)' -> 'hsl(0, 100 %, 50%)'
'rgb (255,0,0)' -> 'rgb (255,0,0)'
== fd6f3ac
'light blue' -> None
'# ff0000' -> None
'hsl(0, 100 %, 50%)' -> None
'rgb (255,0,0)' -> 'rgb (255,0,0)'

non-blocking: 'light blue' and '# ff0000' now raise where main returned them. Invalid CSS, so a fix. CHANGELOG line?

@ShawnNotFound

Copy link
Copy Markdown
Author

Thanks for checking. Added a CHANGELOG line for it. plotly.js's color parser rejects those strings too, so the validator now matches what plotly.js renders.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Color validator rejects CSS color formats that plotly.js accepts

3 participants