Public
Edited
Nov 29
Paused
1 fork
Importers
69 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
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
Zdog = require("zdog@1/dist/zdog.dist.min.js")
Insert cell
culori = import("https://unpkg.com/culori@2.0.0/bundled/culori.min.mjs?module")
Insert cell
Insert cell
Insert cell
Insert cell
middle = (arr) => arr[Math.round((arr.length - 1) / 2)]
Insert cell
<style>
dfn {
font-weight: bold;
}
</style>
Insert cell
Insert cell
startColor = gradientForm.startColor
Insert cell
endColor = gradientForm.endColor
Insert cell
rgbInOklch = gradientForm.rgbInOklch
Insert cell
oklabConverter = culori.converter("oklab")
Insert cell
rgbConverter = culori.converter("rgb")
Insert cell
oklchConverter = culori.converter("oklch")
Insert cell
rgbInterpolator = culori.interpolate([startColor, endColor], "rgb")
Insert cell
oklabInterpolator = culori.interpolate([startColor, endColor], "oklab")
Insert cell
function* renderSpace({
points,
path,
syncRotation = false,
axis = false,
width = 300,
height = 300
}) {
let isSpinning = true;
let element = DOM.canvas(width, height);
let illo = new Zdog.Illustration({
element,
dragRotate: true,
rotate: {
x: -0.3 * Math.PI,
y: -0.1 * Math.PI
},
scale: {
y: 400,
x: 400,
z: 400
}
});

if (axis) {
const x = _.minBy(points, (p) => p.x).x;
const maxX = _.maxBy(points, (p) => p.x).x;
// flipped
const y = _.maxBy(points, (p) => p.y).y;
const maxY = _.minBy(points, (p) => p.y).y;
const z = _.minBy(points, (p) => p.z).z;
const maxZ = _.maxBy(points, (p) => p.z).z;

let rect = new Zdog.Rect({
addTo: illo,
width: maxX - x,
height: maxY - y,
translate: { z: (maxX - x) / 2 },
stroke: 2
});

rect.copy({
translate: { z: (maxX - x) / -2 }
});

rect.copy({
translate: { x: (maxX - x) / -2 },
rotate: { y: Zdog.TAU / 4 }
});

rect.copy({
translate: { x: (maxX - x) / 2 },
rotate: { y: Zdog.TAU / 4 }
});
}

let dragStartRX, dragStartRY;

if (syncRotation) {
// https://zzz.dog/api#dragger
new Zdog.Dragger({
startElement: illo.element,
onDragStart: function () {
// keep track of rotation
dragStartRX = viewRotation.x;
dragStartRY = viewRotation.y;
},
onDragMove: function (pointer, moveX, moveY) {
// move rotation
let moveRX = (moveY / illo.width) * Zdog.TAU * -1;
let moveRY = (moveX / illo.width) * Zdog.TAU * -1;
viewRotation.x = dragStartRX + moveRX;
viewRotation.y = dragStartRY + moveRY;
}
});
}

for (const { x, y, z, cull, hex } of points) {
if (cull) {
continue;
}

new Zdog.Shape({
addTo: illo,
translate: { x, y, z },
stroke: 15,
color: hex
});
}

const pathGroup = new Zdog.Group({
addTo: illo
});

// drag z-index of group underneath the cube: https://zzz.dog/extras#z-fighting
new Zdog.Shape({
addTo: pathGroup,
translate: { y: -50 },
visibility: false
});

if (path) {
new Zdog.Shape({
addTo: pathGroup,
path,
stroke: 10
});
}

while (true) {
if (syncRotation) {
illo.rotate.set(viewRotation);
} else {
illo.rotate.y += 0.01;
}

illo.updateRenderGraph();
yield element;
}
}
Insert cell
// hardcoded for nice default perspective so that Oklab space and RGB cube look "nice"
viewRotation = new Zdog.Vector({
x: 5.751272355710947,
y: 0.4305139451754206,
z: 0
})
Insert cell
function oklch2xyz(lch) {
const oklab = oklabConverter({
...lch,
h:
lch.h + 310 /* hardcoded to make blue-yellow interpolation line up with RGB */
});

const y = -(oklab.l - culori.modeOklab.ranges.l[1] / 2) / 2;
const x = oklab.a;
const z = -oklab.b;

return { x, y, z };
}
Insert cell
function rgb2xyz({ r, g, b }) {
// scaling factor
const s = 0.3;
const x = (r - 0.5) * s;
const y = (-g + 0.5) * s;
const z = (b - 0.5) * s;

return { x, y, z };
}
Insert cell
function steps(start, finish, number) {
return [..._.range(start, finish, (finish - start) / number), finish];
}
Insert cell
rgbCube = {
const red = steps(0, 1, 11);
const green = steps(0, 1, 11);
const blue = steps(0, 1, 11);

return red.flatMap((r) =>
green.flatMap((g) =>
blue.map((b) => {
const rgb = { mode: "rgb", r, g, b };
const { x, y, z } = rgb2xyz(rgb);
const hex = culori.formatHex(rgb);

return { x, y, z, hex };
})
)
);
}
Insert cell
oklchCylinder = {
// max values https://culorijs.org/color-spaces/
const oklch = culori.modeOklch;
// find the highest and lowest values for each param
const { l, c, h } = oklch.ranges;
const lightness = steps(l[0], l[1], 11);
const chroma = steps(c[0], c[1], 11);
const hue = _.range(h[0], h[1], h[1] / 32);

return lightness.flatMap((l) =>
chroma.flatMap((c) =>
hue.map((h) => {
const lch = { mode: oklch.mode, l, c, h };
const cull = !culori.displayable(lch);
const hex = culori.formatHex(culori.clampRgb(oklch.toMode.rgb(lch)));
const { x, y, z } = oklch2xyz(lch);
return { x, y, z, hex, cull };
})
)
);
}
Insert cell
oklabInterpolation = steps(0, 1, 10)
.map(oklabInterpolator)
.map(oklchConverter)
.map((oklch, i) => {
const { x, y, z } = oklch2xyz(oklch);
const hex = culori.formatHex(oklch);

return { x, y, z, hex, i, type: "Oklab" };
})
Insert cell
rgbInOklchInterpolation = steps(0, 1, 10)
.map(rgbInterpolator)
.map(oklchConverter)
.map((oklch) => {
const { x, y, z } = oklch2xyz(oklch);
const hex = culori.formatHex(oklch);

return { x, y, z, hex };
})
Insert cell
rgbInterpolation = steps(0, 1, 10)
.map(rgbInterpolator)
.map((rgb, i) => {
const { x, y, z } = rgb2xyz(rgb);
const hex = culori.formatHex(rgb);

return { x, y, z, hex, i, type: "sRGB" };
})
Insert cell
oklabGradient = d3
.range(0, 1, 0.001)
.map(oklabInterpolator)
.map((oklab, i) => ({
t: i * 0.001,
lightness: oklab.l,
hex: culori.formatHex(oklab)
}))
Insert cell
rgbGradient = d3
.range(0, 1, 0.001)
.map(rgbInterpolator)
.map((rgb, i) => ({
t: i * 0.001,
lightness: oklabConverter(rgb).l,
hex: culori.formatHex(rgb)
}))
Insert cell
Insert cell
schemes = [
// match ordering in https://observablehq.com/@mjbo/color-schemes-under-color-vision-deficiency#plotSchemesAndDeficiencies
["Viridis (sequential, multi-hue)", "viridis"],
["Cividis (sequential, multi-hue)", "cividis"],
["Magma (sequential, multi-hue)", "magma"],
["Inferno (sequential, multi-hue)", "inferno"],
["Plasma (sequential, multi-hue)", "plasma"],
["Cubehelix (sequential, multi-hue)", "cubehelix"],
["Warm (sequential, multi-hue)", "warm"],
["Cool (sequential, multi-hue)", "cool"],
["YlGnBu (sequential, multi-hue)", "ylgnbu"],
["YlGn (sequential, multi-hue)", "ylgn"],
["YlOrBr (sequential, multi-hue)", "ylorbr"],
["YlOrRd (sequential, multi-hue)", "ylorrd"],
["Blues (sequential, single-hue)", "blues"],
["Greens (sequential, single-hue)", "greens"],
["Greys (sequential, single-hue)", "greys"],
["Purples (sequential, single-hue)", "purples"],
["Reds (sequential, single-hue)", "reds"],
["Oranges (sequential, single-hue)", "oranges"],
["BuGn (sequential, multi-hue)", "bugn"],
["BuPu (sequential, multi-hue)", "bupu"],
["GnBu (sequential, multi-hue)", "gnbu"],
["OrRd (sequential, multi-hue)", "orrd"],
["PuBuGn (sequential, multi-hue)", "pubugn"],
["PuBu (sequential, multi-hue)", "pubu"],
["PuRd (sequential, multi-hue)", "purd"],
["RdPu (sequential, multi-hue)", "rdpu"],
["Sinebow (cyclical)", "sinebow"],
["Rainbow (cyclical)", "rainbow"],
["Turbo (sequential, multi-hue)", "turbo"],
["Spectral (diverging)", "spectral"],
["BrBG (diverging)", "brbg"],
["PRGn (diverging)", "prgn"],
["PiYG (diverging)", "piyg"],
["PuOr (diverging)", "puor"],
["RdBu (diverging)", "rdbu"],
["RdGy (diverging)", "rdgy"],
["RdYlBu (diverging)", "rdylbu"],
["RdYlGn (diverging)", "rdylgn"],
["BuRd (diverging)", "burd"],
["BuYlRd (diverging)", "buylrd"]
]
Insert cell
// https://github.com/observablehq/plot#color-options
function materialiseScale(scheme) {
return Plot.scale({
color: {
type: "linear",
scheme: scheme
}
});
}
Insert cell
plotSwatches = (scheme) =>
Plot.plot({
color: {
scheme: scheme
},
x: {
axis: null
},
height: 30,
marks: [Plot.cell(d3.range(-10, 11), { x: (d) => d, fill: (d) => d })],
caption: scheme
})
Insert cell
plotLightness = (scheme) => {
return Plot.plot({
nice: true,
color: { type: "identity" },
aspectRatio: 1,
facet: {
data: allSchemeData,
x: "filter"
},
y: {
label: "↑ L"
},
fx: {
domain: ["none", "greyscale"]
},
r: {
domain: [5, 5]
},
marks: [
Plot.frame(),
Plot.dot(allSchemeData, {
filter: (point) => point.schemeq === scheme,
x: "t",
y: "lightness",
fill: "color",
r: 6
})
]
});
}
Insert cell
deficiencyDeuter = culori.filterDeficiencyDeuter()
Insert cell
allSchemeData = schemes.flatMap(([longName, shortName]) => {
const interpolate = materialiseScale(shortName).interpolate;
return d3.range(0, 1, 0.01).flatMap((t) => [
{
filter: "none",
t,
scheme: longName,
schemeq: shortName,
lightness: oklabConverter(interpolate(t)).l,
oklab: oklabConverter(interpolate(t)),
color: interpolate(t),
hex: culori.formatHex(interpolate(t))
},
{
filter: "greyscale",
t,
scheme: longName,
schemeq: shortName,
color: culori.formatRgb(culori.filterGrayscale()(interpolate(t))),
lightness: oklabConverter(culori.filterGrayscale()(interpolate(t))).l
}
]);
})
Insert cell
cubehelixInterpolator = materialiseScale("cubehelix").interpolate
Insert cell
cubehelixInterpolation = d3
.range(0, 1, 0.01)
.map(cubehelixInterpolator)
.map(culori.parse)
.map((rgb) => {
const { x, y, z } = rgb2xyz(rgb);
const hex = culori.formatHex(rgb);

return { x, y, z, hex };
})
Insert cell
allSchemeData
Insert cell
{
const n = 5; // number of facet columns
const keys = Array.from(d3.union(allSchemeData.map((d) => d.scheme)));
const index = new Map(keys.map((key, i) => [key, i]));
const fx = (key) => index.get(key) % n;
const fy = (key) => Math.floor(index.get(key) / n);

return Plot.plot({
width,
color: { type: "identity" },
nice: true,
aspectRatio: 1.5,
y: {
label: "↑ L"
},
marks: [
Plot.frame(),
Plot.dot(allSchemeData, {
x: "t",
filter: (d) => d.filter === "none",
y: "lightness",
fill: "color",
r: 5,
fx: (d) => fx(d.scheme),
fy: (d) => fy(d.scheme)
}),
Plot.text(keys, { fx, fy, frameAnchor: "bottom-left", dx: 6, dy: -6 })
]
});
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more