From 4d5ec6937247dd98e232175b64f9b0f74685b3cc Mon Sep 17 00:00:00 2001 From: Andrew Michael McNutt Date: Wed, 14 Feb 2024 18:00:43 -0800 Subject: [PATCH] pretty much full affect story --- LintLanguageDocs.md | 10 +- src/components/Tooltip.svelte | 4 +- src/content-modules/MainColumn.svelte | 37 +---- src/controls/PalTypeConfig.svelte | 116 +++++++++++++++ src/controls/SetColorSpace.svelte | 2 - src/lib/ColorLint.ts | 6 +- src/lib/CustomLint.ts | 18 ++- src/lib/api-calls.ts | 11 +- src/lib/linter-tools/lint-fixer.ts | 2 +- src/lib/linter-tools/lint-worker.worker.ts | 2 + src/lib/linter.ts | 45 ++++-- src/lib/lints/affects.ts | 157 +++++++++++++++++++++ src/lib/lints/sequential-order.ts | 2 +- src/lib/utils.ts | 16 +++ src/linting/EvalResponse.svelte | 4 +- src/linting/NewLintSuggestion.svelte | 4 +- src/stores/color-store.ts | 14 +- src/types.ts | 33 +++-- 18 files changed, 400 insertions(+), 83 deletions(-) create mode 100644 src/controls/PalTypeConfig.svelte create mode 100644 src/lib/lints/affects.ts diff --git a/LintLanguageDocs.md b/LintLanguageDocs.md index 4228034e..3bd0ecd4 100644 --- a/LintLanguageDocs.md +++ b/LintLanguageDocs.md @@ -20,11 +20,11 @@ Comparisons: {">": {left: Value, right: Value}} Math Operations: -\*: {left: Number | Variable, right: Number | Variable} -+: {left: Number | Variable, right: Number | Variable} -/: {left: Number | Variable, right: Number | Variable} --: {left: Number | Variable, right: Number | Variable} -absDiff: {left: Number | Variable, right: Number | Variable} +{"\*": {left: Number | Variable, right: Number | Variable}} +{"+": {left: Number | Variable, right: Number | Variable}} +{"/": {left: Number | Variable, right: Number | Variable}} +{"-": {left: Number | Variable, right: Number | Variable}} +{absDiff: {left: Number | Variable, right: Number | Variable}} Value Comparisons: {dist: {left: Color | Variable, right: Color | Variable}, space: COLOR_SPACE } diff --git a/src/components/Tooltip.svelte b/src/components/Tooltip.svelte index 5ca55dbb..2564c6b9 100644 --- a/src/components/Tooltip.svelte +++ b/src/components/Tooltip.svelte @@ -55,8 +55,8 @@ : "0"; $: leftString = boundingBox ? `${boundingBox.x}px` : "0"; $: { - if (boundingBox.y + 300 > window.screen.height) { - topString = `${window.screen.height - 300}px`; + if (boundingBox.y + 500 > window.screen.height) { + topString = `${window.screen.height - 500}px`; } } $: { diff --git a/src/content-modules/MainColumn.svelte b/src/content-modules/MainColumn.svelte index c18d9416..e73e2cf2 100644 --- a/src/content-modules/MainColumn.svelte +++ b/src/content-modules/MainColumn.svelte @@ -2,12 +2,14 @@ import colorStore from "../stores/color-store"; import focusStore from "../stores/focus-store"; import configStore from "../stores/config-store"; + import { buttonStyle } from "../lib/styles"; import AdjustOrder from "../controls/AdjustOrder.svelte"; import Background from "../components/Background.svelte"; import ColorScatterPlot from "../scatterplot/ColorScatterPlot.svelte"; import ExampleAlaCart from "../example/ExampleAlaCarte.svelte"; - import GetColorsFromString from "../controls/GetColorsFromString.svelte"; + import PalTypeConfig from "../controls/PalTypeConfig.svelte"; + import ModifySelection from "../controls/ModifySelection.svelte"; import Nav from "../components/Nav.svelte"; import PalPreview from "../components/PalPreview.svelte"; @@ -17,16 +19,6 @@ import ContentEditable from "../components/ContentEditable.svelte"; $: currentPal = $colorStore.palettes[$colorStore.currentPal]; - - const descriptions = { - sequential: - "Sequential palettes are used to represent a range of values. They are often used to represent quantitative data, such as temperature or elevation.", - diverging: - "Diverging palettes are used to represent a range of values around a central point. They are often used to represent quantitative data, such as temperature or elevation.", - categorical: - "Categorical palettes are used to represent a set of discrete values. They are often used to represent qualitative data, such as different types of land cover or different political parties.", - }; - $: palType = currentPal.type; $: selectedBlindType = $configStore.colorSim; @@ -100,28 +92,7 @@ }} /> {#if $configStore.mainColumnRoute === "palette-config"} - colorStore.setCurrentPalColors(colors)} - colorSpace={currentPal.colorSpace} - colors={currentPal.colors} - /> - -
- This is a - palette. {descriptions[palType]} -
+ {/if} {#if $configStore.mainColumnRoute === "example"} + import colorStore from "../stores/color-store"; + import { affects, contexts } from "../types"; + + $: currentPal = $colorStore.palettes[$colorStore.currentPal]; + $: palType = currentPal.type; + + import GetColorsFromString from "../controls/GetColorsFromString.svelte"; + const descriptions = { + sequential: + "Sequential palettes are used to represent a range of values. They are often used to represent quantitative data, such as temperature or elevation.", + diverging: + "Diverging palettes are used to represent a range of values around a central point. They are often used to represent quantitative data, such as temperature or elevation.", + categorical: + "Categorical palettes are used to represent a set of discrete values. They are often used to represent qualitative data, such as different types of land cover or different political parties.", + }; + + +
+ colorStore.setCurrentPalColors(colors)} + colorSpace={currentPal.colorSpace} + colors={currentPal.colors} + /> + +
Config
+
+ This is a + palette. {descriptions[palType]} +
+
+ What types of affects do you intend to have on the palette? +
+
+ + {#each affects as affect} +
+ +
+ {/each} +
+ +
What types of contexts do you intend to use?
+
+ + {#each contexts as context} +
+ +
+ {/each} +
+
diff --git a/src/controls/SetColorSpace.svelte b/src/controls/SetColorSpace.svelte index 58a7ea44..70c00458 100644 --- a/src/controls/SetColorSpace.svelte +++ b/src/controls/SetColorSpace.svelte @@ -2,13 +2,11 @@ import { colorPickerConfig } from "../lib/Color"; import Tooltip from "../components/Tooltip.svelte"; import { buttonStyle } from "../lib/styles"; - // $: colorSpace = $colorStore.currentPal.colorSpace; export let colorSpace: string; export let onChange: (e: any) => void; // const notAllowed = new Set(["rgb", "hsv", "hsl", "srgb", "lch", "oklch"]); // const notAllowed = new Set(["rgb", "lch", "oklch", "srgb"]); const notAllowed = new Set(["rgb", "oklch", "srgb", "jzazbz", "oklab"]); - // const onChange = (e: any) => colorStore.setColorSpace(e); $: options = Object.keys(colorPickerConfig) .filter((x) => !notAllowed.has(x)) .sort(); diff --git a/src/lib/ColorLint.ts b/src/lib/ColorLint.ts index 0e1e62ac..6cbfbd3c 100644 --- a/src/lib/ColorLint.ts +++ b/src/lib/ColorLint.ts @@ -1,4 +1,4 @@ -import type { Palette, PalType } from "../types"; +import type { Palette, PalType, Affect, Context } from "../types"; export type LintLevel = "error" | "warning"; export interface LintResult { @@ -10,12 +10,16 @@ export interface LintResult { description: string; isCustom: false | string; taskTypes: PalType[]; + affectTypes: Affect[]; + contextTypes: Context[]; subscribedFix: string; } export class ColorLint { name: string = ""; taskTypes: PalType[] = []; + affectTypes: Affect[] = []; + contextTypes: Context[] = []; passes: boolean; checkData: CheckData; palette: Palette; diff --git a/src/lib/CustomLint.ts b/src/lib/CustomLint.ts index 71c69d5d..07dc2be6 100644 --- a/src/lib/CustomLint.ts +++ b/src/lib/CustomLint.ts @@ -1,5 +1,5 @@ import { ColorLint } from "./ColorLint"; -import type { PalType } from "../types"; +import type { PalType, Affect, Context } from "../types"; import { LLEval, prettyPrintLL, @@ -8,22 +8,26 @@ import { import * as Json from "jsonc-parser"; export interface CustomLint { - program: string; - name: string; - taskTypes: PalType[]; - level: "error" | "warning"; - group: string; + affectTypes?: Affect[]; + contextTypes?: Context[]; + blameMode: "pair" | "single" | "none"; description: string; failMessage: string; + group: string; id: string; - blameMode: "pair" | "single" | "none"; + level: "error" | "warning"; + name: string; + program: string; subscribedFix?: string; + taskTypes: PalType[]; } export function CreateCustomLint(props: CustomLint) { return class CustomLint extends ColorLint { name = props.name; taskTypes = props.taskTypes; + affectTypes = props.affectTypes || []; + contextTypes = props.contextTypes || []; level = props.level; group = props.group; description = props.description; diff --git a/src/lib/api-calls.ts b/src/lib/api-calls.ts index 19d891be..52f08e64 100644 --- a/src/lib/api-calls.ts +++ b/src/lib/api-calls.ts @@ -1,6 +1,7 @@ import type { Palette } from "../types"; import * as Json from "jsonc-parser"; import LintWorker from "./linter-tools/lint-worker.worker?worker"; +import { summarizePal } from "./utils"; type Engine = "openai" | "google"; type SimplePal = { background: string; colors: string[] }; @@ -75,6 +76,7 @@ export function suggestNameForPalette( return engineToScaffold[engine](`suggest-name`, body, true); } +// supports the add color search function export function suggestAdditionsToPalette( palette: Palette, engine: Engine, @@ -99,7 +101,11 @@ export function suggestContextualAdjustments( currentPal: Palette, engine: Engine ) { - const body = JSON.stringify({ prompt, ...palToString(currentPal) }); + const adjustPrompt = `${summarizePal(currentPal)}\n\n${prompt}`; + const body = JSON.stringify({ + prompt: adjustPrompt, + ...palToString(currentPal), + }); return engineToScaffold[engine]( `suggest-contextual-adjustments`, body, @@ -107,7 +113,8 @@ export function suggestContextualAdjustments( ); } -export function suggestFix(currentPal: Palette, error: string, engine: Engine) { +export function suggestFix(currentPal: Palette, msg: string, engine: Engine) { + const error = `${summarizePal(currentPal)}\n\n${msg}`; const body = JSON.stringify({ ...palToString(currentPal), error }); return engineToScaffold[engine](`suggest-fix`, body, true); } diff --git a/src/lib/linter-tools/lint-fixer.ts b/src/lib/linter-tools/lint-fixer.ts index 0a2937a6..9f6cdff2 100644 --- a/src/lib/linter-tools/lint-fixer.ts +++ b/src/lib/linter-tools/lint-fixer.ts @@ -1,6 +1,6 @@ import type { Palette } from "../../types"; -import { suggestFix } from "../api-calls"; import type { LintResult } from "../ColorLint"; +import { suggestFix } from "../api-calls"; import { Color } from "../Color"; export async function suggestLintAIFix( diff --git a/src/lib/linter-tools/lint-worker.worker.ts b/src/lib/linter-tools/lint-worker.worker.ts index 2bc25c74..fed1922c 100644 --- a/src/lib/linter-tools/lint-worker.worker.ts +++ b/src/lib/linter-tools/lint-worker.worker.ts @@ -70,6 +70,8 @@ async function dispatch(cmd: Command) { isCustom: x.isCustom, taskTypes: x.taskTypes as any, subscribedFix: x.subscribedFix, + affectTypes: x.affectTypes, + contextTypes: x.contextTypes, }; }); simpleLintCache.set(cmd.content, result); diff --git a/src/lib/linter.ts b/src/lib/linter.ts index 0974b4f4..4b9f8fa5 100644 --- a/src/lib/linter.ts +++ b/src/lib/linter.ts @@ -19,8 +19,10 @@ import MaxColors from "./lints/max-colors"; import MutuallyDistinct from "./lints/mutually-distinct"; import SequentialOrder from "./lints/sequential-order"; import UglyColors from "./lints/ugly-colors"; +import Affects from "./lints/affects"; export const BUILT_INS: CustomLint[] = [ + ...Affects, ...ColorBlindness, ...Fair, ...SizeDiscrim, @@ -39,22 +41,37 @@ export function runLintChecks( customLints: CustomLint[] ): ColorLint[] { const ignoreList = palette.evalConfig; + const affects = palette.intendedAffects; + const contexts = palette.intendedContexts; const lints = [ DivergingOrder, ...customLints.map((x) => CreateCustomLint(x)), ] as (typeof ColorLint)[]; - return lints - .map((x) => new x(palette)) - .filter((x) => x.taskTypes.includes(palette.type)) - .map((x) => { - if (ignoreList[x.name] && ignoreList[x.name].ignore) { - return x; - } - try { - return x.run(); - } catch (e) { - console.error(e); - return x; - } - }); + return ( + lints + .map((x) => new x(palette)) + // task type + .filter((x) => x.taskTypes.includes(palette.type)) + // affect type + .filter((x) => { + if (x.affectTypes.length === 0) return true; + return x.affectTypes.some((a) => affects.includes(a)); + }) + // context type + .filter((x) => { + if (x.contextTypes.length === 0) return true; + return x.contextTypes.some((a) => contexts.includes(a)); + }) + .map((x) => { + if (ignoreList[x.name] && ignoreList[x.name].ignore) { + return x; + } + try { + return x.run(); + } catch (e) { + console.error(e); + return x; + } + }) + ); } diff --git a/src/lib/lints/affects.ts b/src/lib/lints/affects.ts new file mode 100644 index 00000000..9979cd24 --- /dev/null +++ b/src/lib/lints/affects.ts @@ -0,0 +1,157 @@ +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[] = []; +const theseAffects = ["serious", "trustworthy", "calm"] as const; +theseAffects.forEach((affect) => { + const lint: CustomLint = { + name: `Saturated not appropriate for ${affect} affect`, + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varb: "c", + where: { ">": { left: { "hsl.l": "c" }, right: 70 } }, + predicate: { + not: { ">": { left: { "hsl.s": "c" }, right: 70 } }, + }, + }, + }), + taskTypes: ["sequential", "diverging", "categorical"] as const, + affectTypes: [affect], + level: "warning", + group: "affect", + description: `Highly saturated light colors are not appropriate for palettes that seek to be ${affect}. See "Affective color in visualization" for more.`, + failMessage: `This palette has colors which are highly saturated and light, which may not be appropriate for a ${affect} palette.`, + id: `saturated-${affect}-built-in`, + blameMode: "single", + }; + lints.push(lint); +}); + +// "light blues, beiges, and grays are appropriate for PLAYFUL" +// reframed asn assertion: PLAYFUL should have at least one light blue, beige, or gray +const lint1: CustomLint = { + name: `Playful affect issues`, + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + exist: { + in: "colors", + varb: "c", + predicate: { + or: [ + { similar: { left: "c", right: "lightblue", threshold: 20 } }, + { similar: { left: "c", right: "beige", threshold: 20 } }, + { similar: { left: "c", right: "gray", threshold: 20 } }, + ], + }, + }, + }), + taskTypes: ["sequential", "diverging", "categorical"] as const, + affectTypes: ["playful"], + level: "warning", + group: "affect", + description: `Palettes that seek to be playful should have at least one light blue, beige, or gray. See "Affective color in visualization" for more.`, + failMessage: `This palette does not have at least one light blue, beige, or gray, which may not be appropriate for a playful palette. In particular {{blame}} may be problematic.`, + id: `light-blues-beiges-grays-playful-built-in`, + blameMode: "single", +}; +lints.push(lint1); + +// "dark reds and browns are not POSITIVE": ALL colors c, NOT (c similar to "DARK RED" OR c similar to "BROWN") +const lint2: CustomLint = { + name: `Dark reds and browns are not positive`, + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varb: "c", + predicate: { + not: { + or: [ + { similar: { left: "c", right: "darkred", threshold: 20 } }, + { similar: { left: "c", right: "brown", threshold: 20 } }, + ], + }, + }, + }, + }), + taskTypes: ["sequential", "diverging", "categorical"] as const, + affectTypes: ["positive"], + level: "warning", + group: "affect", + description: `Palettes that seek to be positive should not have dark reds or browns. See "Affective color in visualization" for more.`, + failMessage: `This palette has dark reds or browns, which may not be appropriate for a positive palette. In particular {{blame}} may be problematic.`, + id: `dark-reds-browns-positive-built-in`, + blameMode: "single", +}; +lints.push(lint2); + +// "light colors, particularly greens, do not communicate NEGATIVE": ALL colors c, NOT (c similar to "GREEN" AND lab(l) > threshold) maybe more messaging that one? +const lint3: CustomLint = { + name: `Negative palette affect issues`, + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + all: { + in: "colors", + varb: "c", + predicate: { + not: { + or: [ + { similar: { left: "c", right: "green", threshold: 20 } }, + { ">": { left: { "lab.l": "c" }, right: 70 } }, + ], + }, + }, + }, + }), + taskTypes: ["sequential", "diverging", "categorical"], + affectTypes: ["negative"], + level: "warning", + group: "affect", + description: `Palettes that seek to be negative should not have light colors, particularly greens. See "Affective color in visualization" for more.`, + failMessage: `This palette has light colors, particularly greens, which may not be appropriate for a negative palette. In particular {{blame}} may be problematic.`, + id: `light-colors-greens-negative-built-in`, + blameMode: "single", +}; +lints.push(lint3); + +// "trustworthy has two thematic strategies (blue-gray, green-gray) bridge by a common color (yellow)": AND (EXIST color a, a similar to yellow) ....... +// const lint4: CustomLint = { +// name: `Trustworthy palettes usually blue-gray or green-gray with a yellow`, +// program: JSONToPrettyString({ +// // @ts-ignore +// $schema: `${location.href}lint-schema.json`, +// and: [ +// { +// or: [ +// ex +// ] +// }, +// { +// exist: { +// varb: 'a', +// in: 'colors', +// predicate: { similar: { left: 'a', right: 'yellow', threshold: 20 } } +// } +// } +// ], +// }), +// taskTypes: ["sequential", "diverging", "categorical"] as const, +// affectTypes: ["trustworthy"], +// level: "warning", +// group: "affect", +// description: `Palettes that seek to be trustworthy should have two thematic strategies (blue-gray, green-gray) bridged by a common color (yellow). See "Affective color in visualization" for more.`, +// failMessage: `This palette does not have two thematic strategies (blue-gray, green-gray) bridged by a common color (yellow), which may not be appropriate for a trustworthy palette.`, +// id: `trustworthy-thematic-strategies-yellow-built-in`, +// blameMode: "single", +// }; +// lints.push(lint4); + +export default lints; diff --git a/src/lib/lints/sequential-order.ts b/src/lib/lints/sequential-order.ts index 30d04430..ae9a6f8f 100644 --- a/src/lib/lints/sequential-order.ts +++ b/src/lib/lints/sequential-order.ts @@ -26,7 +26,7 @@ const lint: CustomLint = { }, ], }), - taskTypes: ["sequential", "diverging", "categorical"] as const, + taskTypes: ["sequential"] as const, level: "error", group: "usability", description: diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 36742552..60dc0419 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -608,3 +608,19 @@ export const titleCase = (str: string) => .split(" ") .map((x) => x[0].toUpperCase() + x.slice(1)) .join(" "); + +const oxfordJoin = (arr: string[]) => { + if (arr.length === 1) return arr[0]; + if (arr.length === 2) return arr.join(" and "); + return arr.slice(0, -1).join(", ") + ", and " + arr.slice(-1); +}; + +export function summarizePal(pal: Palette) { + const affectMsg = ` It is intended to convey ${oxfordJoin( + pal.intendedAffects + )} affects.`; + const contextMsg = ` It is intended to be used ${oxfordJoin( + pal.intendedContexts.map((x) => `${x}s`) + )}.`; + return `This is a ${pal.type} palette called '${pal.name}'.${affectMsg}${contextMsg}`; +} diff --git a/src/linting/EvalResponse.svelte b/src/linting/EvalResponse.svelte index 60a18f4d..833c60c3 100644 --- a/src/linting/EvalResponse.svelte +++ b/src/linting/EvalResponse.svelte @@ -110,9 +110,9 @@
Failed to generate suggestions
{:else if requestState === "loaded"} {#each suggestions as suggestion} -
+
-
+