Public
Edited
Apr 23
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
// Lower the third argument for greater precision
// WARNING: You will crash your browser if you use anything less than 0.001
cmyk = rgbToCmyk(rgb, maxChannels, 0.01, difference[1])
Insert cell
rgb = ({ r, g, b })
Insert cell
Insert cell
Insert cell
function rgbToCmyk({ r, g, b }, maxChannels = 2, step = 0.01, difference = d3.differenceCiede2000) {
if (maxChannels < 1 || maxChannels > 4 || !Number.isInteger(maxChannels)) {
throw new Error("maxChannels must be an integer from one through 4");
}

const precision = -Math.log10(step);
if (maxChannels === 4) {
return rgbToFullCmyk({ r, g, b }, precision);
}
const channels = ["c", "m", "y", "k"];

// Normalize RGB
const R = r / 255;
const G = g / 255;
const B = b / 255;

let bestDistance = Infinity;
let bestCMYK = { c: 0, m: 0, y: 0, k: 0 };

// Generate all combinations of `maxChannels` active channels
const combinations = getCombinations(channels, maxChannels);

for (const combo of combinations) {
// Generate all values for this combo
const valueSpace = getValues(combo.length, step);

for (const values of valueSpace) {
// Build a candidate CMYK
const candidate = Object.fromEntries(channels.map(ch => [ch, 0]));
combo.forEach((ch, i) => {
candidate[ch] = values[i];
});

// Evaluate distance according to specified difference function
const dist = difference(
rgbToString(cmykToRgb(candidate)),
rgbToString({r, g, b})
);

if (dist < bestDistance) {
bestDistance = dist;
bestCMYK = { ...candidate };
}
}
}

return Object.fromEntries(
Object.entries(bestCMYK).map(([ch, v]) => [ch, +v.toFixed(precision)])
);
}
Insert cell
function getCombinations(array, k) {
if (k === 0) return [[]];
if (array.length < k) return [];
if (array.length === k) return [array];

const [first, ...rest] = array;
const withFirst = getCombinations(rest, k - 1).map(comb => [first, ...comb]);
const withoutFirst = getCombinations(rest, k);

return withFirst.concat(withoutFirst);
}
Insert cell
function getValues(dimensions, step) {
const range = [];
for (let v = 0; v <= 1 + step / 2; v += step) {
range.push(v);
}

// Cartesian product of [range]^dimensions
const result = [];

function recurse(path = [], depth = 0) {
if (depth === dimensions) {
result.push(path);
return;
}
for (const val of range) {
recurse([...path, val], depth + 1);
}
}

recurse();
return result;
}
Insert cell
function cmykToRgb({ c, m, y, k}){
return {
r: 255 * (1 - c) * (1 - k),
g: 255 * (1 - m) * (1 - k),
b: 255 * (1 - y) * (1 - k)
}
}
Insert cell
function rgbToFullCmyk({ r, g, b }, precision = 2) {
const R = r / 255;
const G = g / 255;
const B = b / 255;

const K = 1 - Math.max(R, G, B);

if (K === 1) {
return { c: 0, m: 0, y: 0, k: 1 };
}

const C = (1 - R - K) / (1 - K);
const M = (1 - G - K) / (1 - K);
const Y = (1 - B - K) / (1 - K);

return {
c: +C.toFixed(precision),
m: +M.toFixed(precision),
y: +Y.toFixed(precision),
k: +K.toFixed(precision),
};
}
Insert cell
function rgbToString({ r, g, b }){
const f = v => Math.round(v);
return `rgb(${f(r)}, ${f(g)}, ${f(b)})`
}
Insert cell
Insert cell
Insert cell
function cmykToString({ c, m, y, k }){
const f = v => Math.round(v * 100);
return `cmyk(${f(c)}%, ${f(m)}%, ${f(y)}%, ${f(k)}%)`
}
Insert cell
swatch = (object, label) => {
const isCmyk = Object.keys(object).includes("c");
const rgb = isCmyk ? cmykToRgb(object) : object;
const color = rgbToString(rgb);
const text = isCmyk ? cmykToString(object) : color;
const wrapper = d3.create("div");

if (label) {
wrapper.append("div")
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("font-weight", "300")
.style("letter-spacing", "0.05em")
.style("text-transform", "uppercase")
.text(label);
}
const field = wrapper.append("div")
.style("background", color)
.style("display", "inline-block")
.style("height", "34px")
.style("width", "300px")
.style("padding-top", "10px");
field.append("div")
.style("color", foreground(color, 7, 0.1))
.style("font-family", "monospace")
.style("text-align", "center")
.text(text);

return wrapper.node();
}
Insert cell
differences = [
["Euclidean RGB", d3.differenceEuclideanRGB],
["Euclidean Lab / CIE76", d3.differenceEuclideanLab],
["Euclidean HCL", d3.differenceEuclideanHcl],
["Euclidean HSL", d3.differenceEuclideanHsl],
["Euclidean Cubehelix", d3.differenceEuclideanCubehelix],
["CIE94", d3.differenceCie94],
["CIEDE2000", d3.differenceCiede2000],
["CMC l:c", d3.differenceCmc],
["DIN99o", d3.differenceDin99o]
]
Insert cell
Insert cell
d3 = require("d3@7", "d3-color-difference@0.1.3")
Insert cell
import { foreground } from "@graphicsnyt/foreground@182"
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