Public
Edited
Jan 20, 2023
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const tokens = {
Blue: "#198ebe",

Background: "#FFFFFF",
Focused: "#FFFF00",

// Focused has luminance boosted by 15%
focused: (_) => _.color.tint(_.color.luminance + 0.15),

// Border is 50% grey, 0.25 opaque
border: (_) => _.color.grey(0.5).alpha(0.25),

// Background is 75% blend to background color
background: (_) => _.color.blend(_("Background"), 0.75)
};
return Swatches(
["Blue", "Blue.focused", "Blue.border", "Blue.background"].map(
(_) => token(_).eval(tokens).color
)
);
}
Insert cell
Insert cell
Insert cell
{
const tokens = {
Red: "#FF0000",

// Ln: n between 0-10 will create a corresponding tint with stable luminance steps
L: (_, factor) => _.color.tint(clamp(prel(factor, -1, 10), 0, 1))
};
return ColorScale(
range(10).map((_) => token(`Red.L${_}`).eval(tokens).value)
);
}
Insert cell
{
const tokens = {
Primary: "#198ebe",
Highlight: "#FFFF00",

// Hn: n between 0-10, creates a luminance and color blend to the highlight color
H: (_, factor) =>
_.color
.blend(_("Highlight"), prel(factor, 0, 10))
.tint(prel(factor, -1, 10))
};
return ColorScale(
range(10).map((_) => token(`Primary.H${_}`).eval(tokens).value)
);
}
Insert cell
Insert cell
Insert cell
{
const tokens = {
// We define a primary colour
Primary: "#198ebe",

// We define a normal background
Background: "#FFFFFF",
Text: "#000000",

// We define a dark mode
Dark: {
Background: "#000000",
Text: "#FFFFFF"
},

// Zn, n in 0-10: Z0 makes the color the same luminance as the background, Z9 as the foreground.
Z: (_, factor) => {
const bg = _("Background");
const fg = _("Text");
const k = prel(factor, 0, 10);
const l = lerp(bg.luminance, fg.luminance, k);
return _.color.tint(l, bg, fg);
}
};
return ColorScale(
["Background", "Dark.Background", "Primary.Z8", "Primary.Dark.Z8"].map(
(_) => token(_).eval(tokens).color
)
);
}
Insert cell
Insert cell
Insert cell
Insert cell
RE_TOKEN = new RegExp(
"(\\.(?<key>[A-Za-z_]+)(?<factor>\\d+)|(?<value>\\d+(\\.\\d+)?)(?<unit>[A-Za-z]+)|(?<name>[A-Za-z_]+))",
"g"
)
Insert cell
token = (text) => {
const r = [];
while (true) {
const m = RE_TOKEN.exec(text);
if (!m) {
break;
} else if (m.groups.key !== undefined) {
r.push({
type: "factor",
name: m.groups.key,
factor: parseInt(m.groups.factor)
});
} else if (m.groups.name !== undefined) {
r.push({ type: "symbol", name: m.groups.name });
} else if (m.groups.value !== undefined) {
r.push({ type: "metric", factor: m.groups.value, name: m.groups.unit });
} else {
throw new Error(`Unexpected match: ${m}`);
}
}
return new Token(r);
}
Insert cell
token("Red.L10")
Insert cell
class Token {
constructor(steps) {
this.steps = [...steps];
}

// This is the main function
eval(context = {}) {
// The resolver becomes a function that gets assigned the state, while resolving
// The context.
const resolver = (
name = "value",
factor = undefined,
state = undefined
) => {
const value =
state && state[name] !== undefined ? state[name] : context[name];
if (typeof value === "string") {
return { value: color(value) };
} else if (typeof value === "function") {
// TODO: We may want to merge in the rest of the context, maybe not only the value?
factor = factor ? parseFloat(factor) : factor;
const res = value(
// NOTE: We copy the state in the function so that the context is accessible
Object.assign(
(name, factor) => resolver(name, factor, state).value,
state
),
factor
);
return {
value: res
};
} else if (typeof value === "object") {
return (({ value, ...rest }) => rest)(value);
} else {
throw new Error(`Undefined token '${name}'`);
}
};

return this.steps.reduce((r, v, i) => {
const { type, name, factor } = v;
const { value, ...rest } = resolver(name, factor, r);
return Object.assign(r, {
color: value instanceof Color ? value : r.color,
value,
...rest
});
}, {});
}
}
Insert cell
Insert cell
Insert cell
color = (_) => Color.Ensure(_)
Insert cell
class Color {
static BLACK = new Color([0, 0, 0, 1]);
static WHITE = new Color([1, 1, 1, 1]);

static DarkLight(black = undefined, white = undefined) {
white = Color.Ensure(white || Color.WHITE);
black = Color.Ensure(black || Color.BLACK);
const dark = black.luminance < white.luminance ? black : white;
const light = dark === black ? white : black;
return [dark, light];
}

static Ensure(value, strict = true) {
return value instanceof Color
? value
: value instanceof Array ||
(typeof value === "string" && value.match(RE_COLOR))
? new Color(value)
: strict
? (() => {
throw new Error(
`Could not decode color from ${typeof value}: ${value}`
);
})()
: null;
}

constructor(value) {
if (!(value instanceof Array || typeof value === "string")) {
throw new Error(
`Color value should be RGBA array or hex string, got ${typeof value}: ${value}`
);
}
this.value = value instanceof Array ? value : hex(value);
if (this.value.reduce((r, v) => (isNaN(v) ? r + 1 : r), 0)) {
throw new Error(
`Could not create color from: ${value}, got NaN in ${this.value}`
);
}
if (!this.value) {
throw new Error(`Could not create color from: ${value}`);
}
while (this.value.length < 4) {
this.value.push(1.0);
}
}

get rgb() {
const [r, g, b] = this.value.map((_) => Math.round(gamma(_) * 255));
return `rgb(${r},${g},${b})`;
}

get hex() {
return hex(this.value);
}

get lightness() {
// SEE: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance
// Ref: "Another way to describe this is that our perception roughly follows a power curve with an exponent of ~0.425, so perceptual lightness is approximately L* = Y0.425, though this depends on adaptation."
return Math.pow(this.luminance, 0.425);
}

get luminance() {
const [r, g, b] = this.value;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

// Returns a version of the current color that contrast (in luminance) with
// the other color of `delta` (between 0 and 1). The actual luminance delta
// may be lower if it's not possible to make the luminance greter.
contrast(other, delta = 0.1) {
const la = this.luminance;
const lb = Color.Ensure(other).luminance;
const ld = clamp(lb - la, 0, 1);
return Math.abs(ld) === delta
? this
: this.tint(clamp(la + delta * (ld / Math.abs(ld)), 0, 1));
}

// Creates a derived color with the given alpha value.
alpha(value = 0.5) {
const res = new Color([...this.value]);
res.value[3] = clamp(value);
return res;
}

// Returns a grey version of this color
grey(k=1) {
const l = this.luminance;
const g = new Color([l, l, l, 1])
return k >= 1 ? g : this.blend(g, k);
}

tint(luminance = 0.5, black = undefined, white = undefined) {
const [dark, light] = Color.DarkLight(black, white);
const l = clamp(this.luminance, 0, 1);
const v = clamp(luminance, 0, 1);
// If the current luminance is less than the target luminance
return l < v
? // Then we blend to white of a factor that the difference in
this.blend(light, prel(v, l, light.luminance))
: this.blend(dark, prel(v, l, dark.luminance));
}

scale(steps = 10, black = undefined, white = undefined) {
const [dark, light] = Color.DarkLight(black, white);
return new Array(steps + 1)
.fill(0)
.map((_, i) => this.tint(i / steps, dark, light));
}

blend(color, k = 0.5) {
const c = Color.Ensure(color, true);
return new Color(
this.value.map((v, i) => clamp(lerp(v, c.value[i], k), 0, 1))
);
}

// TODO: Grey
toString() {
return hex(this.value);
}
}
Insert cell
Insert cell
Insert cell
ColorScale(color("#FF0000").scale(10))
Insert cell
Insert cell
Insert cell
color("#FF0000")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
gamma([1.0, 1.0, 1.0])
Insert cell
Insert cell
Insert cell
Insert cell
lerp = (a, b, k) => a + (b - a) * k
Insert cell
clamp = (v, a = 0.0, b = 1.0) => Math.min(Math.max(v, a), b)
Insert cell
minmax = (a, b) => [Math.min(a, b), Math.max(a, b)]
Insert cell
range = (count) => new Array(count).fill(0).map((_, i) => i)
Insert cell
prel = (v, a, b) => (v - a) / (b - a)
Insert cell
steps = (count) =>
range(count)
.map((_) => _ / count)
.concat([1.0])
Insert cell
Insert cell
asMappable = (f) => (_) => _ instanceof Array ? _.map(f) : f(_)
Insert cell
def = (...rest) => {
for (let v of rest) {
if (v !== undefined) {
return v;
}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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