Public
Edited
Aug 3, 2023
1 fork
3 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
colors = bookmark.read().colors ?? [
"#4b94d8",
"#f0a70a",
"#65d0d7",
"#518c31",
"#fbd82d",
"#d17b95"
]
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 { makePicker, makeChanger } from "@ijlyttle/color-picker-bookmark"
Insert cell
import { view } from "@tomlarkworthy/view"
Insert cell
import { bookmark } from "@severo/bookmark"
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