Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 61 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@turf/centroid": "^7.1.0",
"base64-arraybuffer": "^1.0.2",
"canvas-fit": "^1.5.0",
"color": "^5.0.0",
"color-alpha": "1.0.4",
"color-normalize": "1.5.0",
"color-parse": "2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/color/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ exports.borderLine = '#BEC8D9';

// with axis.color and Color.interp we aren't using lightLine
// itself anymore, instead interpolating between axis.color
// and the background color using tinycolor.mix. lightFraction
// and the background color using Color.mix. lightFraction
// gives back exactly lightLine if the other colors are defaults.
exports.lightFraction = 100 * (0xe - 0x4) / (0xf - 0x4);
172 changes: 103 additions & 69 deletions src/components/color/index.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,48 @@
'use strict';

var tinycolor = require('tinycolor2');
var isNumeric = require('fast-isnumeric');
var isTypedArray = require('../../lib/array').isTypedArray;
const isNumeric = require('fast-isnumeric');
const isTypedArray = require('../../lib/array').isTypedArray;
const color = require('color').default

var color = module.exports = {};
const { background, defaultLine, defaults, lightLine } = require('./attributes');

var colorAttrs = require('./attributes');
color.defaults = colorAttrs.defaults;
var defaultLine = color.defaultLine = colorAttrs.defaultLine;
color.lightLine = colorAttrs.lightLine;
var background = color.background = colorAttrs.background;

/*
* tinyRGB: turn a tinycolor into an rgb string, but
* unlike the built-in tinycolor.toRgbString this never includes alpha
*/
color.tinyRGB = function(tc) {
var c = tc.toRgb();
return 'rgb(' + Math.round(c.r) + ', ' +
Math.round(c.g) + ', ' + Math.round(c.b) + ')';
};

color.rgb = function(cstr) { return color.tinyRGB(tinycolor(cstr)); };
const rgb = cstr => {
const { r, g, b } = color(cstr).rgb().object();
return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
}

color.opacity = function(cstr) { return cstr ? tinycolor(cstr).getAlpha() : 0; };
const opacity = cstr => cstr ? color(cstr).alpha() : 0;

