diff --git a/LintLanguageDocs.md b/LintLanguageDocs.md index 3bd0ecd4..d3b7210c 100644 --- a/LintLanguageDocs.md +++ b/LintLanguageDocs.md @@ -37,6 +37,7 @@ Aggregates {min: Variable | Number[]} {max: Variable | Number[]} {mean: Variable | Number[]} +{std: Variable | Number[]} {first: Variable | Number[]} {last: Variable | Number[]} {extent: Variable | Number[]} @@ -50,8 +51,9 @@ Color Manipulations: Maps: {map: Variable | Value[], func: Operation, varb: Variable} {sort: Variable | Value[], func: Operation, varb: Variable} -{reverse: Variable | Value[]} {filter: Variable | Value[], func: EXPR, varb: Variable} +{reverse: Variable | Value[]} +{speed: Variable | Value[]} ```yaml --- diff --git a/README.md b/README.md index 68dbffdd..6e31a56a 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ First time you start it up you should also run `yarn prep data` - [ ] Handles get stuck on channel picker again, ugh - [ ] Bug: Color channel usage slightly cursed (doesn't update positions correctly) - [ ] Bug: rotate in polar coordinates doesn't work right +- [ ] Dont allow saves to examples when there's an error +- [ ] Switch the examples to be tabs or single +- [ ] Consider moving the names and color chunks into the left hand column for space - [x] LCH colors upside down god damn it - [x] Clone Rule - [x] Compact more compact diff --git a/public/lint-schema.json b/public/lint-schema.json index 351b5d56..e4520e47 100644 --- a/public/lint-schema.json +++ b/public/lint-schema.json @@ -131,14 +131,22 @@ "varb": { "$ref": "#/definitions/LintVariable" } }, "required": ["sort", "func", "varb"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "speed": { "$ref": "#/definitions/LintVariable" } + }, + "required": ["speed"] } ] }, - "LintReduce": { + "LintAggregate": { "type": "object", "additionalProperties": false, "patternProperties": { - "^(sum|count|mean|max|min|first|last|extent)$": { + "^(sum|count|mean|max|min|first|last|extent|std)$": { "anyOf": [ { "$ref": "#/definitions/LintVariable" }, { "type": "array" }, @@ -365,7 +373,7 @@ ] }, "LintValue": { - "description": "A LintValue is a JSON object that represents a value. It can be a string, a number, a boolean, a LintColor, a LintVariable, a LintMathOps, a LintPairOps, a LintReduce, a LintColorFunction or a LintExpression.", + "description": "A LintValue is a JSON object that represents a value. It can be a string, a number, a boolean, a LintColor, a LintVariable, a LintMathOps, a LintPairOps, a LintAggregate, a LintColorFunction or a LintExpression.", "anyOf": [ { "type": "string" }, { "type": "number" }, @@ -374,7 +382,7 @@ { "$ref": "#/definitions/LintVariable" }, { "$ref": "#/definitions/LintMathOps" }, { "$ref": "#/definitions/LintPairOps" }, - { "$ref": "#/definitions/LintReduce" }, + { "$ref": "#/definitions/LintAggregate" }, { "$ref": "#/definitions/LintColorFunction" } ] }, diff --git a/src/lib/ColorLint.test.ts b/src/lib/ColorLint.test.ts index c620170a..703d31f2 100644 --- a/src/lib/ColorLint.test.ts +++ b/src/lib/ColorLint.test.ts @@ -12,12 +12,12 @@ import BackgroundContrast from "./lints/background-contrast"; import CatOrderSimilarity from "./lints/cat-order-similarity"; import ColorBlindness from "./lints/color-blindness"; import ColorNameDiscriminability, { getName } from "./lints/name-discrim"; -import Discrims from "./lints/size-discrim"; import Fair from "./lints/fair"; import Gamut from "./lints/in-gamut"; import MaxColors from "./lints/max-colors"; import MutuallyDistinct from "./lints/mutually-distinct"; import SequentialOrder from "./lints/sequential-order"; +import SizeDiscrims from "./lints/size-discrim"; import UglyColors from "./lints/ugly-colors"; const unique = (arr: T[]): T[] => [...new Set(arr)]; @@ -182,13 +182,13 @@ test("ColorLint - ColorNameDiscriminability", async () => { test("ColorLint - SizeDiscrim (Thin)", () => { const examplePal = makePalFromString(["#0084a9", "#bad", "#008000"]); - const newLint = CreateCustomLint(Discrims[0]); const exampleLint = new newLint(examplePal).run(); expect(exampleLint.passes).toBe(true); expect(exampleLint.message).toMatchSnapshot(); const examplePal2 = makePalFromString(["#0084a9", "#009de5", "#8ca9fa"]); const exampleLint2 = new newLint(examplePal2).run(); + const newLint = CreateCustomLint(SizeDiscrims[0]); expect(exampleLint2.passes).toBe(false); expect(exampleLint2.message).toMatchSnapshot(); }); diff --git a/src/lib/lint-language/lint-language.ts b/src/lib/lint-language/lint-language.ts index e8f5faf8..36ebc320 100644 --- a/src/lib/lint-language/lint-language.ts +++ b/src/lib/lint-language/lint-language.ts @@ -478,7 +478,7 @@ export class LLValue extends LLNode { | LLColor | LLNumber | LLVariable - | LLReduces + | LLAggregate | LLNumberOp ) { super(); @@ -495,7 +495,7 @@ export class LLValue extends LLNode { LLColor, LLNumber, LLVariable, - LLReduces, + LLAggregate, LLNumberOp, ]; const value = tryTypes(types, options)(node); @@ -821,11 +821,12 @@ const reduceTypes = [ "min", "max", "mean", + "std", "first", "last", "extent", ] as const; -export class LLReduces extends LLNode { +export class LLAggregate extends LLNode { constructor( private type: (typeof reduceTypes)[number], private children: LLValueArray | LLVariable | LLMap @@ -836,7 +837,9 @@ export class LLReduces extends LLNode { this.evalCheck(env); const children = this.children.evaluate(env).result; if (!Array.isArray(children)) { - throw new Error("Type error"); + throw new Error( + "Type error, aggregate received something that was an array" + ); } switch (this.type) { case "count": @@ -847,6 +850,12 @@ export class LLReduces extends LLNode { return { result: children[children.length - 1], env }; case "sum": return { result: children.reduce((a, b) => a + b, 0), env }; + case "std": + const sum = children.reduce((a, b) => a + b, 0); + const mean = sum / children.length; + const variance = + children.reduce((a, b) => a + (b - mean) ** 2, 0) / children.length; + return { result: Math.sqrt(variance), env }; case "mean": return { result: children.reduce((a, b) => a + b, 0) / children.length, @@ -865,6 +874,9 @@ export class LLReduces extends LLNode { } static tryToConstruct(node: any, options: OptionsConfig) { const reduceType = reduceTypes.find((x) => node[x]); + if (node.avg) { + throw new Error("Did you mean mean instead of avg?"); + } if (!reduceType) return false; const children = node[reduceType]; if (!children) return false; @@ -875,14 +887,14 @@ export class LLReduces extends LLNode { childType = tryTypes([LLVariable, LLMap], options)(children); } if (!childType) return false; - return new LLReduces(reduceType, childType); + return new LLAggregate(reduceType, childType); } toString(): string { return `${this.type}(${this.children.toString()})`; } } -const mapTypes = ["map", "filter", "sort", "reverse"] as const; +const mapTypes = ["map", "filter", "sort", "reverse", "speed"] as const; // example syntax // {map: colors, func: {cvdSim: {type: "protanomaly"}}, varb: "x"} export class LLMap extends LLNode { @@ -920,6 +932,32 @@ export class LLMap extends LLNode { const childrenCopy2 = [...children]; childrenCopy2.reverse(); return { result: childrenCopy2, env }; + case "speed": + // todo maybe make this take algorithm as argument? + const speed = []; + const allNumbers = children.every( + (x) => + typeof x === "number" || + (typeof x === "object" && x?.type === "") + ); + const allColors = children.every((x) => x instanceof Color); + if (!allNumbers && !allColors) { + const types = children.map((x) => x); + console.log(children); + throw new Error( + `Type error, speed must receive all numbers or all colors, got ${types}` + ); + } + for (let i = 0; i < children.length - 1; i++) { + const a = children[i]; + const b = children[i + 1]; + if (allNumbers) { + speed.push(Math.abs(a - b)); + } else { + speed.push(a.symmetricDeltaE(b, "2000")); + } + } + return { result: speed, env }; } } static tryToConstruct(node: any, options: OptionsConfig) { @@ -932,7 +970,7 @@ export class LLMap extends LLNode { let func; if (op === "filter") { func = tryTypes([LLExpression], options)(node.func); - } else if (op === "reverse") { + } else if (op === "reverse" || op === "speed") { // reverse doesn't take any arguments besides the target varb = " "; func = " "; diff --git a/src/lib/lint-language/lint-type.ts b/src/lib/lint-language/lint-type.ts index 1e0be7ec..e88f5814 100644 --- a/src/lib/lint-language/lint-type.ts +++ b/src/lib/lint-language/lint-type.ts @@ -59,13 +59,16 @@ type LintPairOps = | "Lstar" | "DeltaPhi"; }; +type MapTarget = LintVariable | LintValue[]; +type MapEval = LintColorFunction | LintPairOps; type LintMap = - // | { map: LintVariable | LintValue[]; func: LintColorFunction | LintPairOps } - // | { sort: LintVariable | LintValue[]; func: LintColorFunction | LintPairOps } - // | {reverse: LintVariable | LintValue[]} - { filter: LintVariable | LintValue[]; func: LintExpression }; + | { map: MapTarget; func: MapEval; varb: string } + | { sort: MapTarget; func: MapEval; varb: string } + | { reverse: LintVariable | LintValue[] } + | { filter: MapTarget; func: LintExpression; varb: string } + | { speed: MapTarget }; -type LintReduce = Record< +type Aggs = | "sum" | "count" | "mean" @@ -74,9 +77,9 @@ type LintReduce = Record< | "mean" | "first" | "last" - | "extent", - LintVariable | any[] | LintMap ->; + | "extent" + | "std"; +type LintAggregate = Record; type LintColorFunction = | { cvdSim: LintVariable | LintColor; @@ -127,7 +130,7 @@ type LintValue = | LintVariable | LintMathOps | LintPairOps - | LintReduce + | LintAggregate | LintColorFunction; // raw values diff --git a/src/lib/linter.ts b/src/lib/linter.ts index 4b9f8fa5..503dc859 100644 --- a/src/lib/linter.ts +++ b/src/lib/linter.ts @@ -8,18 +8,18 @@ import DivergingOrder from "./lints/diverging-order"; import EvenDistribution from "./lints/even-distribution"; // custom lints +import Affects from "./lints/affects"; import AvoidExtremes from "./lints/avoid-extremes"; import BackgroundDifferentiability from "./lints/background-contrast"; import CatOrderSimilarity from "./lints/cat-order-similarity"; import ColorBlindness from "./lints/color-blindness"; -import SizeDiscrim from "./lints/size-discrim"; import Fair from "./lints/fair"; import Gamut from "./lints/in-gamut"; import MaxColors from "./lints/max-colors"; import MutuallyDistinct from "./lints/mutually-distinct"; import SequentialOrder from "./lints/sequential-order"; +import SizeDiscrim from "./lints/size-discrim"; import UglyColors from "./lints/ugly-colors"; -import Affects from "./lints/affects"; export const BUILT_INS: CustomLint[] = [ ...Affects, diff --git a/src/lib/lints/discriminative-power.ts b/src/lib/lints/discriminative-power.ts new file mode 100644 index 00000000..e0c947a9 --- /dev/null +++ b/src/lib/lints/discriminative-power.ts @@ -0,0 +1,25 @@ +import { JSONToPrettyString } from "../utils"; +import type { CustomLint } from "../CustomLint"; +import type { LintFixer } from "../linter-tools/lint-fixer"; + +const lint: CustomLint = { + name: "Discriminative Power Sufficient", + program: JSONToPrettyString({ + // @ts-ignore + $schema: `${location.href}lint-schema.json`, + "<": { + left: { + std: { speed: "colors" }, + }, + right: 0, + }, + }), + taskTypes: ["sequential", "diverging", "categorical"] as const, + level: "warning", + group: "design", + description: "Palette should have sufficient discriminative power. ", + failMessage: ``, + id: "discrim-power-built-in", + blameMode: "single", +}; +export default lint;