Public
Edited
May 29
Paused
1 fork
Importers
72 stars
SQLite Geospatial - intersections!London Isodemographic CartogramGolden Tonal Palette AnalysisHexbin Transform / Observable Plot
NBA Finals, Game 4
Disk Sampling on a SphereNBA Finals, Game 1Build your first scatterplot with Observable PlotDisplaying IBM Carbon charts in ObservableModelo de Cúpula CatenáriaBivariate Bubble MapAnalyzing Star Wars movies + the first anniversary of Observable Plot!Single-family homes owned by large corporate landlords in North CarolinaThe MoMA Collection DataMapping the Vietnamese DiasporaElection map as striped circles #2They heard the callDiagrammatic EmbeddingsHegel's Complete System of PhilosophyGeneration Arc DiagramVulnerability of Mountain PeoplesVisualizing Air Raid Sirens in UkraineUkrainian refugees welcomeConcentric Circles in D3Daylight Saving Time Gripe Assistant Tool
Perceptually uniform color models and their implications
Top Notebooks of February 2022Zillow Home PricesBlack History Month ActivityExamples from: Three Simple Flexible tools for Empowered Data VisualizationFull Covid Vaccination Rate in CA by County Over TimePlot CheatsheetsHunga Tonga–Hunga Ha'apai Global Pressure WaveVisualizing The New York Times’s Mini Crossword100 Beautiful and Informative Notebooks of 2021When Presidents Fade AwayThe Most Frequently Used Emoji of 2021Upset Plots with Observable PlotLucy mission animationSpreadsheet-based data collaboration gets supercharged with Observable30 Day Map Challenge: Day 4 - HexagonsDisputed territoriesMapping the Cities of US Highway SignsThirty day Map Challenge: Day 1 - PointsCube Pushing Loop 💪 🧊 🔄Guided Tour of an Infinite Sphere GridCanvas, P5.js and circle packing (collision and cluster force) on a mapWomen's History Month DataViz ContestWomen in DataViz EventSVG flowersMultiple Set Venn with Color BlendingA Stupid Emoji Utility FunctionQuilting with d3-curveCurvilinear RosettesStrange AttractorsSolo board gameDouble pendilumVertical SlidersDrawing and animating blobs with svg and javascriptOrbiting Particles III#genuary 2021 ~ Do some golf!Imitation Game 🖐Happy New Year (2019) (And other celebrations)Earth Temperature SpiralStar MapCorrelation FilteringBattle of ColorsPapercraft Christmas Ornaments GeneratorChristmas Tree Perspective 🎄bouncing circles2D (Non-physical) N-body Gravity with Poisson's EquationWaterfall Fill2020 TimelineTime Spiral with a COVID DemoTransition between Three.js camerasJulio Le Parc Replications and VariationsFlexible HeatmapSquare PackingSierpiński curve animationTrainsWhy use a radial data visualization?Earthquakes from the past 30 daysThe US COVID SyringeHello OGL - Minimal WebGL libraryVersor MultiplicationLabyrinthTruncated-octahedron-based 3D-space-filling curveNUTS regions (The Nomenclature of territorial units for statistics): PerspectiveStretchy FishTP3: Power Diagram and Semi-Discrete Optimal TransportEmoji ParticlesFirma de color de BogotáCovid19 WorldwideWeb 3.0 Explorable # 3: NFTs -- Game Items With Real World ValueUnemployment ExplorationSierpinski curveEight QueensVoronoi StipplingSpinning out of controlFragment shader raytracerSunny day, rainy day in SeattleAs votes are countedOrbit of the dayVoronoi ClothTry to impeach this? Challenge accepted!Dispersion in Water Surface WavesSelf-Organizing Maps meet DelaunayGenerative ArtReturn to a squareCreating a Canvas Noise Map for Generative ArtMARTINI: Real-Time RTIN Terrain MeshMunsell spinGenerative artAs votes come in, what would it take for the trailing candidate to win?3a. Historical participation in early voting vs. Election Day votingWaffle ChartSun settingRaymarch HelperF1 Constructor Standings 2010-2019Electoral College Decision TreeThe Woman Citizen's Wheel of Progress, 1917Equisurface bull's eye
Also listed in…
Talks
Color
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

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