Skip to content

Commit

Permalink
Develop muth guidelines
Browse files Browse the repository at this point in the history
  • Loading branch information
mcnuttandrew committed Feb 26, 2024
1 parent 718c65d commit 9ffaf02
Show file tree
Hide file tree
Showing 20 changed files with 372 additions and 48 deletions.
8 changes: 7 additions & 1 deletion src/components/Background.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export let onChange: (color: Color) => void;
export let bg: Color;
export let colorSpace: any;
export let onSpaceChange: (space: string) => void;
$: bgHex = bg.toHex();
</script>

Expand All @@ -19,7 +20,12 @@
onChange(Color.colorFromString(e.target.value, colorSpace));
}}
/>
<ColorChannelPicker color={bg} onColorChange={onChange} />
<ColorChannelPicker
{onSpaceChange}
colorMode={colorSpace}
color={bg}
onColorChange={onChange}
/>
</div>
<button
let:toggle
Expand Down
8 changes: 3 additions & 5 deletions src/components/ColorChannelPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ColorIO from "colorjs.io";
export let color: Color;
export let onColorChange: (color: Color) => void;
export let onSpaceChange: (space: string) => void;
export let colorMode: any = "lab";
$: measuredColorMode = color.spaceName;
type Channel = {
Expand Down Expand Up @@ -148,10 +149,7 @@
<div class="flex flex-col">
<select
value={colorMode}
on:change={(e) => {
// @ts-ignore
configStore.setChannelPickerSpace(e.currentTarget.value);
}}
on:change={(e) => onSpaceChange(e.currentTarget.value)}
>
{#each [...Object.keys(colorConfigs)] as colorMode}
<option value={colorMode}>{colorMode}</option>
Expand All @@ -170,7 +168,7 @@
{channel.name} ({channel.min}-{channel.max})
</span>
<input
class="h-6 text-right w-16"
class="h-6 text-right w-16 border-2"
type="number"
value={formatter(channel.value)}
min={channel.min}
Expand Down
6 changes: 5 additions & 1 deletion src/content-modules/ComparePal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@
bg = background.toHex();
configStore.setCompareBackground(background.toHex());
}}
onSpaceChange={(space) => {
// @ts-ignore
configStore.setCompareBackgroundSpace(space);
}}
bg={Color.colorFromHex(bg, colorSpace)}
{colorSpace}
colorSpace={$configStore.compareBackgroundSpace}
/>
<div>
<button class={buttonStyle} on:click={() => (showDiff = !showDiff)}>
Expand Down
4 changes: 4 additions & 0 deletions src/content-modules/Controls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
<ColorChannelPicker
color={colors[focusedColors[0]].toColorSpace(colorSpace)}
colorMode={$configStore.channelPickerSpace}
onSpaceChange={(space) => {
// @ts-ignore
configStore.setChannelPickerSpace(space);
}}
onColorChange={(color) => {
const updatedColors = [...colors];
updatedColors[focusedColors[0]] = color.toColorSpace(colorSpace);
Expand Down
9 changes: 7 additions & 2 deletions src/content-modules/MainColumn.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@
onChange={(space) => colorStore.setColorSpace(space)}
/>
<Background
onChange={(bg) => colorStore.setBackground(bg)}
onSpaceChange={(space) => {
// @ts-ignore
configStore.setChannelPickerSpaceBackground(space);
}}
onChange={(bg) =>
colorStore.setBackground(bg.toColorSpace(currentPal.colorSpace))}
bg={currentPal.background}
colorSpace={currentPal.colorSpace}
colorSpace={$configStore.channelPickerSpaceBackground}
/>
</div>
<ColorScatterPlot
Expand Down
81 changes: 81 additions & 0 deletions src/lib/ColorLint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ColorNameDiscriminability, { getName } from "./lints/name-discrim";
import Fair from "./lints/fair";
import Gamut from "./lints/in-gamut";
import MaxColors from "./lints/max-colors";
import MuthGuidelines from "./lints/muth-guidelines";
import MutuallyDistinct from "./lints/mutually-distinct";
import SequentialOrder from "./lints/sequential-order";
import SizeDiscrims from "./lints/size-discrim";
Expand Down Expand Up @@ -272,3 +273,83 @@ test("ColorLint - Background Contrast", async () => {
const fix2 = await suggestLintFix(examplePal, exampleLint2).then((x) => x[0]);
expect(fix2.colors.map((x) => x.toHex())).toMatchSnapshot();
});

test("ColorLint - MuthGuidelines (1) Background desaturation sufficient", () => {
const newLint = CreateCustomLint(MuthGuidelines[0]);

const examplePal = makePalFromString([
"#0084a9",
"#009de5",
"#5fb1ff",
"#bbc3ff",
]);
examplePal.background = Color.colorFromHex("#f4e3e3", "lab");
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromString([
"#0084a9",
"#009de5",
"#5fb1ff",
"#ecddff",
]);
examplePal2.background = Color.colorFromHex("#000", "lab");
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - MuthGuidelines (2) Avoid Tetradic Palettes", () => {
const newLint = CreateCustomLint(MuthGuidelines[1]);

const examplePal = makePalFromString([
"#0084a9",
"#009de5",
"#5fb1ff",
"#bbc3ff",
]);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromString([
"#d23bae",
"#3b6dd2",
"#d89a35",
"#36d745",
]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - MuthGuidelines (3) Prefer yellowish or blueish greens", () => {
const newLint = CreateCustomLint(MuthGuidelines[2]);

const examplePal = makePalFromString(["#bee38d", "#bbc3ff"]);
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromString(["#0084a9", "#93e789"]);
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});

test("ColorLint - MuthGuidelines (4) Avoid too much contrast with the background", () => {
const newLint = CreateCustomLint(MuthGuidelines[3]);

const examplePal = makePalFromString(["#f4b05d", "#3c828d", "#1c4b76"]);
examplePal.background = Color.colorFromHex("#f9f9f9", "lab");
const exampleLint = new newLint(examplePal).run();
expect(exampleLint.passes).toBe(true);
expect(exampleLint.message).toMatchSnapshot();

const examplePal2 = makePalFromString(["#0e2d48", "#3c828d", "#b87930"]);
examplePal2.background = Color.colorFromHex("#f9f9f9", "lab");
const exampleLint2 = new newLint(examplePal2).run();
expect(exampleLint2.passes).toBe(false);
expect(exampleLint2.message).toMatchSnapshot();
});
2 changes: 2 additions & 0 deletions src/lib/ColorLint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface LintResult {
affectTypes: Affect[];
contextTypes: Context[];
subscribedFix: string;
naturalLanguageProgram: string;
}

export class ColorLint<CheckData, ParamType> {
Expand All @@ -28,6 +29,7 @@ export class ColorLint<CheckData, ParamType> {
group: string = "";
description: string = "";
blameMode: "pair" | "single" | "none" = "none";
naturalLanguageProgram: string = "";
level: LintLevel = "error";
subscribedFix: string = "none";

Expand Down
1 change: 1 addition & 0 deletions src/lib/CustomLint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function CreateCustomLint(props: CustomLint) {
isCustom = props.id;
blameMode = props.blameMode;
subscribedFix = props.subscribedFix || "none";
naturalLanguageProgram = prettyPrintLL(Json.parse(props.program));

_runCheck(options: any) {
const prog = Json.parse(props.program);
Expand Down
16 changes: 16 additions & 0 deletions src/lib/__snapshots__/ColorLint.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ exports[`ColorLint - MaxColors 1`] = `"This palette has too many colors and may

exports[`ColorLint - MaxColors 2`] = `"This palette has too many colors and may be hard to discriminate in some contexts. Maximum: 10."`;

exports[`ColorLint - MuthGuidelines (1) Background desaturation sufficient 1`] = `"Colorful backgrounds can seem like a good idea, but they can easily distract from your data and they limit your potential color palette. Desaturated colors are your best bet. Roughly, you want either HSL.L > 90 and HSV.S < 8 for light backgrounds, or you want 10 < HSL.L < 25 and HSV.S < 20 for dark backgrounds. See "https://blog.datawrapper.de/beautifulcolors/#12" for more"`;
exports[`ColorLint - MuthGuidelines (1) Background desaturation sufficient 2`] = `"Colorful backgrounds can seem like a good idea, but they can easily distract from your data and they limit your potential color palette. Desaturated colors are your best bet. Roughly, you want either HSL.L > 90 and HSV.S < 8 for light backgrounds, or you want 10 < HSL.L < 25 and HSV.S < 20 for dark backgrounds. See "https://blog.datawrapper.de/beautifulcolors/#12" for more"`;
exports[`ColorLint - MuthGuidelines (2) Avoid Tetradic Palettes 1`] = `"This palette is tetradic, which is not recommended."`;
exports[`ColorLint - MuthGuidelines (2) Avoid Tetradic Palettes 2`] = `"This palette is tetradic, which is not recommended."`;
exports[`ColorLint - MuthGuidelines (3) Prefer yellowish or blueish greens 1`] = `"When using green, it is better to try to it a yellow or blue one (hue angle less than 60 or greater than 160). T See "https://blog.datawrapper.de/beautifulcolors/#5" for more."`;
exports[`ColorLint - MuthGuidelines (3) Prefer yellowish or blueish greens 2`] = `"When using green, it is better to try to it a yellow or blue one (hue angle less than 60 or greater than 160). T See "https://blog.datawrapper.de/beautifulcolors/#5" for more."`;
exports[`ColorLint - MuthGuidelines (4) Avoid too much contrast with the background 1`] = `"Don't make your colors too dark and saturated when you're using a bright background. If in doubt, try it out. Make your colors lighter, pull some saturation out of them and see how it feels. See "https://blog.datawrapper.de/beautifulcolors/#11" for more."`;
exports[`ColorLint - MuthGuidelines (4) Avoid too much contrast with the background 2`] = `"Don't make your colors too dark and saturated when you're using a bright background. If in doubt, try it out. Make your colors lighter, pull some saturation out of them and see how it feels. See "https://blog.datawrapper.de/beautifulcolors/#11" for more."`;
exports[`ColorLint - MutuallyDistinct 1`] = `"Some colors in this palette () are not differentiable from each other."`;
exports[`ColorLint - MutuallyDistinct 2`] = `"Some colors in this palette (#f5f5dc and #d7fcef) are not differentiable from each other."`;
Expand Down
21 changes: 13 additions & 8 deletions src/lib/lint-language/LintLanguage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ test("LintLanguage conjunctions", () => {
],
};
expect(LLEval(prog1, exampleColors).result).toBe(true);
expect(prettyPrintLL(prog1)).toBe("count(colors) < 10 and count(colors) > 2");
expect(prettyPrintLL(prog1)).toBe(
"(count(colors) < 10 and count(colors) > 2)"
);

const prog2 = {
or: [
Expand All @@ -76,7 +78,9 @@ test("LintLanguage conjunctions", () => {
],
};
expect(LLEval(prog2, exampleColors).result).toBe(false);
expect(prettyPrintLL(prog2)).toBe("count(colors) < 2 or count(colors) > 10");
expect(prettyPrintLL(prog2)).toBe(
"(count(colors) < 2 or count(colors) > 10)"
);
});

test("LintLanguage Quantifiers All - Simple", () => {
Expand Down Expand Up @@ -165,7 +169,7 @@ test("LintLanguage permutativeBlame - Tableau 10", () => {
});

const expectedOutBlind = (type: string) =>
`ALL a in colors, ALL b in colors WHERE index(a) != index(b), NOT similar(cvdSim(a, ${type}), cvdSim(b, ${type})) > 9`;
`ALL a in colors, ALL b in colors WHERE index(a) != index(b), NOT similar(cvdSim(a, ${type}), cvdSim(b, ${type})) < 9`;
const allBlindProg = (type: string) => ({
all: {
in: "colors",
Expand Down Expand Up @@ -397,19 +401,20 @@ test("LintLanguage to color rotate", () => {
in: "colors",
varb: "b",
predicate: {
"==": {
similar: {
left: { "hsl.h": "a" },
right: {
"+": { left: { "hsl.h": "b" }, right: 180 },
},
threshold: 5,
},
},
},
},
},
};
expect(prettyPrintLL(realisticProgram)).toBe(
"EXIST a in colors, EXIST b in colors, hsl.h(a) == hsl.h(b) + 180"
"EXIST a in colors, EXIST b in colors, similar(hsl.h(a), hsl.h(b) + 180) < 5"
);
expect(LLEval(realisticProgram, exampleColors).result).toBe(false);
});
Expand Down Expand Up @@ -633,7 +638,7 @@ test("LintLanguage Sequential Colors", () => {
const inOrder = toPal(["#d4a8ff", "#7bb9ff", "#008694"]);
expect(LLEval(program, inOrder).result).toBe(true);
expect(prettyPrintLL(program)).toBe(
"ALL (a, b) in colors WHERE index(a) - 1 == index(b), lab.l(a) > lab.l(b) or ALL (a, b) in colors WHERE index(a) - 1 == index(b), lab.l(a) < lab.l(b)"
"(ALL (a, b) in colors WHERE index(a) - 1 == index(b), lab.l(a) > lab.l(b) or ALL (a, b) in colors WHERE index(a) - 1 == index(b), lab.l(a) < lab.l(b))"
);
});

Expand Down Expand Up @@ -721,7 +726,7 @@ test("LintLanguage Background differentiability", () => {
},
};
const astString = prettyPrintLL(program);
expect(astString).toBe("ALL a in colors, NOT similar(a, background) > 15");
expect(astString).toBe("ALL a in colors, NOT similar(a, background) < 15");
const result = LLEval(program, toPal(["#fff", "#eee", "#000", "#ddd"]));
expect(result.result).toBe(false);
expect(result.blame).toStrictEqual([0, 1, 3]);
Expand All @@ -740,7 +745,7 @@ test("LintLanguage Sequential Similarity", () => {
};
const astString = prettyPrintLL(program);
expect(astString).toBe(
"ALL (a, b) in colors WHERE index(a) != index(b), NOT similar(a, b) > 10"
"ALL (a, b) in colors WHERE index(a) != index(b), NOT similar(a, b) < 10"
);
const result = LLEval(program, toPal(["#fff", "#eee", "#000", "#ddd"]));
expect(result.result).toBe(false);
Expand Down
11 changes: 8 additions & 3 deletions src/lib/lint-language/lint-language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export class LLConjunction extends LLNode {
if (this.type === "id") return this.children[0].toString();
if (this.type === "none") return "";
if (this.type === "not") return `NOT ${this.children[0].toString()}`;
return this.children.map((x) => x.toString()).join(` ${this.type} `);
return `(${this.children.map((x) => x.toString()).join(` ${this.type} `)})`;
}
}

Expand Down Expand Up @@ -407,7 +407,12 @@ function compareValues(
}
return diff < thresh;
}
throw new Error("Type error. Similar must be used with colors.");
if (typeof left === "number" && typeof right === "number") {
return Math.abs(left - right) < thresh;
}
throw new Error(
"Type error. Similar must be used with colors or numbers."
);
case "==":
return left === right;
case "!=":
Expand Down Expand Up @@ -477,7 +482,7 @@ export class LLPredicate extends LLNode {
const left = this.left.toString();
const right = this.right.toString();
if (this.type === "similar") {
return `similar(${left}, ${right}) > ${this.threshold}`;
return `similar(${left}, ${right}) < ${this.threshold}`;
}
return `${left} ${type} ${right}`;
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/linter-tools/lint-fixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export async function suggestLintAIFix(
engine: string
) {
const colorSpace = palette.colorSpace;
console.log(lint);
return suggestFix(palette, lint.message, engine as any).then((x) => {
const msg = `${lint.message}\n\nFailed: ${lint.naturalLanguageProgram}`;
return suggestFix(palette, msg, engine as any).then((x) => {
if (x.length === 0) {
throw new Error("No suggestions");
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/linter-tools/lint-worker.worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as idb from "idb-keyval";
import { runLintChecks } from "../linter";
import { prettyPrintLL } from "../lint-language/lint-language";
import type { CustomLint } from "../CustomLint";
import type { Palette } from "../../types";
import { Color } from "../Color";
Expand Down Expand Up @@ -84,6 +85,7 @@ async function dispatch(cmd: Command) {
subscribedFix: x.subscribedFix,
affectTypes: x.affectTypes,
contextTypes: x.contextTypes,
naturalLanguageProgram: x.naturalLanguageProgram,
};
});
simpleLintCache.set(cmd.content, result);
Expand Down
2 changes: 2 additions & 0 deletions src/lib/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Fair from "./lints/fair";
import Gamut from "./lints/in-gamut";
import MaxColors from "./lints/max-colors";
import MutuallyDistinct from "./lints/mutually-distinct";
import MuthGuidelines from "./lints/muth-guidelines";
import NameDiscriminability from "./lints/name-discrim";
import SequentialOrder from "./lints/sequential-order";
import SizeDiscrim from "./lints/size-discrim";
Expand All @@ -26,6 +27,7 @@ export const BUILT_INS: CustomLint[] = [
...Affects,
...ColorBlindness,
...Fair,
...MuthGuidelines,
...SizeDiscrim,
AvoidExtremes,
BackgroundDifferentiability,
Expand Down
1 change: 0 additions & 1 deletion src/lib/lints/affects.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { JSONToPrettyString } from "../utils";
import type { CustomLint } from "../CustomLint";
import type { LintFixer } from "../linter-tools/lint-fixer";

// "Highly saturated light colors will not be appropriate for SERIOUS/TRUST/CALM": ALL (FILTER colors c, lab(c) > threshold) b, NOT hsl(b) > threshold
const lints: CustomLint[] = [];
Expand Down
Loading

0 comments on commit 9ffaf02

Please sign in to comment.