Public
Edited
Aug 3, 2023
2 forks
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
custom = [
"#802a1d",
"#a44f1b",
"#c37628",
"#db9e44",
"#ebc777",
"#e3e3e3",
"#77d9f0",
"#37b7e3",
"#2790c9",
"#2b69a8",
"#3b4280"
]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
scheme
Insert cell
Insert cell
Insert cell
csd = colorScaleData(scheme, {
fnCvd: fnCvd,
fnSpace100: fnSpace100,
label: "",
dark: dark
})
Insert cell
colorScaleData = function (
colors,
{
fnCvd = _.identity,
fnSpace100 = _.flow(
colorSpaces.munsell.fromSRgb,
colorSpaces.munsell.to100
),
label = "",
dark = dark
} = {}
) {
const colorsOriginal = colors.map((x, i) => ({
i,
sRgb: asSRgb(x),
space100: _.flow(asSRgb, fnSpace100)(x),
hex: asHex(x)
}));
const colorsCvd = tidy.tidy(
colorsOriginal,
tidy.mutate({
sRgb: (d) => fnCvd(d.sRgb),
space100: (d) => fnSpace100(d.sRgb),
hex: (d) => colorUtils.hexFromSRgb(d.sRgb)
})
);
const distancesCvd = tidy.tidy(
[],
tidy.expand({
a: colorsCvd,
b: colorsCvd
}),
tidy.transmute({
i: (d) => d.a.i,
j: (d) => d.b.i,
distance: (d) => math.distance(d.a.space100, d.b.space100)
}),
tidy.filter((d) => d.i < d.j)
);

const chromaOriginal = tidy.tidy(
colorsOriginal,
tidy.mutate({
polar100: (d) => colorUtils.toPolar(d.space100)
})
);

const maxChroma = math.max(chromaOriginal.map((x) => x.polar100[1]));

return {
colorsOriginal,
colorsCvd,
distancesCvd,
maxChroma,
dark
};
}
Insert cell
Insert cell
colorScalePlot = function ({
colorsOriginal = [],
colorsCvd = [],
distancesCvd = [],
maxChroma = 20,
dark = false
} = {}) {
const margins = {
marginTop: 25,
marginRight: 40,
marginBottom: 30,
marginLeft: 35
};

// TODO: nail-down widths to achieve orthographic projection
const ab = 250;
const widthAB = ab + margins.marginLeft + margins.marginRight;
const widthLum =
(ab * 100) / (2 * maxChroma) + margins.marginLeft + margins.marginRight;

const aB = aBPlot({
colorsOriginal,
colorsCvd,
distancesCvd,
maxChroma,
width: widthAB,
margins,
dark
});

const lumB = lumBPlot({
colorsOriginal,
colorsCvd,
distancesCvd,
maxChroma,
width: widthLum,
margins,
dark
});

const aLum = aLumPlot({
colorsOriginal,
colorsCvd,
distancesCvd,
maxChroma,
width: widthAB,
margins,
dark
});

const distance = distancePlot({
colorsOriginal,
colorsCvd,
distancesCvd,
dark
});

return html`
<div style="display: flex;">
<div>${aB}</div>
<div>${lumB}</div>
</div>
<div style="display: flex;">
<div>${aLum}</div>
<div style="margin-left: 30px;">${distance}</div>
</div>`;
}
Insert cell
aBPlot(csd)
Insert cell
aBPlot = function ({
colorsOriginal = [],
colorsCvd = [],
maxChroma = 20,
width = 300,
margins = {
marginTop: 20,
marginRight: 20,
marginBottom: 30,
marginLeft: 35
},
dark = false
} = {}) {
const hexOriginal = joinCvd(colorsOriginal, colorsCvd);

return Plot.plot({
aspectRatio: 1,
width: width,
...margins,
grid: true,
y: { domain: [-maxChroma, maxChroma], label: "b", ticks: 5 },
x: {
domain: [-maxChroma, maxChroma],
label: "a",
ticks: 5
},
marks: [
Plot.dot(colorsCvd, {
x: (d) => d.space100[1],
y: (d) => d.space100[2],
sort: (d) => d.space100[0],
r: 10,
fill: (d) => d.hex
}),
Plot.dot(
hexOriginal,
Plot.pointer({
x: (d) => d.space100[1],
y: (d) => d.space100[2],
r: 12,
stroke: (d) => d.hexOriginal,
strokeWidth: 2
})
),
Plot.text(
hexOriginal,
Plot.pointer({
x: (d) => d.space100[1],
y: (d) => d.space100[2],
text: (d) => d.hexOriginal,
textAnchor: "start",
dx: 17
})
)
]
});
}
Insert cell
lumBPlot(csd)
Insert cell
lumBPlot = function ({
colorsOriginal = [],
colorsCvd = [],
maxChroma = 20,
width = 500,
margins = {
marginTop: 20,
marginRight: 20,
marginBottom: 30,
marginLeft: 35
},
dark = false
} = {}) {
const hexOriginal = joinCvd(colorsOriginal, colorsCvd);

return Plot.plot({
aspectRatio: 1,
width: width,
...margins,
grid: true,
y: { domain: [-maxChroma, maxChroma], label: "b", ticks: 5 },
x: {
domain: [100, 0],
label: "luminance",
ticks: 11
},
marks: [
Plot.dot(colorsCvd, {
x: (d) => d.space100[0],
y: (d) => d.space100[2],
sort: (d) => d.space100[1],
r: 10,
fill: (d) => d.hex
}),
Plot.dot(
hexOriginal,
Plot.pointer({
x: (d) => d.space100[0],
y: (d) => d.space100[2],
r: 12,
stroke: (d) => d.hexOriginal,
strokeWidth: 2
})
),
Plot.text(
hexOriginal,
Plot.pointer({
x: (d) => d.space100[0],
y: (d) => d.space100[2],
text: (d) => d.hexOriginal,
textAnchor: "start",
dx: 17
})
)
]
});
}
Insert cell
aLumPlot(csd)
Insert cell
aLumPlot = function ({
colorsOriginal = [],
colorsCvd = [],
maxChroma = 20,
width = 200,
margins = {
marginTop: 20,
marginRight: 20,
marginBottom: 30,
marginLeft: 35
},
dark = false
} = {}) {
const hexOriginal = joinCvd(colorsOriginal, colorsCvd);

return Plot.plot({
aspectRatio: 1,
width: width,
...margins,
grid: true,
x: { domain: [-maxChroma, maxChroma], label: "a", ticks: 5 },
y: {
domain: [100, 0],
reverse: true,
label: "luminance",
ticks: 11
},
marks: [
Plot.dot(colorsCvd, {
x: (d) => d.space100[1],
y: (d) => d.space100[0],
sort: (d) => d.space100[2],
r: 10,
fill: (d) => d.hex
}),
Plot.dot(
hexOriginal,
Plot.pointer({
x: (d) => d.space100[1],
y: (d) => d.space100[0],
r: 12,
stroke: (d) => d.hexOriginal,
strokeWidth: 2
})
),
Plot.text(
hexOriginal,
Plot.pointer({
x: (d) => d.space100[1],
y: (d) => d.space100[0],
text: (d) => d.hexOriginal,
textAnchor: "start",
dx: 17
})
)
]
});
}
Insert cell
distancePlot(csd)
Insert cell
distancePlot = function ({
colorsOriginal = [],
colorsCvd = [],
distancesCvd = [],
dark = false
} = {}) {
const nColor = colorsOriginal.length;

const marginLeft = 0;
const marginRight = 50;
const cell = { width: 30, padding: 0.05 };
const width =
nColor * cell.width +
cell.padding * cell.width * (nColor + 1) +
marginLeft +
marginRight;

// correct aspectRatio for margin
const aspect = width / (width - 50);

return Plot.plot({
caption: "Euclidean distance through color space",
marginLeft: marginLeft,
marginRight: marginRight,
width: width,
padding: cell.padding,
aspectRatio: aspect,
x: { axis: false },
y: { axis: false, reverse: true },
color: {
scheme: "greys",
domain: [1, 100],
reverse: !dark,
type: "log"
},
marks: [
Plot.cell(distancesCvd, {
x: "i",
y: "j",
fill: "distance",
rx: cell.width / 10,
ry: cell.width / 10
}),
Plot.text(
distancesCvd,
colorContrast({
x: "i",
y: "j",
text: (d) => d3.format(".1f")(d.distance),
fill: "distance"
})
),
Plot.dot(colorsCvd, {
x: "i",
y: "i",
fill: "hex",
r: 10
}),
Plot.dot(
colorsOriginal,
Plot.pointer({
x: "i",
y: "i",
r: 12,
stroke: (d) => d.hex,
strokeWidth: 2
})
),
Plot.text(
colorsOriginal,
Plot.pointer({
x: "i",
y: "i",
text: (d) => d.hex,
textAnchor: "start",
dx: 17
})
)
]
});
}
Insert cell
Insert cell
joinCvd = function (orig, cvd) {
return tidy.tidy(
orig,
tidy.transmute({
i: (d) => d.i,
hexOriginal: (d) => d.hex
}),
tidy.leftJoin(cvd, { by: ["i"] })
);
}
Insert cell
asSRgb = function(x) {
// caution - very simple, not a lot of validation
if (Array.isArray(x)) {
return x
}

if (typeof x === "string") {
return colorUtils.hexToSRgb(x)
}

throw "asSRgb() input unknown"
}
Insert cell
asHex = function (x) {
// caution - very simple, not a lot of validation
if (typeof x === "string") {
return x;
}

if (Array.isArray(x)) {
return colorUtils.hexFromSRgb(x);
}

throw "asHex() input unknown";
}
Insert cell
margins = ({ marginTop: 20, marginRight: 20, marginBottom: 30, marginLeft: 35 })
Insert cell
Insert cell
swatchNew = function (colors, { caption = "" } = {}) {
const data = colors.map((x, i) => ({ i, hex: x }));

return Plot.plot({
width: data.length * 35,
aspectRatio: 1,
caption: caption,
padding: 0,
margin: 0,
marginBottom: 5,
x: { axis: false, grid: false },
y: { axis: false, grid: false },
marks: [Plot.cell(data, { x: "i", y: 0, fill: "hex", r: 20 })]
});
}
Insert cell
swatchNew(["#663399", "#808080"], { caption: "A caption" })
Insert cell
Insert cell
chromaMax = tidy.tidy(
[],
tidy.expand({
space: Array.from(colorUtils.mapColorSpaces().values()),
hex: ["#0000FF", "#00FF00", "#00FFFF", "#FF0000", "#FF00FF", "#FFFF00"],
condition: ["none", "protan", "deuteran", "tritan"]
}),
tidy.mutate({
sRgb: (d) => colorUtils.hexToSRgb(d.hex),
sRgbCvd: (d) =>
_.flow([
colorUtils.linearRgbFromSRgb,
(x) => math.multiply(matrixCvd(d.condition, 1), x),
colorUtils.clampRgbGamut,
colorUtils.linearRgbToSRgb
])(d.sRgb),
space100Cvd: (d) =>
_.flow([colorSpaces[d.space].fromSRgb, colorSpaces[d.space].to100])(
d.sRgbCvd
),
chroma: (d) => math.distance([0, 0], d.space100Cvd.slice(1))
}),
tidy.groupBy("space", [
tidy.summarize({
chroma: tidy.max("chroma")
})
])
)
Insert cell
Insert cell
viewof transSpace = Inputs.input()
Insert cell
transitionFilter(
transitionGenerator(viewof space, {
states: Array.from(colorUtils.mapColorSpaces().values())
}),
viewof transSpace
)
Insert cell
transSpace
Insert cell
Insert cell
Insert cell
transitionFilter(
transitionGenerator(viewof cvd.children[1], {
states: ["none", "protan", "deuteran", "tritan"]
}),
viewof transCvd
)
Insert cell
matCvd = transitionInterpolate(transCvd, (state) =>
matrixCvd(state, cvd.severity)
)
Insert cell
fnCvd = (x) => {
const matCvd = transitionInterpolate(transCvd, (state) =>
matrixCvd(state, cvd.severity)
);
return _.flow([
colorUtils.linearRgbFromSRgb,
(x) => math.multiply(matCvd, x),
colorUtils.clampRgbGamut,
colorUtils.linearRgbToSRgb
])(x);
}
Insert cell
fnSpace100 = (x) =>
transitionInterpolate(transSpace, (space) =>
_.flow([colorSpaces[space].fromSRgb, colorSpaces[space].to100])(x)
)
Insert cell
Insert cell
Insert cell
Insert cell
math = import("https://cdn.skypack.dev/mathjs@11.8.2?")
Insert cell
tidy = import("https://unpkg.com/@tidyjs/tidy@2.5.2/dist/es/index.js?module")
Insert cell
import { colorContrast } from "@observablehq/plot-colorcontrast-custom-transform"
Insert cell
import {
transitionGenerator,
transitionFilter,
transitionInterpolate,
Easing
} from "@ijlyttle/transition-helper"
Insert cell
import { colorSpaces, colorUtils, ColorSpace } from "@ijlyttle/color-utilities"
Insert cell
import { changeTable } from "@ijlyttle/change-log"
Insert cell
import { inputCvd, matrixCvd } from "@ijlyttle/cvd-widget"
Insert cell
import { apcaTextColor } from "@ijlyttle/apca-w3-color-contrast"
Insert cell
import { inputMode, invokeMode, styleDark } from "@ijlyttle/dark-mode-input"
Insert cell
invokeMode(dark)
Insert cell
styleDark()
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