Skip to content

feat: Switch color processing library#7536

Open
camdecoster wants to merge 43 commits into
v4.0from
cam/7523/switch-color-library
Open

feat: Switch color processing library#7536
camdecoster wants to merge 43 commits into
v4.0from
cam/7523/switch-color-library

Conversation

@camdecoster

@camdecoster camdecoster commented Aug 26, 2025

Copy link
Copy Markdown
Contributor

Description

Switch color processing library from TinyColor to color.

Closes #7523.

Changes

  • Change color processing library
    • hsv color strings are no longer permitted
    • rgb()/rgba() strings with decimal 0–1 fractions are now normalized to integers via Color.clean() on input
    • New color specifiers supported:
      • 8-digit hex with alpha (#rrggbbaa) and 4-digit hex short (#rgba)
      • Space-separated and slash-syntax rgb/rgba/hsl (rgba(255 0 0 / 0.5))
      • hsla() and hwb() color functions
  • There are subtle output differences in darken/lighten/mix (HSL multiplicative vs. tinycolor's additive)
  • Update/refactor method calls for dealing with colors
  • Update tests per library change

Testing

  • Check CI for broken tests
  • Run npm run test-jasmine and verify that all tests pass
  • Run npm run test-image and verify that all image-baseline diffs match the regenerated baselines in this PR
  • Verify that npm run schema produces no diff against test/plot-schema.json
  • Run devtools and open the new mock color_syntax_formats and verify that it runs correctly without any errors

Screenshots

Color.contrast, Color.brighten, and Color.addOpacity produce slightly different RGB output. Here are some example plots that can be used to see the difference between tinycolor and color when pasted into Plotly devtools:

Auto-contrast text on a heatmap

Plotly.newPlot(
    gd,
    [
        {
            type: 'heatmap',
            z: [
                [1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]
            ],
            colorscale: 'Jet',
            texttemplate: '%{z}'
        }
    ],
    { width: 400, height: 300 }
);
Before After
image image

Auto inside-text color on default colorway

Plotly.newPlot(gd, {
    data: [
        {
            type: 'bar',
            x: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
            y: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            marker: {
                color: [
                    '#1f77b4',
                    '#ff7f0e',
                    '#2ca02c',
                    '#d62728',
                    '#9467bd',
                    '#8c564b',
                    '#e377c2',
                    '#7f7f7f',
                    '#bcbd22',
                    '#17becf'
                ]
            },
            text: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
            textposition: 'inside',
            insidetextanchor: 'middle',
            textfont: { size: 32 },
            hoverinfo: 'skip'
        }
    ],
    layout: {
        title: { text: 'Auto inside-text color on default colorway' },
        showlegend: false,
        bargap: 0,
        xaxis: { visible: false },
        yaxis: { visible: false },
        width: 900,
        height: 220,
        margin: { l: 20, r: 20, t: 60, b: 20 }
    }
});
Before After
image image

Pie with extended colorway (lighten/darken)

Plotly.newPlot(
    gd,
    [
        {
            type: 'pie',
            values: [7, 6, 5, 4, 3, 2, 1]
        }
    ],
    {
        colorway: ['#777', '#F00'],
        width: 400,
        height: 400
    }
);
Before After
image image

Notes

  • The new library allows for use of modern CSS color syntax
  • Derived colors are now always rgb() / rgba() strings
  • The new library increases the plotly.js bundle size by 32kB (14kB for minified)

@camdecoster camdecoster self-assigned this Aug 26, 2025
@gvwilson gvwilson added P1 needed for current cycle fix fixes something broken labels Aug 28, 2025
@camdecoster

Copy link
Copy Markdown
Contributor Author

I created a couple of PRs (#78 and #81) in a dependency used by color that will add some new features for parsing color strings that would bring it closer to parity with TinyColor. The maintainer recently merged one of these and will (hopefully) be merging the other soon. Once he publishes a new release, I'll be able to use it in plotly.js.

@camdecoster camdecoster force-pushed the cam/7523/switch-color-library branch from fc905c7 to 6e83d8a Compare April 27, 2026 19:56
@camdecoster camdecoster changed the base branch from master to v4.0 June 9, 2026 21:43
@camdecoster camdecoster marked this pull request as ready for review June 9, 2026 23:03
@emilykl

emilykl commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

@camdecoster Looks good overall, but lightened/darkened colors seem much less distinct after this change, is there any way to adjust that? Maybe increase the degree of lightening/darkening?

Before After
Screenshot 2026-06-12 at 2 07 23 PM Screenshot 2026-06-12 at 2 07 43 PM

(codepen)

var c = tc.toRgb();
return 'rgb(' + Math.round(c.r) + ', ' +
Math.round(c.g) + ', ' + Math.round(c.b) + ')';
const _color = require('color').default;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
const _color = require('color').default;
'use strict';
const _color = require('color').default;

};

color.rgb = function(cstr) { return color.tinyRGB(tinycolor(cstr)); };
const rgb = (cstr) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add a comment explaining that this is for returning a string which doesn't contain alpha? (Or whatever is the purpose of this function, I'm assuming it's that because of the comment in the old file.)

// and convert them to rgb(0-255 values)
color.clean = function(container) {
if(!container || typeof container !== 'object') return;
const clean = (container) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure we can just remove this function and its usages entirely, since it looks we deprecated the rgb(fractions) format 11(!) years ago.


function cleanOne(val) {
if(isNumeric(val) || typeof val !== 'string') return val;
const cleanOne = (val) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Likewise for this function, can remove

Comment thread src/lib/coerce.js
Comment on lines +163 to +168
"- hex (e.g. '#d3d3d3', '#d3d3d3aa')",
"- rgb (e.g. 'rgb(255, 0, 0)', 'rgb(255 0 0)')",
"- rgba (e.g. 'rgba(255, 0, 0, 0.5)', 'rgba(255 0 0 / 0.5)')",
"- hsl (e.g. 'hsl(0, 100%, 50%)')",
"- hsla (e.g. 'hsla(0, 100%, 50%, 0.5)')",
"- hwb (e.g. 'hwb(0, 0%, 0%)')",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Weirdly, I can't find this info in the docs anywhere, although I could be missing it. We should make sure the supported color formats are documented with the next release.

});

var gridColor = coerce2('gridcolor', addOpacity(dfltColor, 0.3));
var gridColor = coerce('gridcolor', addOpacity(dfltColor, 0.3));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@camdecoster Why this change, is it intentional?

Comment thread src/traces/pie/calc.js

color = tinycolor(color);
if(!color.isValid()) return false;
const newColor = Color.addOpacity(color, Color.opacity(color));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Couldn't this just be Color.color(color).rgb().string()?

Comment thread src/traces/pie/calc.js

for(i = 0; i < colorList.length; i++) {
colors.push(tinycolor(colorList[i]).lighten(20).toHexString());
colors.push(Color.color(colorList[i]).lighten(0.2).hex());

@emilykl emilykl Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re: my comment about lightened/darkened colors being too similar, I assume you could just increase this value here to fix.

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

Labels

fix fixes something broken P1 needed for current cycle

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update color processing library to handle modern CSS color syntax

3 participants