color.addOpacity = function(cstr, op) {
var c = tinycolor(cstr).toRgb();
return 'rgba(' + Math.round(c.r) + ', ' +
Math.round(c.g) + ', ' + Math.round(c.b) + ', ' + op + ')';
const addOpacity = (cstr, op) => {
const c = color(cstr).rgb().object();
return `rgba(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)}, ${op})`;
};

// combine two colors into one apparent color
// if back has transparency or is missing,
// color.background is assumed behind it
color.combine = function(front, back) {
var fc = tinycolor(front).toRgb();
if(fc.a === 1) return tinycolor(front).toRgbString();

var bc = tinycolor(back || background).toRgb();
var bcflat = bc.a === 1 ? bc : {
r: 255 * (1 - bc.a) + bc.r * bc.a,
g: 255 * (1 - bc.a) + bc.g * bc.a,
b: 255 * (1 - bc.a) + bc.b * bc.a
// background is assumed behind it
const combine = (front, back = background) => {
const fc = color(front).rgb().object();
fc.alpha ||= 1;
if(fc.alpha === 1) return color(front).rgb().string();

const bc = color(back).rgb().object();
bc.alpha ||= 1;
const bcflat = bc.alpha === 1
? bc
: {
r: 255 * (1 - bc.alpha) + bc.r * bc.alpha,
g: 255 * (1 - bc.alpha) + bc.g * bc.alpha,
b: 255 * (1 - bc.alpha) + bc.b * bc.alpha
};

const fcflat = {
r: bcflat.r * (1 - fc.alpha) + fc.r * fc.alpha,
g: bcflat.g * (1 - fc.alpha) + fc.g * fc.alpha,
b: bcflat.b * (1 - fc.alpha) + fc.b * fc.alpha
};
var fcflat = {
r: bcflat.r * (1 - fc.a) + fc.r * fc.a,
g: bcflat.g * (1 - fc.a) + fc.g * fc.a,
b: bcflat.b * (1 - fc.a) + fc.b * fc.a
};
return tinycolor(fcflat).toRgbString();

return color(fcflat).string();
};

/*
Expand All @@ -59,17 +51,17 @@ color.combine = function(front, back) {
* Ignores alpha channel values.
* The resulting color is computed as: factor * first + (1 - factor) * second.
*/
color.interpolate = function(first, second, factor) {
var fc = tinycolor(first).toRgb();
var sc = tinycolor(second).toRgb();
const interpolate = (first, second, factor) => {
const fc = color(first).rgb().object();
const sc = color(second).rgb().object();

var ic = {
const ic = {
r: factor * fc.r + (1 - factor) * sc.r,
g: factor * fc.g + (1 - factor) * sc.g,
b: factor * fc.b + (1 - factor) * sc.b,
};

return tinycolor(ic).toRgbString();
return color(ic).rgb().string();
};

/*
Expand All @@ -80,34 +72,28 @@ color.interpolate = function(first, second, factor) {
* If lightAmount / darkAmount are used, we adjust by these percentages,
* otherwise we go all the way to white or black.
*/
color.contrast = function(cstr, lightAmount, darkAmount) {
var tc = tinycolor(cstr);
const contrast = (cstr, lightAmount, darkAmount) => {
let c = color(cstr)

if(tc.getAlpha() !== 1) tc = tinycolor(color.combine(cstr, background));
if(c.alpha() !== 1) c = color(combine(cstr, background));

var newColor = tc.isDark() ?
(lightAmount ? tc.lighten(lightAmount) : background) :
(darkAmount ? tc.darken(darkAmount) : defaultLine);
// TODO: Should the API change such that lightAmount/darkAmount are passed in as decimal instead of percent number?
const newColor = color(
c.isDark()
? (lightAmount ? c.lighten(lightAmount / 100) : background)
: (darkAmount ? c.darken(darkAmount / 100) : defaultLine)
);

return newColor.toString();
return newColor.rgb().string();
};

color.stroke = function(s, c) {
var tc = tinycolor(c);
s.style({stroke: color.tinyRGB(tc), 'stroke-opacity': tc.getAlpha()});
};
const stroke = (s, cstr) => s.style({ stroke: rgb(cstr), 'stroke-opacity': opacity(cstr) });

color.fill = function(s, c) {
var tc = tinycolor(c);
s.style({
fill: color.tinyRGB(tc),
'fill-opacity': tc.getAlpha()
});
};
const fill = (s, cstr) => s.style({ fill: rgb(cstr), 'fill-opacity': opacity(cstr) });

// search container for colors with the deprecated rgb(fractions) format
// and convert them to rgb(0-255 values)
color.clean = function(container) {
const clean = container => {
if(!container || typeof container !== 'object') return;

var keys = Object.keys(container);
Expand All @@ -134,13 +120,13 @@ color.clean = function(container) {

var el0 = val[0];
if(!Array.isArray(el0) && el0 && typeof el0 === 'object') {
for(j = 0; j < val.length; j++) color.clean(val[j]);
for(j = 0; j < val.length; j++) clean(val[j]);
}
} else if(val && typeof val === 'object' && !isTypedArray(val)) color.clean(val);
} else if(val && typeof val === 'object' && !isTypedArray(val)) clean(val);
}
};

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

var valTrim = val.trim();
Expand Down Expand Up @@ -181,3 +167,51 @@ function cleanOne(val) {
if(rgba) return 'rgba(' + rgbStr + ', ' + parts[3] + ')';
return 'rgb(' + rgbStr + ')';
}

const equals = (cstr1, cstr2) => cstr1 && cstr2 && color(cstr1).rgb().string() === color(cstr2).rgb().string();

const isValid = cstr => {
try { return cstr && !!color(cstr); }
catch { return false; }
}

const mix = (cstr1, cstr2, weight) => color(cstr1).mix(color(cstr2), weight / 100).rgb().string();

const mostReadable = (baseColor, colorList = []) => {
let bestColor;
let bestContrast = -Infinity;

for (const cstr of colorList) {
const contrast = color(baseColor).contrast(color(cstr));
if (contrast > bestContrast) {
bestContrast = contrast;
bestColor = color(cstr).rgb().string();
}
}

// Fall back to black/white if provided colors don't have proper contrast level
return bestColor && color(baseColor).level(color(bestColor))
? bestColor
: mostReadable(baseColor, ["#000", "#fff"]);
};

module.exports = {
addOpacity,
background,
clean,
color,
combine,
contrast,
defaultLine,
defaults,
equals,
fill,
interpolate,
isValid,
lightLine,
mix,
mostReadable,
opacity,
rgb,
stroke
}
10 changes: 4 additions & 6 deletions src/components/colorbar/draw.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

var d3 = require('@plotly/d3');
var tinycolor = require('tinycolor2');

var Plots = require('../../plots/plots');
var Registry = require('../../registry');
Expand Down Expand Up @@ -548,10 +547,9 @@ function drawColorBar(g, opts, gd) {
if(opts._fillgradient) {
Drawing.gradient(fillEl, gd, opts._id, isVertical ? 'vertical' : 'horizontalreversed', opts._fillgradient, 'fill');
} else {
// tinycolor can't handle exponents and
// at this scale, removing it makes no difference.
// The color library can't handle exponents and at this scale, removing it makes no difference.
var colorString = fillColormap(d).replace('e-', '');
fillEl.attr('fill', tinycolor(colorString).toHexString());
fillEl.attr('fill', Color.color(colorString).hex());
}
});

Expand Down Expand Up @@ -716,8 +714,8 @@ function drawColorBar(g, opts, gd) {

if(!isVertical && (
borderwidth || (
tinycolor(bgcolor).getAlpha() &&
!tinycolor.equals(fullLayout.paper_bgcolor, bgcolor)
Color.opacity(bgcolor) &&
!Color.equals(fullLayout.paper_bgcolor, bgcolor)
)
)) {
// for horizontal colorbars when there is a border line or having different background color
Expand Down
Loading