Public
Edited
Feb 1, 2023
6 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
// Adobe Color Wheel (originally “Adobe Kuler”)
// see: https://color.adobe.com
// code: https://github.com/smallpath/adobe-color-picker/blob/master/colorPicker.js
function mapAdobeWheel(i) {
const a = { R:0, O:30, Y:60, G:120, C:180, B:240, M:300, R2:360 } // hsl degrees
const b = { R:0, O:45, Y:120, G:180, C:220, B:275, M:320, R2:360 } // adobe degrees

const n = 360
const h = (i%1)*n
if (h<a.O) return map(h, [a.R,a.O], [b.R,b.O])/n
if (h<a.Y) return map(h, [a.O,a.Y], [b.O,b.Y])/n
if (h<a.G) return map(h, [a.Y,a.G], [b.Y,b.G])/n
if (h<a.C) return map(h, [a.G,a.C], [b.G,b.C])/n
if (h<a.B) return map(h, [a.C,a.B], [b.C,b.B])/n
if (h<a.M) return map(h, [a.B,a.M], [b.B,b.M])/n
if (h<a.R2) return map(h, [a.M,a.R2], [b.M,b.R2])/n
}
Insert cell
Insert cell
Insert cell
hueCanvas(Oklch, "OkLCh 2021")
Insert cell
// Oklch
// We use the `ac-colors` library to perform this transformation from sRGB -> XYZ -> LAB
// (extra effort to put red at 0)
Oklch = i => ((oklch(hue_culori(i)).h - oklch(hue_culori(0)).h)/360 + 1) % 1
Insert cell
Insert cell
Insert cell
// CIELAB
// We use the `ac-colors` library to perform this transformation from sRGB -> XYZ -> LAB
// (extra effort to put red at 0)
mapHCLab = i => ((hue(i).lchab[2] - hue(0).lchab[2])/360 + 1) % 1
Insert cell
Insert cell
Insert cell
// CIELUV
// We use the `ac-colors` library to perform this transformation from sRGB -> XYZ -> LUV
// (extra effort to put red at 0)
mapHCLuv = i => ((hue(i).lchuv[2] - hue(0).lchuv[2])/360 + 1) % 1
Insert cell
Insert cell
Insert cell
// HCL as designed by Sarifuddin and Missaou in 2005.
// Paper: http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip
// (formula corrected below, since the one in the paper has an error)
mapHCLsm = i => {
const {atan2,PI} = Math
const [r,g,b] = hue(i).rgb
const h = atan2((g-b),(r-g))
return h>=0 ?
map(h, [0,PI], [0,0.5]) :
map(h, [-PI,0], [0.5,1])
}
Insert cell
Insert cell
Insert cell
Insert cell
// sRGB hue angle -> wavelength
// Uses a piecewise linear approximation.
// (derived from: http://www.physics.sfasu.edu/astro/color/spectra.html)
function mapWavelength(i) {
const a = { R:0, Y:60, G:120, C:180, B:240, M:300 } // hsl degrees
const b = { R:645, Y:580, G:510, C:490, B:440, M:380 } // wavelengths
// NOTE: magenta is only 30% bright on spectrum^
// (according to the referenced model)

const h = (i%1)*360
let w
if (h<a.Y) w=map(h, [a.R,a.Y], [b.R,b.Y])
else if (h<a.G) w=map(h, [a.Y,a.G], [b.Y,b.G])
else if (h<a.C) w=map(h, [a.G,a.C], [b.G,b.C])
else if (h<a.B) w=map(h, [a.C,a.B], [b.C,b.B])
else if (h<=a.M) w=map(h, [a.B,a.M], [b.B,b.M])

return map(w, [b.R,b.M], [hueI.R,hueI.M])
}
Insert cell
Insert cell
// hue index (fraction from 0 to 1)
hueI = ({
R: 0/6, // red
O: 1/12, // orange
Y: 1/6, // yellow
G: 2/6, // green
B: 3/6, // blue (“cyan” is historically blue, like the sky)
I: 4/6, // indigo
V: 9/12, // violet
M: 5/6, // magenta
R2: 6/6, // red loopback
})
Insert cell
// use for evenly distributing the rainbow by the Roy G Biv names
function mapRoygbiv(t) { // 0 ≤ t ≤ 1
const hues = Object.values(hueI)
const n = hues.length
t = t%1
for (let i=0; i<n; i++) {
if (t < hues[i+1]) {
return map(t, [hues[i], hues[i+1]], [i,i+1])/(n-1)
}
}
}
Insert cell
function hueToRgb(i) {
const a = i*6%6
const t = a%1
const s = 1-t
if (a < 1) return [1, t, 0] // r->y
if (a < 2) return [s, 1, 0] // y->g
if (a < 3) return [0, 1, t] // g->c
if (a < 4) return [0, s, 1] // c->b
if (a < 5) return [t, 0, 1] // b->m
if (a < 6) return [1, 0, s] // m->r
}
Insert cell
// extreme point on sRGB with the given hue (for ac-colors library)
// 0 <= i <= 1
hue = i => new Color({type:'rgb', color:hueToRgb(i).map(c => 255*c)})
Insert cell
// extreme point on sRGB with the given hue (for culori library)
// 0 <= i <= 1
hue_culori = i => ({mode:'hsv', h:i*360, s:1, v:1})
Insert cell
Insert cell
Color = require('ac-colors@1.4.2/dist/ac-colors.min.js') // needed for CIE LUV
Insert cell
chroma = require('chroma-js') // needed for clamped CIE HCLab
Insert cell
// Re-maps a number from one range to another:
// https://processing.org/reference/map_.html
function map(n, [start1, stop1], [start2, stop2]) {
return (n - start1) / (stop1 - start1) * (stop2 - start2) + start2
}
Insert cell
culori = require('culori') // needed for OkLCh
Insert cell
oklch = culori.converter('oklch')
Insert cell
function hueCanvas(f,label,max=1) {
const w = width
const tickH = 6
const gradH = 25
const unigradH = 6
const labelH = 20
const h = gradH + tickH + (label ? labelH : 0) + unigradH
const ctx = DOM.context2d(w,h)

const y = h - gradH - unigradH

ctx.font = '16px sans-serif'
if (label) {
ctx.fillStyle = '#3558'
ctx.textAlign = 'left'
ctx.textBaseline = 'top'
ctx.fillText(label, 10, 0)
}

ctx.shadowColor = '#000'
ctx.shadowBlur = 1.4

// get ticks
const tickRes = 4
const tickN = 6 * 2**tickRes
const tickStops = []
for (let j=0; j<tickN; j++) {
const i = j/tickN
if (i > max) break
tickStops.push([i,f(i), hue(i).hex])
}
if (max == 1) tickStops.push([1,1, hue(0).hex])

// draw ticks
if (!cleanMode) {
for (const [i,j,color] of tickStops.slice(1,-1)) {
ctx.beginPath()
const x = j*w
ctx.moveTo(x,y-tickH)
ctx.lineTo(x,y)
ctx.strokeStyle = color; ctx.stroke()
}
}

// draw gradients drawn between each tick
const g = ctx.createLinearGradient(0,0,w,0)
for (const [i,j,color] of tickStops) {
g.addColorStop(j,color)
}
ctx.fillStyle = g
ctx.fillRect(0,y,w*max,gradH)
const g2 = ctx.createLinearGradient(0,0,w,0)
for (const [i,j] of tickStops) {
const h = hue(i).lchab[2]
g2.addColorStop(j, chroma.hcl(h,80,80).hex())
}
ctx.fillStyle = g2
ctx.fillRect(0,y+gradH,w*max,unigradH)

// draw labels
if (!cleanMode) {
ctx.textBaseline = 'middle'
for (const [label,i] of Object.entries(hueI)) {
if (max < 1 && label=='R2') break
const j = i==max ? max : f(i)
const col = hue(i).hex
ctx.beginPath()
const tx = j*w
const ty = y+gradH/2
const text = ' '+label[0]+' '
ctx.textAlign = i==0 ? 'left' : i==max ? 'right' : 'center'
ctx.lineWidth = 3
ctx.strokeStyle = '#0004'; ctx.strokeText(text,tx,ty)
ctx.fillStyle = '#fff'; ctx.fillText(text,tx,ty)
}
}
return ctx.canvas
}
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