Public
Edited
Apr 7
Importers
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
colorSpaceDisplayHaveNote = true
Insert cell
Insert cell
rgbScale = 0.39
Insert cell
cWidth = Math.min(width, 2 * rgbdata.length);
Insert cell
function createCtx(w, h) {
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
canvas.style.width = w + "px";
return canvas.getContext("2d", {"colorSpace": colorSpace});
}
Insert cell
function displaySpectrum(correction, height = 100, cssBg = null){
const ctx = createCtx(rgbdata.length, height);
var cssColor0 = cssBg ? cssBg : colorToCss(correction ? correction([1e-6, 1e-6, 1e-6]) : [0, 0, 0]);
ctx.fillStyle = cssColor0;
ctx.fillRect(0, 0, rgbdata.length, height);
for (var i = 0; i < rgbdata.length; ++i)
{
var r = rgbdata[i].r * rgbScale;
var g = rgbdata[i].g * rgbScale;
var b = rgbdata[i].b * rgbScale;
var a = 1;

if (correction) [r, g, b, a] = correction([r, g, b]);

var cssColor = colorToCss([r, g, b, a]);
var gradient = ctx.createLinearGradient(0, 0, 0, height);
try {
gradient.addColorStop(0.10, cssColor0);
gradient.addColorStop(0.15, cssColor);
gradient.addColorStop(0.85, cssColor);
gradient.addColorStop(0.90, cssColor0);
ctx.beginPath();
ctx.fillStyle = gradient;
ctx.fillRect(i, 0, 1, height);
} catch (e) { console.log(cssColor0, cssColor) }

if (showOutOfGamut && Math.min(r, g, b) < 0)
{
ctx.fillStyle = '#f0f';
ctx.fillRect(i, height-15, 1, 15);
}
}

var div = html`<div>`;
div.appendChild(ctx.canvas);
ctx.canvas.style.width = cWidth + 'px';
ctx.canvas.style.height = height + 'px';
return div;
}
Insert cell
function displaySpectrumGraph(correction, {height=350, domain=[0, 100], scale=rgbScale, displaySpace=true, colorbar=false})
{
var rgbdata2 = rgbdata.map(row => {
var {r, g, b, wl} = row;
[r, g, b] = correction([r * scale, g * scale, b * scale]);
return {r:r, g:g, b:b, wl:wl};
});
var plotConfig = {
height: height,
insetBottom: colorbar ? 4 : 0, // make room for color bar
x: {label: "Wavelength (nm) →"},
y: {
label: displaySpace ? "↑ sRGB (%)" : "↑ RGB",
grid: true,
domain: domain,
percent: displaySpace
},
marks: [
Plot.areaY(rgbdata2, {x: "wl", y: "r", fill: "#f00", opacity: .07}),
Plot.areaY(rgbdata2, {x: "wl", y: "g", fill: "#0f0", opacity: .07}),
Plot.areaY(rgbdata2, {x: "wl", y: "b", fill: "#00f", opacity: .07}),
Plot.line(rgbdata2, {x: "wl", y: "r", stroke: "#f00"}),
Plot.line(rgbdata2, {x: "wl", y: "g", stroke: "#0d0"}),
Plot.line(rgbdata2, {x: "wl", y: "b", stroke: "#00f"}),
Plot.ruleY([0]),
]
};
if (displaySpace)
{
plotConfig.marks.push(
Plot.ruleY([1])
);
}
if (colorbar)
{
function rgbBar(e)
{
var rgb = rgbCap(rgbGreyDesaturate([e.r, e.g, e.b], {overdrive: 1.8}));
return colorToCss(sRGB(rgb));
}
rgbCap
plotConfig.marks.push(
// colored bar
Plot.line(rgbdata, {x: "wl", y: -1,strokeWidth: 5, z: null, dy: 2, stroke: rgbBar})
);
}
return Plot.plot(plotConfig);
}
Insert cell
Insert cell
function desaturate([r, g, b], amount) {
var y = rgbToY([r, g, b]);
return [
r * (1 - amount) + y * amount,
g * (1 - amount) + y * amount,
b * (1 - amount) + y * amount];
}
Insert cell
function desaturateToGamut([r, g, b], amount)
{
// Y value (luminance) of our color. We always return a color with this luminance.
var y = rgbToY([r, g, b]);

// clip to 3 RGB cube faces (the ones going through (0, 0, 0))
var min = Math.min(r, g, b, 0);
// inverse lerp: add small extra margin so we definitely end up inside the RGB cube.
// turn NaN into 0 in case we receive [0, 0, 0]
var amountNeeded = (-min / (y - min) * 1.00001 * amount) || 0;
return desaturate([r, g, b], amountNeeded);
}
Insert cell
desaturateToGamut([0, 0, 0])
Insert cell
function desaturateBlues([r, g, b], amount)
{
// Y value (luminance) of our color. We always return a color with this luminance.
var y = rgbToY([r, g, b]);

// similar to desaturateToGamut(), but clip colors to some arbitrary
// extra plane in the RGB space, to keep them away from the blue primary.
// amount controls how far away from the blue primary this plane is.
var b1 = r * 0.75 + g * 1.25 - b * (amount - 0.06);
if (b1 < 0)
{
var y1 = 2*y - y * amount; // value of b1 if we fully desaturate.
var amountNeeded = -b1 / (y1 - b1); // inverse lerp for target value 0
// soft low shoulder:
amountNeeded = amountNeeded*amountNeeded / (0.1 + amountNeeded);
return desaturate([r, g, b], Math.max(amountNeeded, 0));
}
else
{
return [r, g, b];
}
}
Insert cell
function mix([r1, g1, b1], [r2, g2, b2], amount, method)
{
if (method == 'Oklab')
{
[r1, g1, b1] = sRGBtoOklab([r1, g1, b1]);
[r2, g2, b2] = sRGBtoOklab([r2, g2, b2]);
}
var r = r1 * (1 - amount) + r2 * amount;
var g = g1 * (1 - amount) + g2 * amount;
var b = b1 * (1 - amount) + b2 * amount;
if (method == 'Oklab')
{
[r, g, b] = OklabTosRGB([r, g, b]);
}
return [r, g, b];
}
Insert cell
function mixGrey([r, g, b], amount, grey=0.95) {
return mix([r, g, b], [grey, grey, grey], amount);
}
Insert cell
function rgbNormalize([r, g, b])
{
var scale = 1/Math.max(Math.hypot(r, g, b), 0.0001);
return [r*scale, g*scale, b*scale];
}
Insert cell
function rgbCap([r, g, b])
{
// soft clamps the RGB color to 100%. This is done by simple scaling.
// use 4-norm. This results in less reduced brightness of yellows than the 2-norm,
// but still avoids the harsh clipping of using max() (i.e. ∞-norm).
var norm4 = Math.sqrt(Math.hypot(r*r, g*g, b*b));
var scale = Math.min(1, 1/Math.max(norm4, 0.0001));
return [r*scale, g*scale, b*scale];
}
Insert cell
function angry(rgb)
{
var [r, g, b] = desaturateToGamut(rgb, 1.0, 0.0);
var val = Math.max(r, g, b, 0.00001);
return [r/val, g/val, b/val];
}
Insert cell
Insert cell
function appxSineBow(rgb)
{
// Map a color onto a color in the sinebow by linear hue (approximately)
var [r, g, b] = angry(rgb)
// an angry color has exactly one value that is not 0 or 1
// this value is related to linear hue, and depending on this
// value we will desaturate and multiply our color.
var x = r;
if (x < .0001 || x == 1) x = g;
if (x < .0001 || x == 1) x = b;
// A sinebow is usually defined in sRGB. If you plot this by
// linear hue you get these strange shapes. Some empirical
// polynomial approximations:
var mx = 1 - x;
var mx4 = mx*mx*mx*mx
var mx12 = mx4*mx4*mx4
var appx_val = 0.83 - 0.29 * x + 0.3 * mx4 - 0.13 * mx12;
var appx_desat = 0.015 * mx4 + 0.035 * mx12;
[r, g, b] = mixGrey([r, g, b], appx_desat, 1);
return [r * appx_val, g * appx_val, b * appx_val];
}
Insert cell
Insert cell
Insert cell
colorSpace = {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", {"colorSpace": colorSpaceChoice});
return ctx.getContextAttributes().colorSpace;
}
Insert cell
function testPattern() {
const ctx2 = [null, null];
var div = html`<div>`
for (var i = 0; i < 2; ++i)
{
const canvas = document.createElement("canvas");
canvas.width = 100;
canvas.height = 20;
canvas.style.width = "4em";
canvas.style.height = "2em";
canvas.style.display = "block";
var rgbM = m_m(RgbColorM.xyzToRgb, SrgbM.rgbToXyz);
var rgb = [[.9, .2, .3], [1.0, 0.8, .3], [.0, .9, .2], [.0, .8, .9], [.6, .4, 1.0]];
const ctx = canvas.getContext("2d", {"colorSpace": i == 0 ? "srgb" : colorSpaceChoice});
for (var ij = 0; ij < rgb.length; ++ij) {
ctx.fillStyle = colorToCss(sRGB(i == 0 ? rgb[ij] : apply_m(rgbM, rgb[ij])), i == 0 ? "srgb" : undefined);
ctx.fillRect(20 * ij, 0, 20, 20);
}

ctx2[i] = ctx;
div.appendChild(canvas)
}
return div;
}
Insert cell
testCss = {
var div = html`<div style='line-height:1'>`
for (var i = 0; i < 2; ++i)
{
var div2 = html`<div>→`
div.appendChild(div2)
var rgbM = m_m(DisplayP3M.xyzToRgb, SrgbM.rgbToXyz);
var rgb = [[1, .2, .2], [.2, 1, .2], [.2, .2, 1], [.8, .8, .8]];
for (var ij = 0; ij < rgb.length; ++ij) {
var c = colorToCss(sRGB(i == 0 ? rgb[ij] : apply_m(rgbM, rgb[ij])), i == 0 ? "srgb" : "display-p3");
div2.appendChild(html`<span style="background: ${c}; padding-left: 1em;"></span>`)
}
}
return div;
}
Insert cell
Insert cell
function clip(x) { return Math.max(0, Math.min(1, x)); }
Insert cell
function colorToCss([r, g, b], otherColorSpace) {
otherColorSpace ??= colorSpace;
return 'color(' + otherColorSpace + ' ' + clip(r) + ' ' + clip(g) + ' ' + clip(b) + ')';
}
Insert cell
function sRGB([r, g, b])
{
// both sRGB and Display-P3 use this transfer function.
var a = 0.055;
r = r < .0031308 ? r / 12.92 : (1 + a) * Math.pow(r, 1/2.4) - a;
g = g < .0031308 ? g / 12.92 : (1 + a) * Math.pow(g, 1/2.4) - a;
b = b < .0031308 ? b / 12.92 : (1 + a) * Math.pow(b, 1/2.4) - a;
return [r, g, b]
}
Insert cell
Insert cell
Insert cell
function sRGBtoOklab([r, g, b])
{
var [l, m, s] = apply_m(OklabLmsRgbM.rgbToLms, [r, g, b]);

l = Math.cbrt(l);
m = Math.cbrt(m);
s = Math.cbrt(s);

return [
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
];
}
Insert cell
function OklabTosRGB([L, a, b])
{
var l = L + 0.3963377774 * a + 0.2158037573 * b;
var m = L - 0.1055613458 * a - 0.0638541728 * b;
var s = L - 0.0894841775 * a - 1.2914855480 * b;
l = l * l * l;
m = m * m * m;
s = s * s * s;
return apply_m(OklabLmsRgbM.lmsToRgb, [l, m, s]);
}
Insert cell
function rgbToY([r, g, b])
{
const [m21, m22, m23] = RgbColorM.rgbToXyz[1];
return m21 * r + m22 * g + m23 * b;
}
Insert cell
Insert cell
function OklabCopyHue(rgb, rgbHue, desat = true)
{
var Lab1 = sRGBtoOklab(rgbHue);
var Lab2 = sRGBtoOklab(rgb);
var hue1 = Math.atan2(Lab1[2], Lab1[1]);
var sat2 = Math.hypot(Lab2[1], Lab2[2]);
var cs = Math.cos(hue1);
var sn = Math.sin(hue1);
var [r, g, b] = OklabTosRGB([Lab2[0], sat2 * cs, sat2 * sn])

// desaturate again if requested
if (desat) {
var min = Math.min(r, g, b);
if (min < 0) {
var max = Math.max(r, g, b);
sat2 *= 1 - 2 * -min / max;
[r, g, b] = OklabTosRGB([Lab2[0], sat2 * cs, sat2 * sn]);
}
}
return [r, g, b];
}
Insert cell
function OklabSaturate(rgb, sat)
{
var [L, a, b] = sRGBtoOklab(rgb);
a *= sat;
b *= sat;
return OklabTosRGB([L, a, b]);
}
Insert cell
Insert cell
RgbColorM = (colorSpace == "display-p3" ? DisplayP3M : SrgbM)
Insert cell
OklabLmsRgbM = ({
rgbToLms : m_m(OklabLmsM.xyzToLms, RgbColorM.rgbToXyz),
lmsToRgb : m_m(RgbColorM.xyzToRgb, OklabLmsM.lmsToXyz),
})
Insert cell
DisplayP3M = ({
rgbToXyz:
[[ 0.4865709, 0.2656677, 0.1982173],
[ 0.2289746, 0.6917385, 0.0792869],
[ 0.0000000, 0.0451134, 1.0439444]],
xyzToRgb:
[[ 2.4934969, -0.9313836, -0.4027108],
[-0.8294890, 1.7626641, 0.0236247],
[ 0.0358458, -0.0761724, 0.9568845]]
})
Insert cell
SrgbM = ({
rgbToXyz:
[[ 0.4123908, 0.3575843, 0.1804808],
[ 0.2126390, 0.7151687, 0.0721923],
[ 0.0193308, 0.1191948, 0.9505322]],
xyzToRgb:
[[ 3.2409699, -1.5373832, -0.4986108],
[-0.9692436, 1.8759675, 0.0415551],
[ 0.0556301, -0.2039770, 1.0569715]]
})
Insert cell
OklabLmsM = ({
lmsToXyz:
[[ 1.22701385, -0.55779998, 0.28125615],
[-0.04058018, 1.11225687, -0.07167668],
[-0.07638128, -0.42148198, 1.58616322]],
xyzToLms:
[[0.8189330101, 0.3618667424, -0.1288597137],
[0.0329845436, 0.9293118715, 0.0361456387],
[0.0482003018, 0.2643662691, 0.6338517070]]
})
Insert cell
function apply_m(M, [r, g, b])
{
const [[m11, m12, m13], [m21, m22, m23], [m31, m32, m33]] = M;
return [
m11 * r + m12 * g + m13 * b,
m21 * r + m22 * g + m23 * b,
m31 * r + m32 * g + m33 * b];
}
Insert cell
function m_m(M1, M2)
{
function apply_left_m([r1, r2, r3], M2)
{
const [[m11, m12, m13], [m21, m22, m23], [m31, m32, m33]] = M2;
return [
m11 * r1 + m21 * r2 + m31 * r3,
m12 * r1 + m22 * r2 + m32 * r3,
m13 * r1 + m23 * r2 + m33 * r3];
}
return [
apply_left_m(M1[0], M2),
apply_left_m(M1[1], M2),
apply_left_m(M1[2], M2),
]
}
Insert cell
Insert cell
Insert cell
rgbdata = {
var data = [];
for (var i = 0; i < cie1931.x.length; ++i)
{
var x = cie1931.x[i];
var y = cie1931.y[i];
var z = cie1931.z[i];
var [r, g, b] = apply_m(RgbColorM.xyzToRgb, [x, y, z])
data.push({r: r, g: g, b: b, wl: 380 + i});
}
return data;
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more