chroma-js
Version:
JavaScript library for color conversions
395 lines (358 loc) • 11.3 kB
JavaScript
// minimal multi-purpose interface
// @requires utils color analyze
import chroma from '../chroma.js';
import { limit, type } from '../utils/index.js';
const { pow } = Math;
export default function (colors) {
// constructor
let _mode = 'rgb';
let _nacol = chroma('#ccc');
let _spread = 0;
// const _fixed = false;
let _domain = [0, 1];
let _pos = [];
let _padding = [0, 0];
let _classes = false;
let _colors = [];
let _out = false;
let _min = 0;
let _max = 1;
let _correctLightness = false;
let _colorCache = {};
let _useCache = true;
let _gamma = 1;
// private methods
const setColors = function (colors) {
colors = colors || ['#fff', '#000'];
if (
colors &&
type(colors) === 'string' &&
chroma.brewer &&
chroma.brewer[colors.toLowerCase()]
) {
colors = chroma.brewer[colors.toLowerCase()];
}
if (type(colors) === 'array') {
// handle single color
if (colors.length === 1) {
colors = [colors[0], colors[0]];
}
// make a copy of the colors
colors = colors.slice(0);
// convert to chroma classes
for (let c = 0; c < colors.length; c++) {
colors[c] = chroma(colors[c]);
}
// auto-fill color position
_pos.length = 0;
for (let c = 0; c < colors.length; c++) {
_pos.push(c / (colors.length - 1));
}
}
resetCache();
return (_colors = colors);
};
const getClass = function (value) {
if (_classes != null) {
const n = _classes.length - 1;
let i = 0;
while (i < n && value >= _classes[i]) {
i++;
}
return i - 1;
}
return 0;
};
let tMapLightness = (t) => t;
let tMapDomain = (t) => t;
// const classifyValue = function(value) {
// let val = value;
// if (_classes.length > 2) {
// const n = _classes.length-1;
// const i = getClass(value);
// const minc = _classes[0] + ((_classes[1]-_classes[0]) * (0 + (_spread * 0.5))); // center of 1st class
// const maxc = _classes[n-1] + ((_classes[n]-_classes[n-1]) * (1 - (_spread * 0.5))); // center of last class
// val = _min + ((((_classes[i] + ((_classes[i+1] - _classes[i]) * 0.5)) - minc) / (maxc-minc)) * (_max - _min));
// }
// return val;
// };
const getColor = function (val, bypassMap) {
let col, t;
if (bypassMap == null) {
bypassMap = false;
}
if (isNaN(val) || val === null) {
return _nacol;
}
if (!bypassMap) {
if (_classes && _classes.length > 2) {
// find the class
const c = getClass(val);
t = c / (_classes.length - 2);
} else if (_max !== _min) {
// just interpolate between min/max
t = (val - _min) / (_max - _min);
} else {
t = 1;
}
} else {
t = val;
}
// domain map
t = tMapDomain(t);
if (!bypassMap) {
t = tMapLightness(t); // lightness correction
}
if (_gamma !== 1) {
t = pow(t, _gamma);
}
t = _padding[0] + t * (1 - _padding[0] - _padding[1]);
t = limit(t, 0, 1);
const k = Math.floor(t * 10000);
if (_useCache && _colorCache[k]) {
col = _colorCache[k];
} else {
if (type(_colors) === 'array') {
//for i in [0.._pos.length-1]
for (let i = 0; i < _pos.length; i++) {
const p = _pos[i];
if (t <= p) {
col = _colors[i];
break;
}
if (t >= p && i === _pos.length - 1) {
col = _colors[i];
break;
}
if (t > p && t < _pos[i + 1]) {
t = (t - p) / (_pos[i + 1] - p);
col = chroma.interpolate(
_colors[i],
_colors[i + 1],
t,
_mode
);
break;
}
}
} else if (type(_colors) === 'function') {
col = _colors(t);
}
if (_useCache) {
_colorCache[k] = col;
}
}
return col;
};
var resetCache = () => (_colorCache = {});
setColors(colors);
// public interface
const f = function (v) {
const c = chroma(getColor(v));
if (_out && c[_out]) {
return c[_out]();
} else {
return c;
}
};
f.classes = function (classes) {
if (classes != null) {
if (type(classes) === 'array') {
_classes = classes;
_domain = [classes[0], classes[classes.length - 1]];
} else {
const d = chroma.analyze(_domain);
if (classes === 0) {
_classes = [d.min, d.max];
} else {
_classes = chroma.limits(d, 'e', classes);
}
}
return f;
}
return _classes;
};
f.domain = function (domain) {
if (!arguments.length) {
return _domain;
}
_min = domain[0];
_max = domain[domain.length - 1];
_pos = [];
const k = _colors.length;
if (domain.length === k && _min !== _max) {
// update positions
for (let d of Array.from(domain)) {
_pos.push((d - _min) / (_max - _min));
}
} else {
for (let c = 0; c < k; c++) {
_pos.push(c / (k - 1));
}
if (domain.length > 2) {
// set domain map
const tOut = domain.map((d, i) => i / (domain.length - 1));
const tBreaks = domain.map((d) => (d - _min) / (_max - _min));
if (!tBreaks.every((val, i) => tOut[i] === val)) {
tMapDomain = (t) => {
if (t <= 0 || t >= 1) return t;
let i = 0;
while (t >= tBreaks[i + 1]) i++;
const f =
(t - tBreaks[i]) / (tBreaks[i + 1] - tBreaks[i]);
const out = tOut[i] + f * (tOut[i + 1] - tOut[i]);
return out;
};
}
}
}
_domain = [_min, _max];
return f;
};
f.mode = function (_m) {
if (!arguments.length) {
return _mode;
}
_mode = _m;
resetCache();
return f;
};
f.range = function (colors, _pos) {
setColors(colors, _pos);
return f;
};
f.out = function (_o) {
_out = _o;
return f;
};
f.spread = function (val) {
if (!arguments.length) {
return _spread;
}
_spread = val;
return f;
};
f.correctLightness = function (v) {
if (v == null) {
v = true;
}
_correctLightness = v;
resetCache();
if (_correctLightness) {
tMapLightness = function (t) {
const L0 = getColor(0, true).lab()[0];
const L1 = getColor(1, true).lab()[0];
const pol = L0 > L1;
let L_actual = getColor(t, true).lab()[0];
const L_ideal = L0 + (L1 - L0) * t;
let L_diff = L_actual - L_ideal;
let t0 = 0;
let t1 = 1;
let max_iter = 20;
while (Math.abs(L_diff) > 1e-2 && max_iter-- > 0) {
(function () {
if (pol) {
L_diff *= -1;
}
if (L_diff < 0) {
t0 = t;
t += (t1 - t) * 0.5;
} else {
t1 = t;
t += (t0 - t) * 0.5;
}
L_actual = getColor(t, true).lab()[0];
return (L_diff = L_actual - L_ideal);
})();
}
return t;
};
} else {
tMapLightness = (t) => t;
}
return f;
};
f.padding = function (p) {
if (p != null) {
if (type(p) === 'number') {
p = [p, p];
}
_padding = p;
return f;
} else {
return _padding;
}
};
f.colors = function (numColors, out) {
// If no arguments are given, return the original colors that were provided
if (arguments.length < 2) {
out = 'hex';
}
let result = [];
if (arguments.length === 0) {
result = _colors.slice(0);
} else if (numColors === 1) {
result = [f(0.5)];
} else if (numColors > 1) {
const dm = _domain[0];
const dd = _domain[1] - dm;
result = __range__(0, numColors, false).map((i) =>
f(dm + (i / (numColors - 1)) * dd)
);
} else {
// returns all colors based on the defined classes
colors = [];
let samples = [];
if (_classes && _classes.length > 2) {
for (
let i = 1, end = _classes.length, asc = 1 <= end;
asc ? i < end : i > end;
asc ? i++ : i--
) {
samples.push((_classes[i - 1] + _classes[i]) * 0.5);
}
} else {
samples = _domain;
}
result = samples.map((v) => f(v));
}
if (chroma[out]) {
result = result.map((c) => c[out]());
}
return result;
};
f.cache = function (c) {
if (c != null) {
_useCache = c;
return f;
} else {
return _useCache;
}
};
f.gamma = function (g) {
if (g != null) {
_gamma = g;
return f;
} else {
return _gamma;
}
};
f.nodata = function (d) {
if (d != null) {
_nacol = chroma(d);
return f;
} else {
return _nacol;
}
};
return f;
}
function __range__(left, right, inclusive) {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}