Public
Edited
Aug 5, 2023
1 fork
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
colorSpaces = ({
munsell: munsellSpace,
oklab: oklabSpace,
cieluv: cieluvSpace,
jzazbz: jzazbzSpace
})
Insert cell
Insert cell
munsellSpace = new ColorSpace({
label: "Munsell",
fromSRgb: _.flow([(x) => munsell.rgbToMhvc(...x), mhvcToPolar, fromPolar]),
toSRgb: _.flow([toPolar, mhvcFromPolar, (x) => munsell.mhvcToRgb(...x)])
})
Insert cell
Insert cell
// Munsell luminance: 10
chromaScale = oklabSpace.scale.chroma /
oklabSpace.scale.luminance /
(munsellUnscaledChromaRms / 10)
Insert cell
oklabSpace.scale.chroma
Insert cell
munsellSpace.scale.chroma
Insert cell
// use this to make comparison with OKLab
munsellUnscaledFromSRgb = _.flow([
(x) => munsell.rgbToMhvc(...x),
(x) => [x[1], x[2], x[0] * 3.6], // LCh
fromPolar
])
Insert cell
munsellUnscaledChromaRms = scaleChroma(munsellUnscaledFromSRgb, {
colors: munsellGamutSRgb
})
Insert cell
// Number[3] -> Number[3]
mhvcToPolar = function (x) {
const [h, v, c] = x;

// scale chroma to OKLab, hue to 360 degrees
return [v, c * chromaScale, h * 3.6];
}
Insert cell
mhvcFromPolar = function (x) {
const [L, C, hue] = x;

// munsell library seems not to like hue values near, but not, zero
const H = math.abs(hue) < 1e-7 ? 0 : hue;

// scale chroma from OKLab, hue to 100
return [H / 3.6, L, C / chromaScale];
}
Insert cell
Insert cell
cieluvSpace = new ColorSpace({
label: "CIELuv",
fromSRgb: makeFromSRgb("luv", ["l", "u", "v"]),
toSRgb: (x) => (x[0] == 0 ? [0, 0, 0] : makeToSRgb("luv", ["l", "u", "v"])(x))
})
Insert cell
Insert cell
cieluvSpace.toSRgb([0, 0, 0])
Insert cell
Insert cell
oklabSpace = new ColorSpace({
label: "OKLab",
fromSRgb: makeFromSRgb("oklab", ["l", "a", "b"]),
toSRgb: makeToSRgb("oklab", ["l", "a", "b"])
})
Insert cell
oklabSample = _.flow(oklabSpace.fromSRgb, oklabSpace.to100)([0.5, 0.5, 0.5])
Insert cell
_.flow(oklabSpace.from100, oklabSpace.toSRgb)(oklabSample)
Insert cell
_.flow(oklabSpace.fromSRgb, oklabSpace.to100)([0.4, 0.2, 0.6])
Insert cell
_.flow(oklabSpace.fromSRgb)([0.5, 0, 0])
Insert cell
// to confirm
toOKlab = function (x) {
{
const l = 0.4122214708 * x[0] + 0.5363325363 * x[1] + 0.0514459929 * x[2];
const m = 0.2119034982 * x[0] + 0.6806995451 * x[1] + 0.1073969566 * x[2];
const s = 0.0883024619 * x[0] + 0.2817188376 * x[1] + 0.6299787005 * x[2];

const l_ = Math.cbrt(l);
const m_ = Math.cbrt(m);
const s_ = Math.cbrt(s);

return [
0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
];
}
}
Insert cell
_.flow([linearRgbFromSRgb, toOKlab])([0.5, 0, 0])
Insert cell
Insert cell
scaleJzazbz = {
const conv = culori.converter("jab");
const jabBlack = conv({ mode: "rgb", r: 1, g: 1, b: 1 });

return jabBlack.j;
}
Insert cell
jzazbzSpace = new ColorSpace({
label: "Jzazbz",
fromSRgb: makeFromSRgb("jab", ["j", "a", "b"]),
toSRgb: makeToSRgb("jab", ["j", "a", "b"])
})
Insert cell
_.flow([jzazbzSpace.fromSRgb, jzazbzSpace.to100])([0.4, 0.2, 0.6])
Insert cell
Insert cell
// String, String[] -> (Number[] -> Number[])
makeToSRgb = function (space, index) {
const converter = culori.converter("rgb");
return function (x) {
const objSpace = {
mode: space,
[index[0]]: x[0],
[index[1]]: x[1],
[index[2]]: x[2]
};
const objRGB = converter(objSpace);
return ["r", "g", "b"].map((x) => objRGB[x]);
};
}
Insert cell
// String, String[] -> (Number[] -> Number[])
makeFromSRgb = function (space, index) {
const converter = culori.converter(space);
return function (x) {
const objRgb = {
mode: "rgb",
r: x[0],
g: x[1],
b: x[2]
};
const objSpace = converter(objRgb);
return index.map((x) => objSpace[x]);
};
}
Insert cell
spaceNew = makeFromSRgb("oklab", ["l", "a", "b"])([0.5, 0.5, 0.5])
Insert cell
makeToSRgb("oklab", ["l", "a", "b"])(spaceNew)
Insert cell
Insert cell
colorUtils = ({
munsellRenotation,
mapColorSpaces,
inRgbGamut,
hexFromSRgb,
hexToSRgb,
d3StringFromSRgb,
d3StringToSRgb,
clampRgbGamut,
toPolar,
fromPolar,
linearRgbFromSRgb,
linearRgbToSRgb
})
Insert cell
Insert cell
mapColorSpaces()
Insert cell
mapColorSpaces = {
const defColorSpaces = colorSpaces;
return function ({ colorSpaces = defColorSpaces } = {}) {
const names = Object.keys(colorSpaces);
const arr = names.map((x) => [colorSpaces[x].label, x]);
return new Map(arr);
};
}
Insert cell
// Number -> Boolean
inGamut = function (x) {
return x >= 0 && x <= 1;
}
Insert cell
// Number[] -> Boolean
inRgbGamut = function (x) {
return _.every(x.map(inGamut));
}
Insert cell
inRgbGamut([-0.5, 0.5, 1.5]) // [false, true, false]
Insert cell
// Number -> Number
clampGamut = function (x) {
return x < 0 ? 0 : x > 1 ? 1 : x;
}
Insert cell
// Number[] -> Number[]
clampRgbGamut = function (x) {
return x.map(clampGamut);
}
Insert cell
clampRgbGamut([-0.5, 0.5, 1.5]) // [0, 0.5, 1]
Insert cell
// Number[3] -> Number[3]
toPolar = function (x) {
const [L, a, b] = x;
return [L, Math.sqrt(a ** 2 + b ** 2), (Math.atan2(b, a) * 180) / Math.PI];
}
Insert cell
polar = toPolar([50, 86.60254, 50])
Insert cell
// Number[3] -> Number[3]
fromPolar = function (x) {
// h expressed as degrees
const [L, c, h] = x;
const theta = (Math.PI / 180) * h;
return [L, c * Math.cos(theta), c * Math.sin(theta)];
}
Insert cell
fromPolar(polar)
Insert cell
// Number -> Number
linearFromS = function (x) {
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}
Insert cell
// Number[3] -> Number[3]
linearToS = function (x) {
return x <= 0.03928 / 12.92
? 12.92 * x
: 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
}
Insert cell
Insert cell
lin = arrZeroToOne.map(linearFromS)
Insert cell
lin.map(linearToS)
Insert cell
// Number -> Number
linearRgbFromSRgb = function (x) {
return x.map(linearFromS);
}
Insert cell
linRGB = linearRgbFromSRgb([0.5, 0.5, 0.5])
Insert cell
// Number[3] -> Number[3]
linearRgbToSRgb = function (x) {
return x.map(linearToS);
}
Insert cell
linearRgbToSRgb(linRGB)
Insert cell
// Number[3] -> String
hexFromSRgb = function (x) {
const color = { r: x[0], g: x[1], b: x[2], mode: "rgb" };
return culori.formatHex(color);
}
Insert cell
hexFromSRgb([0.4, 0.2, 0.6]) // "#663399"
Insert cell
// String -> Number[3]
hexToSRgb = function (x) {
const color = culori.parseHex(x);
return [color.r, color.g, color.b];
}
Insert cell
hexToSRgb("#663399")
Insert cell
// String -> Number[3]
d3StringToSRgb = function (x) {
const regex = /\d+/g;
return x.match(regex).map((x) => +x / 255);
}
Insert cell
d3StringToSRgb(d3.interpolateBlues(0))
Insert cell
// Number[3] -> String
d3StringFromSRgb = function (x) {
const format = d3.format(".0f");
return `rgb(${x.map((x) => format(x * 255)).join(", ")})`;
}
Insert cell
d3StringFromSRgb([1.23, 4.56, 5.78])
Insert cell
// adapted from https://observablehq.com/@d3/color-schemes
function swatches(colors) {
const n = colors.length;
const dark = d3.lab(colors[0]).l < 50;
const canvas = svg`<svg viewBox="0 0 ${n} 1" style="display:block;width:${
n * 33
}px;height:33px;margin:0;">${colors.map(
(c, i) => svg`<rect x=${i} width=1 height=1 fill=${c}>`
)}`;

return html`<div>${canvas}</div>`;
}
Insert cell
Insert cell
class ColorSpace {
constructor({ label = "", fromSRgb = _.identity, toSRgb = _.identity } = {}) {
this.label = label;
// TODO: validate that these are functions
this.fromSRgb = fromSRgb;
this.toSRgb = toSRgb;

this.scale = {
luminance: scaleLuminance(this.fromSRgb),
chroma: scaleChroma(this.fromSRgb),
maxChroma: scaleMaxChroma(this.fromSRgb)
};

const scale100 = 100 / this.scale.luminance;
this.from100 = (x) => math.divide(x, scale100);
this.to100 = (x) => math.multiply(x, scale100);

this.scale.maxChroma100 = this.to100(this.scale.maxChroma);
}

maxChroma = (luminance, hue) => {
// short-circuit if white, black, or beyond
if (luminance <= 0 || luminance >= this.scale.luminance) return 0;

const toSRgb = this.toSRgb;

const fy = function (chroma) {
return _.flow(fromPolar, toSRgb, distGamut)([luminance, chroma, hue]);
};

return findRoot(fy, 0, this.scale.maxChroma);
};

maxChroma100 = (luminance100, hue) => {
const luminance = this.from100(luminance100);
return this.to100(this.maxChroma(luminance, hue));
};

clampChromaSRgb = (x) => {
if (inRgbGamut(x)) {
return x;
}

const [luminance, , hue] = _.flow(this.fromSRgb, toPolar)(x);
const chroma = this.maxChroma(luminance, hue);

return _.flow(fromPolar, this.toSRgb)([luminance, chroma, hue]);
};
}
Insert cell
munsellSpace.maxChroma(9, -180)
Insert cell
_.flow([fromPolar, munsellSpace.toSRgb, distGamut])([5, 2.5689, 0])
Insert cell
testSpace = munsellSpace
Insert cell
testChroma = [1.01, -0.01, 0.4]
Insert cell
_.flow(testSpace.fromSRgb, toPolar)(testChroma)
Insert cell
testChromaClamp = testSpace.clampChromaSRgb(testChroma)
Insert cell
_.flow(testSpace.fromSRgb, toPolar)(testChromaClamp)
Insert cell
testSpace.clampChromaSRgb([0.5, 1, 0])
Insert cell
Insert cell
// (Number[3] -> Number[3]) -> Number
scaleLuminance = function (fromSRgb) {
// given a function that translates from SRgb to a colorspace
// return the difference in luminance between "black" and "white"
const black = fromSRgb([0, 0, 0]);
const white = fromSRgb([1, 1, 1]);

return white[0] - black[0];
}
Insert cell
// (Number[3] -> Number[3]), Number[3][] -> Number
scaleChroma = function (fromSRgb, { colors = munsellGamutSRgb } = {}) {
// given a function that translates from SRgb to a colorspace, and a set of colors
// return the RMS chroma
const colorsChroma = colors.map(_.flow([fromSRgb, toPolar, (x) => x[1]]));

const sumsqChroma = colorsChroma.reduce((acc, val) => acc + val * val, 0);

const rmsChroma = math.sqrt(sumsqChroma / colors.length);

return rmsChroma;
}
Insert cell
scaleChroma(munsellSpace.fromSRgb)
Insert cell
scaleChroma(oklabSpace.fromSRgb, { colors: munsellGamutSRgb })
Insert cell
munsellGamutSRgb = munsellRenotation.map((x) => x.sRgb).filter(inRgbGamut)
Insert cell
munsellV5C8SRgb
Insert cell
scaleMaxChroma = function (
fromSRgb,
{
colors = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[0, 1, 1],
[1, 0, 1],
[0, 1, 1]
]
} = {}
) {
const colorsChroma = colors.map(_.flow([fromSRgb, toPolar, (x) => x[1]]));

return math.max(colorsChroma);
}
Insert cell
scaleMaxChroma(munsellSpace.fromSRgb)
Insert cell
// adapted from: https://github.com/mikolalysenko/bisect
findRoot = function (fy, lo, hi, { tol = 1e-3 } = {}) {
let yhi = fy(hi);
let ylo = fy(lo);

if (yhi > 0 == ylo > 0) {
throw "findRoot cannot proceed: both values on same side of zero";
}

// bisection
while (math.abs(hi - lo) > tol) {
let m = (hi + lo) / 2;
let ym = fy(m);

if (ym > 0 == yhi > 0) {
hi = m;
yhi = ym;
} else {
lo = m;
ylo = ym;
}
}

// secant
let approx = lo - ((hi - lo) * ylo) / (yhi - ylo);

return approx;
}
Insert cell
findRoot((x) => 1 - 2 * math.sqrt(x), 0, 100)
Insert cell
distGamut = function (x) {
return math.min(x.map((x) => [x, 1 - x]));
}
Insert cell
distGamut([0.5, 0.95, 1.01])
Insert cell
Insert cell
class ColorSurface {
constructor(fnSurface, space) {
this.fnSurface = fnSurface;
this.space = space;
}

get raster100() {
const fnSurface = this.fnSurface;
const toSrgb = _.flow([
colorUtils.fromPolar,
this.space.from100,
this.space.toSRgb
]);

return function (chroma, luminance) {
try {
const hue = fnSurface(luminance);
const sRgb = toSrgb([luminance, chroma, hue]);

const hex = colorUtils.inRgbGamut(sRgb)
? colorUtils.hexFromSRgb(sRgb)
: undefined;

return hex;
} catch (error) {
throw `${error} for {luminance: ${luminance}, chroma: ${chroma}}`;
}
};
}

plotMarks100 = ({ negateChroma = false } = {}) => {
return [
Plot.raster({
fill: negateChroma ? (x, y) => this.raster100(-x, y) : this.raster100,
x1: 0,
x2: negateChroma
? -this.space.scale.maxChroma100
: this.space.scale.maxChroma100,
y1: 0,
y2: 100
})
];
};

// todo: serialize to object, create from object

static fromHue = function (
hue,
{ lum100 = 50, dHue100 = 0, space = colorSpaces.munsell } = {}
) {
if (!(typeof hue === "number")) {
throw "hue is not a number";
}

const fnSurface = function (luminance) {
return hue + dHue100 * (luminance - lum100);
};

return new ColorSurface(fnSurface, space);
};

static fromHex = function (
hex,
{ dHue100 = 0, space = colorSpaces.munsell } = {}
) {
if (!(typeof hex === "string")) {
throw "hex is not a string";
}

const [lum100, , hue] = _.flow(
colorUtils.hexToSRgb,
space.fromSRgb,
space.to100,
colorUtils.toPolar
)(hex);

return this.fromHue(hue, {
lum100: lum100,
dHue100: dHue100,
space: space
});
};

static fromHexHex = function (
hex1,
hex2,
{ short = true, space = colorSpaces.munsell } = {}
) {
if (!(typeof hex1 === "string")) {
throw "hex1 is not a string";
}

if (!(typeof hex2 === "string")) {
throw "hex2 is not a string";
}

const toPolar = _.flow(
colorUtils.hexToSRgb,
space.fromSRgb,
space.to100,
colorUtils.toPolar
);

const [lum100_1, , hue1] = toPolar(hex1);
const [lum100_2, , hue2] = toPolar(hex2);

const dHue100 =
deltaHue(hue1, hue2, { short: short }) / (lum100_2 - lum100_1);
return this.fromHue(hue1, {
lum100: lum100_1,
dHue100: dHue100,
space: space
});
};

static fromHexArray(arrHex, { space = colorSpaces.jzazbz }) {
// idea: spline hue = f(luminance)
// need to assure monotonicity
throw "not implemented";
}
}
Insert cell
surface = {
return ColorSurface.fromHexHex("#0000FF", "#00D8FF", {
short: true,
space: colorSpaces.munell
});
// return ColorSurface.fromHue(270, { lum100: 30, dHue100: -1 });
// return ColorSurface.fromHex("#00FFFF", { dHue100: -1 });
}
Insert cell
Plot.plot({
width: (200 * surface.space.scale.maxChroma100) / 15,
aspectRatio: 3,
grid: true,
x: { label: "chroma" },
y: { label: "luminance" },
marks: [
//...surface.plotMarks100({ negateChroma: true }),
surface.plotMarks100()
]
})
Insert cell
Insert cell
deltaHue = function (h1, h2, { short = true } = {}) {
const difference = h2 - h1;
const isDiffShort = math.abs(difference) < 180;

const complement = difference > 0 ? difference - 360 : 360 + difference;

return short == isDiffShort ? difference : complement;
}
Insert cell
deltaHue(45, 90, { short: true })
Insert cell
Insert cell
class ColorTrajectory {
constructor(controlPoints) {
// array of objects with luminance, chroma
if (!Array.isArray(controlPoints)) {
throw "controlPoints must be an array";
}

const hasForm = controlPoints.map((x) => {
return typeof x.luminance == "number" && typeof x.chroma == "number";
});

if (hasForm.length == 0 || !_.every(hasForm)) {
throw "Each element of controlPoints must have a `luminance` and `chroma`, both numbers";
}

this.controlPoints = controlPoints;
this.spline = (x) => ({
luminance: d3.interpolateBasis(controlPoints.map((x) => x.luminance))(x),
chroma: d3.interpolateBasis(controlPoints.map((x) => x.chroma))(x)
});
}

plotMarks100ControlPoints = ({
negateChroma = false,
rControlPoints = 10
} = {}) => {
const controlPoints = this.controlPoints.map(negChroma(negateChroma));

return [
Plot.dot(controlPoints, {
x: "chroma",
y: "luminance",
stroke: "black",
strokeWidth: 1,
r: rControlPoints,
symbol: "times"
}),
Plot.dot(controlPoints, {
x: "chroma",
y: "luminance",
stroke: "white",
strokeWidth: 1,
r: rControlPoints,
symbol: "plus"
})
];
};

// TODO: serialize, de-serialize

plotMarks100Spline = ({
negateChroma = false,
nStepSpline = 100, // approximate, as d3.ticks clamps to multiples of 2 and 5
strokeSpline = "#777777"
} = {}) => {
const samples = d3
.ticks(0, 1, nStepSpline)
.map(this.spline)
.map(negChroma(negateChroma));

return [
Plot.line(samples, { x: "chroma", y: "luminance", stroke: strokeSpline })
];
};

plotMarks100 = ({
negateChroma = false,
nStepSpline = 100,
strokeSpline = "#777777",
rControlPoints = 10
} = {}) => {
return [
...this.plotMarks100ControlPoints({ negateChroma, rControlPoints }),
...this.plotMarks100Spline({
negateChroma,
nStepSpline,
strokeSpline
})
];
};
}
Insert cell
// Boolean -> (Object -> Object)
negChroma = function (negate) {
return function (x) {
return negate ? { ...x, chroma: -x.chroma } : x;
};
}
Insert cell
traj = new ColorTrajectory([
{ luminance: 90, chroma: 0 },
{ luminance: 87, chroma: 6.5 },
{ luminance: 75, chroma: 13 },
{ luminance: 45, chroma: 14.5 },
{ luminance: 30, chroma: 12 }
])
Insert cell
Plot.plot({
width: (200 * surface.space.scale.maxChroma100) / 30,
aspectRatio: 1,
grid: true,
x: { label: "chroma" },
y: { label: "luminance" },
marks: [
...surface.plotMarks100({ negateChroma: false }),
...traj.plotMarks100({ negateChroma: false })
]
})
Insert cell
[0, 0.5, 1].map(traj.spline)
Insert cell
Insert cell
class ColorScaleSequential {
constructor(surface, trajectory, { nStep = 100, scale = 1 } = {}) {
if (!(surface instanceof ColorSurface)) {
throw "surface must be a ColorSurface";
}

if (!(trajectory instanceof ColorTrajectory)) {
throw "trajectory must be a ColorTrajectory";
}

this.surface = surface;
this.trajectory = trajectory;
this.nStep = nStep;

// calculate length100
const indices = d3.ticks(0, 1, nStep);
const distances = indices
.map(this.trajectory.spline) // get luminance, chroma
.map((x) => [x.luminance, x.chroma, this.surface.fnSurface(x.luminance)]) // get hue, put in array
.map(colorUtils.fromPolar) // into Cartesian
.map((x, i, arr) => {
return i == 0 ? 0 : math.distance(x, arr[i - 1]); // distance from previous point
});

const cumulativeDistances = math.cumsum(distances);

this.distance = cumulativeDistances.slice(-1)[0];

const normalizedDistances = cumulativeDistances.map(
(x) => x / this.distance
);

const rescaleFunction = (x) => scale * x;
const transferFunction = NaturalCubicSpline(normalizedDistances, indices);

this.sRgbInterp = _.flow([
rescaleFunction,
transferFunction,
this.trajectory.spline,
(x) => [x.luminance, x.chroma, this.surface.fnSurface(x.luminance)],
fromPolar,
this.surface.space.from100,
this.surface.space.toSRgb,
this.surface.space.clampChromaSRgb
]);

this.hexInterp = _.flow([this.sRgbInterp, colorUtils.hexFromSRgb]);
}

// use arrow notation to preserve "this"
data100 = (x) => {
const sRgb = this.sRgbInterp(x);
const lab = _.flow([this.surface.space.fromSRgb, this.surface.space.to100])(
sRgb
);

const lch = toPolar(lab);

return {
x: x,
luminance: lch[0],
chroma: lch[1],
hue: lch[2],
a: lab[1],
b: lab[2],
hex: hexFromSRgb(sRgb)
};
};

data100Points = (nStepPoints) => {
return d3
.range(0, 1, 1 / nStepPoints)
.concat(1)
.map(this.data100);
};

plotMarks100Points = ({
negateChroma: negateChroma,
nStepPoints = 10,
strokePoints = "#777777",
rPoints = 10
} = {}) => {
const data = this.data100Points(nStepPoints).map(negChroma(negateChroma));

return [
Plot.dot(data, {
x: "chroma",
y: "luminance",
r: rPoints,
fill: "hex",
stroke: strokePoints
})
];
};

plotMarks100 = ({
negateChroma: negateChroma,
nStepSpline = 100,
strokeSpline = "#777777",
rControlPoints = 10,
nStepPoints = 10,
strokePoints = "#777777",
rPoints = 10
} = {}) => {
return [
...this.surface.plotMarks100({ negateChroma }),
...this.trajectory.plotMarks100({
negateChroma,
nStepSpline,
strokeSpline,
rControlPoints
}),
...this.plotMarks100Points({
negateChroma,
nStepPoints,
strokePoints,
rPoints
})
];
};
}
Insert cell
scaleSeqBlue.hexInterp(1)
Insert cell
scaleSeqBlue.distance
Insert cell
scaleSeqBlue = new ColorScaleSequential(surface, traj)
Insert cell
Plot.plot({
width: (200 * scaleSeqBlue.surface.space.scale.maxChroma100) / 30,
aspectRatio: 1,
grid: true,
marks: scaleSeqBlue.plotMarks100()
})
Insert cell
Insert cell
scaleSeqBlue.data100Points(10)
Insert cell
scaleSeqBlue.data100(0)
Insert cell
Insert cell
class ColorScaleDiverging {
constructor(colorScaleSeqA, colorScaleSeqB, { scale = 1 } = {}) {
if (!(colorScaleSeqA instanceof ColorScaleSequential)) {
throw "colorScaleSeqA must be a ColorScaleSequential";
}

if (!(colorScaleSeqB instanceof ColorScaleSequential)) {
throw "colorScaleSeqB must be a ColorScaleSequential";
}

// TODO: verify that scales meet at t = 0
console.log(scale);

this.colorScaleSeqA = new ColorScaleSequential(
colorScaleSeqA.surface,
colorScaleSeqA.trajectory,
{ nStep: colorScaleSeqA.nStep, scale: scale }
);
this.colorScaleSeqB = new ColorScaleSequential(
colorScaleSeqB.surface,
colorScaleSeqB.trajectory,
{ nStep: colorScaleSeqB.nStep, scale: scale }
);

this.distance = this.colorScaleSeqA.distance + this.colorScaleSeqB.distance;
}

reflect(x, fnA, fnB) {
return x < 0.5 ? fnA(1 - 2 * x) : fnB(2 * x - 1);
}

sRgbInterp = (x) => {
return this.reflect(
x,
this.colorScaleSeqA.sRgbInterp,
this.colorScaleSeqB.sRgbInterp
);
};

hexInterp = _.flow([this.sRgbInterp, colorUtils.hexFromSRgb]);

data100 = (x) => {
return this.reflect(
x,
this.colorScaleSeqA.data100,
this.colorScaleSeqB.data100
);
};

data100Points = (nStepPoints) => {
return [
...this.colorScaleSeqA
.data100Points(nStepPoints / 2)
.map((a) => ({ ...a, x: 0.5 - 0.5 * a.x }))
.reverse()
.slice(0, -1), // avoid repeating the midpoint
...this.colorScaleSeqB
.data100Points(nStepPoints / 2)
.map((a) => ({ ...a, x: 0.5 + 0.5 * a.x }))
];
};

plotMarks100Points = ({
nStepPoints = 10,
strokePoints = "#777777",
rPoints = 10
} = {}) => {
const data = this.data100Points(nStepPoints).map((a) =>
a.x < 0.5 ? { ...a, chroma: -a.chroma } : a
);

return [
Plot.dot(data, {
x: "chroma",
y: "luminance",
r: rPoints,
fill: "hex",
stroke: strokePoints
})
];
};

plotMarks100 = ({
nStepSpline = 100,
strokeSpline = "#777777",
rControlPoints = 10,
nStepPoints = 10,
strokePoints = "#777777",
rPoints = 10
} = {}) => {
return [
...this.colorScaleSeqA.surface.plotMarks100({ negateChroma: true }),
...this.colorScaleSeqA.trajectory.plotMarks100({
negateChroma: true,
nStepSpline,
strokeSpline,
rControlPoints
}),
...this.colorScaleSeqB.surface.plotMarks100({ negateChroma: false }),
...this.colorScaleSeqB.trajectory.plotMarks100({
negateChroma: false,
nStepSpline,
strokeSpline,
rControlPoints
}),
...this.plotMarks100Points({
nStepPoints,
strokePoints,
rPoints
})
];
};
}
Insert cell
surfOrange = ColorSurface.fromHue(66, { lum100: 65, dHue100: 0.9 })
Insert cell
scaleSeqOrange = new ColorScaleSequential(surfOrange, traj)
Insert cell
scaleDiv = new ColorScaleDiverging(scaleSeqOrange, scaleSeqBlue, { scale: 0.8 })
Insert cell
scaleDiv.data100Points(10)
Insert cell
scaleDiv.colorScaleSeqA.sRgbInterp(0.5)
Insert cell
Plot.plot({
width: 400,
aspectRatio: 1,
grid: true,
marks: scaleDiv.plotMarks100()
})
Insert cell
swatches(
d3
.range(0, 1, 1 / 10)
.concat(1)
.map(scaleDiv.hexInterp)
)
Insert cell
JSON.stringify(
d3
.range(0, 1, 1 / 10)
.concat(1)
.map(scaleDiv.hexInterp)
)
Insert cell
Insert cell
// source: http://www.rit-mcsl.org/MunsellRenotation/real.dat
// - cannot use Observable to download from http (vs https)
munsellRenotation_text = FileAttachment("munsell-renotation.txt").text()
Insert cell
Insert cell
munsellRenotation_csv = munsellRenotation_text
.split("\n")
.map((x) => x.trim().replaceAll(/\s+/g, ","))
.join("\n")
Insert cell
munsellRenotation_data = d3.csvParse(munsellRenotation_csv)
Insert cell
// keep this until colorUtils are complete
munsellRenotation_strings = munsellRenotation_data.map(munsellObjectToString)
Insert cell
// Object -> String
munsellObjectToString = function (x) {
return `${x.h} ${x.V}/${x.C}`;
}
Insert cell
// String -> Object
munsellStringToObject = function (x) {
const array = x.split(/[\s\/]/);
return {
h: array[0],
V: +array[1],
C: array[2] === undefined ? 0 : +array[2]
};
}
Insert cell
munsellStringToObject("7.5R 1/8")
Insert cell
munsellObjectToString({ h: "7.5R", V: "1", C: "8" })
Insert cell
Insert cell
munsellSRgb = munsellRenotation_strings.map((x) => munsell.munsellToRgb(x))
Insert cell
Insert cell
munsellGreySRgb = [...Array(11).keys()]
.map((x) => `N ${x}`)
.map((x) => munsell.munsellToRgb(x))
.map(clampRgbGamut)
Insert cell
Insert cell
munsellGreyStrings = [...Array(11).keys()].map((x) => `N ${x}`)
Insert cell
munsellRenotation = [...munsellGreyStrings, ...munsellRenotation_strings]
.map((x) => {
return {
munsell: { ...munsellStringToObject(x), string: x },
sRgb: _.flow([munsell.munsellToRgb])(x)
};
})
.map((x) => (x.munsell.C == 0 ? { ...x, sRgb: clampRgbGamut(x.sRgb) } : x))
Insert cell
munsellV5C8SRgb = munsellRenotation
.filter((x) => x.munsell.V == 5 && x.munsell.C == 8)
.map((x) => x.sRgb)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
munsell = import("https://cdn.skypack.dev/munsell@1.0.11?min")
Insert cell
culori = import("https://unpkg.com/culori@3.1.3/bundled/culori.mjs?module")
Insert cell
math = import("https://cdn.skypack.dev/mathjs@11.8.2?")
Insert cell
import { NaturalCubicSpline } from "@jrus/cubic-spline"
Insert cell
import { changeTable } from "@ijlyttle/change-log"
Insert cell
Insert cell
luv = culori.converter("luv")
Insert cell
luv({ mode: "lrgb", r: 0.5, g: 0.5, b: 0.5 })
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