@kanzo-tech/graph 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +25 -7
  2. package/dist/core/detail.d.ts +2 -0
  3. package/dist/core/detail.d.ts.map +1 -1
  4. package/dist/core/detail.js +51 -30
  5. package/dist/core/detail.js.map +1 -1
  6. package/dist/core/state.d.ts +7 -0
  7. package/dist/core/state.d.ts.map +1 -1
  8. package/dist/core/store.d.ts.map +1 -1
  9. package/dist/core/store.js +112 -108
  10. package/dist/core/store.js.map +1 -1
  11. package/dist/core/types.d.ts +4 -4
  12. package/dist/core/types.d.ts.map +1 -1
  13. package/dist/index.d.ts +7 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +24 -18
  16. package/dist/index.js.map +1 -1
  17. package/dist/parts/graph-counts.d.ts +15 -0
  18. package/dist/parts/graph-counts.d.ts.map +1 -0
  19. package/dist/parts/graph-counts.js +41 -0
  20. package/dist/parts/graph-counts.js.map +1 -0
  21. package/dist/parts/graph-legend.d.ts +3 -2
  22. package/dist/parts/graph-legend.d.ts.map +1 -1
  23. package/dist/parts/graph-legend.js +30 -48
  24. package/dist/parts/graph-legend.js.map +1 -1
  25. package/dist/parts/graph-looks.d.ts +16 -0
  26. package/dist/parts/graph-looks.d.ts.map +1 -0
  27. package/dist/parts/graph-looks.js +138 -0
  28. package/dist/parts/graph-looks.js.map +1 -0
  29. package/dist/parts/graph-search.d.ts +17 -0
  30. package/dist/parts/graph-search.d.ts.map +1 -0
  31. package/dist/parts/graph-search.js +89 -0
  32. package/dist/parts/graph-search.js.map +1 -0
  33. package/dist/react/use-graph-prefs.d.ts +6 -2
  34. package/dist/react/use-graph-prefs.d.ts.map +1 -1
  35. package/dist/react/use-graph-prefs.js +7 -7
  36. package/dist/react/use-graph-prefs.js.map +1 -1
  37. package/dist/render/camera.d.ts +26 -0
  38. package/dist/render/camera.d.ts.map +1 -0
  39. package/dist/render/camera.js +40 -0
  40. package/dist/render/camera.js.map +1 -0
  41. package/dist/render/graph-looks.d.ts +10 -0
  42. package/dist/render/graph-looks.d.ts.map +1 -1
  43. package/dist/render/graph-looks.js +37 -20
  44. package/dist/render/graph-looks.js.map +1 -1
  45. package/dist/render/placement.d.ts +23 -0
  46. package/dist/render/placement.d.ts.map +1 -0
  47. package/dist/render/placement.js +23 -0
  48. package/dist/render/placement.js.map +1 -0
  49. package/dist/render/renderer.d.ts.map +1 -1
  50. package/dist/render/renderer.js +109 -100
  51. package/dist/render/renderer.js.map +1 -1
  52. package/dist/render/webgl.d.ts +5 -4
  53. package/dist/render/webgl.d.ts.map +1 -1
  54. package/dist/render/webgl.js +14 -11
  55. package/dist/render/webgl.js.map +1 -1
  56. package/package.json +7 -6
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-looks.js","sources":["../../src/parts/graph-looks.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Button,\n cn,\n Collapsible,\n CollapsibleContent,\n CollapsibleIndicator,\n CollapsibleTrigger,\n PreferencesFieldSet,\n PreferencesSections,\n RadioGroup,\n RadioGroupCard,\n SegmentGroup,\n Show,\n useKanzoTheme,\n} from \"@kanzo-tech/ui\";\nimport { useId } from \"react\";\nimport type { Channels } from \"../core/channels\";\nimport { useGraphPrefs } from \"../react/use-graph-prefs\";\nimport { lookFrom, PRESETS, type Look, type LookPreset } from \"../render/graph-looks\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { GRAPH_SECTION } from \"../section\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport type GraphLooksProps = React.ComponentProps<\"div\">;\n\nconst NAMESPACE = GRAPH_SECTION.namespace;\n\nconst CARDS: Record<LookPreset, { label: string; blurb: string; channels: Channels }> = {\n nebula: { label: \"Nebula\", blurb: \"Dense, dim points and additive links: a picture that reads as flow.\", channels: {} },\n atlas: {\n label: \"Atlas\",\n blurb: \"Map-steady points, links that just bow, generous labels.\",\n channels: { stroke: \"var(--muted-foreground)\" },\n },\n ink: {\n label: \"Ink\",\n blurb: \"Large, legible marks in one ink, identity on shape — the print-and-projector register.\",\n channels: { fill: \"var(--foreground)\", symbol: \"category\", stroke: \"var(--muted-foreground)\" },\n },\n};\n\n/** The axes under \"Customize\", after Marks; the forces are a live layout's and are not the picture. */\nconst CUSTOMIZE = [\"links\", \"labels\", \"additive-links\", \"bowed-links\", \"vignette\", \"grid\"] as const;\n\n/**\n * One small graph — the same seven vertices and eight edges on every card, so every pixel that differs\n * comes from the look. Glyphs are `ShapeGlyph` and colours `scaleOf`, the canvas's own, so a card\n * cannot promise a picture the canvas does not paint.\n */\nconst NODES = [\n { x: 22, y: 20, rank: 0, ramp: 1 },\n { x: 58, y: 8, rank: 1, ramp: 0.3 },\n { x: 58, y: 32, rank: 1, ramp: 0.3 },\n { x: 98, y: 20, rank: 2, ramp: 0.7 },\n { x: 140, y: 10, rank: 3, ramp: 0.45 },\n { x: 140, y: 31, rank: 2, ramp: 0.2 },\n { x: 184, y: 21, rank: 0, ramp: 0.85 },\n] as const;\n\nconst EDGES = [\n [0, 1],\n [0, 2],\n [1, 2],\n [0, 3],\n [3, 4],\n [3, 5],\n [4, 6],\n [5, 6],\n] as const;\n\n/** Screen pixels to the card's units, one factor for every look: Ink's larger floor is the look. */\nconst SCALE = 0.55;\n\nfunction LookPreview({ channels, look }: { channels: Channels; look: Look }) {\n const id = useId();\n const scale = scaleOf(channels);\n const radius = (ramp: number) => (look.size[0] + ramp * (look.size[1] - look.size[0])) * SCALE;\n return (\n <svg aria-hidden className=\"h-16 w-full rounded-[4px] border bg-background\" preserveAspectRatio=\"xMidYMid meet\" viewBox=\"0 0 208 40\">\n {EDGES.map(([from, to]) => {\n const a = NODES[from];\n const b = NODES[to];\n // cosmos.gl bows a link by a fraction of its length: the midpoint pushed along the perpendicular.\n const cx = (a.x + b.x) / 2 - (b.y - a.y) * look.link.curve;\n const cy = (a.y + b.y) / 2 + (b.x - a.x) * look.link.curve;\n return (\n <path\n d={`M${a.x} ${a.y}Q${cx} ${cy} ${b.x} ${b.y}`}\n fill=\"none\"\n key={`${from}-${to}`}\n stroke={channels.stroke ?? scale.color(a.rank)}\n strokeOpacity={look.link.opacity}\n strokeWidth={look.link.width}\n />\n );\n })}\n {NODES.map((node, i) => {\n const r = radius(node.ramp);\n return (\n <ShapeGlyph\n color={scale.color(node.rank)}\n height={r * 2}\n key={i}\n shape={scale.shape(node.rank)}\n width={r * 2}\n x={node.x - r}\n y={node.y - r}\n />\n );\n })}\n {look.vignette ? (\n <>\n <defs>\n <radialGradient id={id}>\n <stop offset=\"55%\" stopColor=\"var(--background)\" stopOpacity=\"0\" />\n <stop offset=\"100%\" stopColor=\"var(--background)\" stopOpacity=\"0.85\" />\n </radialGradient>\n </defs>\n <rect fill={`url(#${id})`} height=\"40\" width=\"208\" />\n </>\n ) : null}\n </svg>\n );\n}\n\n/**\n * **The graph's picture, as three looks and the axes under them** — GitHub's appearance settings:\n * pick by looking, then customise. The looks are `PRESETS` over `GRAPH_SECTION`'s axes; wearing one\n * writes every axis it names, and a look is checked exactly when the resolved axes are its own, so\n * there is no name stored beside them to go stale. \"Customize\" holds the axes themselves — Marks as\n * a segmented control, the rest as the section declares them — and opens on its own when the axes\n * are no preset.\n *\n * Under `KanzoThemeProvider` with `GRAPH_SECTION` among its `sections` — without it there is nothing\n * to write and it draws nothing, as `PreferencesSections` does for an unknown namespace. It needs no\n * `GraphRoot`, so it sits in a preferences panel as well as in a dock. A look whose axes a tenant pinned is disabled,\n * because wearing it would change nothing.\n */\nexport function GraphLooks({ className, slot, ...rest }: GraphLooksProps) {\n const { sectionPrefs, setSectionPref } = useKanzoTheme();\n const { preset } = useGraphPrefs();\n const prefs = sectionPrefs[NAMESPACE];\n if (!prefs) return null;\n const offered = (key: string) => prefs[key]?.offered === true;\n const marks = prefs.marks;\n const options = marks?.decl.kind === \"choice\" && Array.isArray(marks.decl.options) ? marks.decl.options : [];\n\n return (\n <div {...rest} className={cn(\"flex flex-col gap-4\", className)} data-slot={slot ?? \"graph-looks\"}>\n <PreferencesFieldSet label=\"Look\">\n <RadioGroup\n onValueChange={(details) => {\n const id = details.value as LookPreset | null;\n if (id) setSectionPref(NAMESPACE, PRESETS[id]);\n }}\n value={preset}\n >\n {(Object.keys(CARDS) as LookPreset[]).map((id) => (\n <RadioGroupCard\n className=\"flex-col gap-0 p-2\"\n disabled={!Object.keys(PRESETS[id]).every(offered)}\n key={id}\n value={id}\n >\n <LookPreview channels={CARDS[id].channels} look={lookFrom(PRESETS[id])} />\n <span className=\"mt-1.5 font-medium text-xs\">{CARDS[id].label}</span>\n <span className=\"mt-0.5 text-[10px] text-muted-foreground leading-relaxed\">{CARDS[id].blurb}</span>\n </RadioGroupCard>\n ))}\n </RadioGroup>\n </PreferencesFieldSet>\n\n <Collapsible defaultOpen={preset === null}>\n <CollapsibleTrigger asChild>\n <Button className=\"w-full\" size=\"sm\" variant=\"ghost\">\n Customize\n <Show when={preset === null}>\n <span className=\"ms-auto me-1 font-normal text-muted-foreground text-xs\">Custom</span>\n </Show>\n <CollapsibleIndicator />\n </Button>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"flex flex-col gap-4 pt-3\">\n <Show when={offered(\"marks\")}>\n <PreferencesFieldSet label={marks?.decl.label ?? \"Marks\"}>\n <SegmentGroup\n onValueChange={(details) => details.value && setSectionPref(NAMESPACE, { marks: details.value })}\n options={options}\n size=\"sm\"\n value={marks?.value ?? null}\n variant=\"solid\"\n />\n </PreferencesFieldSet>\n </Show>\n <PreferencesSections namespace={NAMESPACE} only={CUSTOMIZE} />\n </CollapsibleContent>\n </Collapsible>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AA2BA;AAEwF;AAC8B;AAC7G;AACE;AACA;AACa;AAA0B;AAE3C;AACI;AACA;AAC4D;AAEvE;AAUc;AACmB;AACD;AACC;AACA;AACC;AACA;AAElC;AAEc;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AAEP;AAKA;AACE;AAGA;AAEK;AACC;AAKA;AACE;AAAC;AAAA;AAC4C;AACtC;AAEwC;AACpB;AACF;AAAA;AAHL;AAAA;AAMvB;AAEC;AACA;AACE;AAAC;AAAA;AAC6B;AAChB;AAEgB;AACjB;AACC;AACA;AAAA;AAJP;AAAA;AAOV;AAGG;AAEI;AAAiE;AACI;AAEzE;AACmD;AAEnD;AAGV;AAeO;AACL;AAGA;AACA;;AAAiC;AAAwB;AAIzD;AAEI;AACE;AAAC;AAAA;AAEG;AACA;AAA6C;AAC/C;AACO;AAGL;AAAC;AAAA;AACW;AACuC;AAE1C;AAEP;AAAwE;AACV;AAC8B;AAAA;AAAA;AALvF;AAOR;AAAA;AAEL;AAGE;AACuD;AAAA;AAInD;AACsB;AAE1B;AAEE;AAEI;AAAC;AAAA;AACgG;AAC/F;AACK;AACkB;AACf;AAAA;AAGd;AAC4D;AAC9D;AACF;AAGN;;;;"}
@@ -0,0 +1,17 @@
1
+ import { ComboboxInput } from '@kanzo-tech/ui';
2
+ export interface GraphSearchProps extends Pick<React.ComponentProps<typeof ComboboxInput>, "className" | "placeholder" | "size"> {
3
+ /** How many matches the list shows; past it, the list says to keep typing. */
4
+ limit?: number;
5
+ }
6
+ /**
7
+ * **Find a vertex by its text and go to it.** Every drawn vertex's `title` — or its table's
8
+ * `identity` — is read once, then filtered in the browser as the reader types: no query per
9
+ * keystroke. The list is ordered by the `r` ramp, so with `r="degree"` the hubs come first, and each
10
+ * match is named by the root's `categories`. Picking one is `reveal`: the canvas frames it and
11
+ * selects it with its neighbours, and `GraphInspector` reads it.
12
+ *
13
+ * A vertex the page's filter hides is not offered, because there is nothing on the canvas to go to.
14
+ * A read that fails is handed to `onFailure` whole and the input says so, rather than staying disabled.
15
+ */
16
+ export declare function GraphSearch({ className, limit, placeholder, size }: GraphSearchProps): import("react").JSX.Element;
17
+ //# sourceMappingURL=graph-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-search.d.ts","sourceRoot":"","sources":["../../src/parts/graph-search.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,aAAa,EAMd,MAAM,gBAAgB,CAAC;AAUxB,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,aAAa,CAAC,EAAE,WAAW,GAAG,aAAa,GAAG,MAAM,CAAC;IAC9H,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmCD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,KAAU,EAAE,WAA4B,EAAE,IAAW,EAAE,EAAE,gBAAgB,+BAkFjH"}
@@ -0,0 +1,89 @@
1
+ "use client";
2
+ import { jsxs as h, jsx as r } from "react/jsx-runtime";
3
+ import { useFilter as z, createListCollection as A, Combobox as O, ComboboxInput as q, InputGroupAddon as K, ComboboxContent as L, ComboboxEmpty as M, ComboboxItem as Q, Show as E } from "@kanzo-tech/ui";
4
+ import { SearchIcon as R } from "lucide-react";
5
+ import { useState as T, useEffect as $, useMemo as V } from "react";
6
+ import { nameOf as B } from "../core/categories.js";
7
+ import { bindingOf as D } from "../core/channels.js";
8
+ import { readEveryTitle as H } from "../core/detail.js";
9
+ import { useGraphContext as J } from "../react/graph-root.js";
10
+ import { useGraphState as N, useGraphSnapshot as v } from "../react/use-graph-state.js";
11
+ function P(m, s, d, f, a) {
12
+ const i = [];
13
+ for (let t = 0; t < m.size; t++)
14
+ (!d || d[t]) && !Number.isNaN(m.positions[t * 2]) && i.push(t);
15
+ const { sizes: l } = s;
16
+ if (l) {
17
+ const t = (o) => Number.isNaN(l[o]) ? -1 / 0 : l[o];
18
+ i.sort((o, c) => t(c) - t(o) || o - c);
19
+ }
20
+ return i.map((t) => ({
21
+ value: String(t),
22
+ label: f[t] || `#${t}`,
23
+ kind: a ? a(s.ranks[t]) : null
24
+ }));
25
+ }
26
+ function oe({ className: m, limit: s = 50, placeholder: d = "Find a node…", size: f = "sm" }) {
27
+ const a = J(), i = N((e) => e.corpus), l = N((e) => e.options), t = v((e) => e.geometry), o = v((e) => e.encoding), c = v((e) => e.mask), w = N((e) => {
28
+ var n;
29
+ return ((n = e.drawn) == null ? void 0 : n.domain) ?? e.domain;
30
+ }), [p, S] = T(null), [b, j] = T(""), { contains: k } = z({ sensitivity: "base" }), { title: u, categories: I } = l;
31
+ $(() => {
32
+ if (!i || !t) return;
33
+ const e = new AbortController();
34
+ return H(i, t, u, e.signal).then(
35
+ (n) => !e.signal.aborted && S({ geometry: t, title: u, titles: n }),
36
+ (n) => {
37
+ e.signal.aborted || (S({ geometry: t, title: u, titles: !1 }), a.getState().options.onFailure(n));
38
+ }
39
+ ), () => e.abort();
40
+ }, [a, i, t, u]);
41
+ const g = p && p.geometry === t && p.title === u ? p.titles : null, x = g || null, G = D(l), F = G.byTable || G.category !== void 0, y = V(() => !t || !o || !x ? null : P(t, o, c, x, F ? (e) => B(w[e], I) : null), [t, o, c, x, F, w, I]), C = V(() => {
42
+ const e = [];
43
+ for (const n of y ?? []) {
44
+ if (e.length === s) break;
45
+ (!b || k(n.label, b)) && e.push(n);
46
+ }
47
+ return A({ items: e });
48
+ }, [y, b, k, s]);
49
+ return /* @__PURE__ */ h(
50
+ O,
51
+ {
52
+ collection: C,
53
+ disabled: y === null,
54
+ onInputValueChange: (e) => j(e.inputValue),
55
+ onValueChange: (e) => {
56
+ const n = e.value[0];
57
+ n !== void 0 && a.reveal(Number(n));
58
+ },
59
+ children: [
60
+ /* @__PURE__ */ r(
61
+ q,
62
+ {
63
+ "aria-invalid": g === !1 || void 0,
64
+ className: m,
65
+ placeholder: g === !1 ? "The names could not be read." : d,
66
+ size: f,
67
+ children: /* @__PURE__ */ r(K, { align: "inline-start", children: /* @__PURE__ */ r(R, {}) })
68
+ }
69
+ ),
70
+ /* @__PURE__ */ h(L, { children: [
71
+ /* @__PURE__ */ r(M, { children: "Nothing drawn by that name." }),
72
+ C.items.map((e) => /* @__PURE__ */ h(Q, { item: e, children: [
73
+ /* @__PURE__ */ r("span", { className: "min-w-0 truncate", children: e.label }),
74
+ /* @__PURE__ */ r(E, { when: e.kind !== null, children: /* @__PURE__ */ r("span", { className: "ms-auto ps-2 text-muted-foreground text-xs", children: e.kind }) })
75
+ ] }, e.value)),
76
+ /* @__PURE__ */ r(E, { when: C.items.length === s, children: /* @__PURE__ */ h("p", { className: "border-t px-2 py-1.5 text-muted-foreground text-xs", children: [
77
+ "First ",
78
+ s,
79
+ ". Keep typing to narrow it."
80
+ ] }) })
81
+ ] })
82
+ ]
83
+ }
84
+ );
85
+ }
86
+ export {
87
+ oe as GraphSearch
88
+ };
89
+ //# sourceMappingURL=graph-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-search.js","sources":["../../src/parts/graph-search.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Combobox,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n createListCollection,\n InputGroupAddon,\n Show,\n useFilter,\n} from \"@kanzo-tech/ui\";\nimport { SearchIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { readEveryTitle } from \"../core/detail\";\nimport type { Encoding, Geometry } from \"../core/load\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphSearchProps extends Pick<React.ComponentProps<typeof ComboboxInput>, \"className\" | \"placeholder\" | \"size\"> {\n /** How many matches the list shows; past it, the list says to keep typing. */\n limit?: number;\n}\n\ninterface Entry {\n value: string;\n label: string;\n kind: string | null;\n}\n\n/** The titles read for one geometry and title column, or `false` when the read failed. */\ntype Titles = { geometry: Geometry; title: string | undefined; titles: string[] | false } | null;\n\n/** Every drawn vertex, biggest on the `r` ramp first — the vertices a reader most likely means. */\nfunction entriesOf(\n geometry: Geometry,\n encoding: Encoding,\n mask: Uint8Array | null,\n titles: readonly string[],\n kindOf: ((rank: number) => string) | null,\n): Entry[] {\n const ids: number[] = [];\n for (let id = 0; id < geometry.size; id++) {\n if ((!mask || mask[id]) && !Number.isNaN(geometry.positions[id * 2])) ids.push(id);\n }\n const { sizes } = encoding;\n if (sizes) {\n const ramp = (id: number) => (Number.isNaN(sizes[id]) ? -Infinity : (sizes[id] as number));\n ids.sort((a, b) => ramp(b) - ramp(a) || a - b);\n }\n return ids.map((id) => ({\n value: String(id),\n label: titles[id] || `#${id}`,\n kind: kindOf ? kindOf(encoding.ranks[id] as number) : null,\n }));\n}\n\n/**\n * **Find a vertex by its text and go to it.** Every drawn vertex's `title` — or its table's\n * `identity` — is read once, then filtered in the browser as the reader types: no query per\n * keystroke. The list is ordered by the `r` ramp, so with `r=\"degree\"` the hubs come first, and each\n * match is named by the root's `categories`. Picking one is `reveal`: the canvas frames it and\n * selects it with its neighbours, and `GraphInspector` reads it.\n *\n * A vertex the page's filter hides is not offered, because there is nothing on the canvas to go to.\n * A read that fails is handed to `onFailure` whole and the input says so, rather than staying disabled.\n */\nexport function GraphSearch({ className, limit = 50, placeholder = \"Find a node…\", size = \"sm\" }: GraphSearchProps) {\n const api = useGraphContext();\n const corpus = useGraphState((s) => s.corpus);\n const options = useGraphState((s) => s.options);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const mask = useGraphSnapshot((s) => s.mask);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const [read, setRead] = useState<Titles>(null);\n const [query, setQuery] = useState(\"\");\n const { contains } = useFilter({ sensitivity: \"base\" });\n const { title, categories } = options;\n\n useEffect(() => {\n if (!corpus || !geometry) return;\n const aborter = new AbortController();\n readEveryTitle(corpus, geometry, title, aborter.signal).then(\n (titles) => !aborter.signal.aborted && setRead({ geometry, title, titles }),\n (error: unknown) => {\n if (aborter.signal.aborted) return;\n setRead({ geometry, title, titles: false });\n api.getState().options.onFailure(error);\n },\n );\n return () => aborter.abort();\n }, [api, corpus, geometry, title]);\n\n const answered = read && read.geometry === geometry && read.title === title ? read.titles : null;\n const titles = answered || null;\n const binding = bindingOf(options);\n const bound = binding.byTable || binding.category !== undefined;\n\n const entries = useMemo(() => {\n if (!geometry || !encoding || !titles) return null;\n return entriesOf(geometry, encoding, mask, titles, bound ? (rank) => nameOf(domain[rank], categories) : null);\n }, [geometry, encoding, mask, titles, bound, domain, categories]);\n\n const collection = useMemo(() => {\n const matches: Entry[] = [];\n for (const entry of entries ?? []) {\n if (matches.length === limit) break;\n if (!query || contains(entry.label, query)) matches.push(entry);\n }\n return createListCollection({ items: matches });\n }, [entries, query, contains, limit]);\n\n return (\n <Combobox\n collection={collection}\n disabled={entries === null}\n onInputValueChange={(details) => setQuery(details.inputValue)}\n onValueChange={(details) => {\n const picked = details.value[0];\n if (picked !== undefined) api.reveal(Number(picked));\n }}\n >\n <ComboboxInput\n aria-invalid={answered === false || undefined}\n className={className}\n placeholder={answered === false ? \"The names could not be read.\" : placeholder}\n size={size}\n >\n <InputGroupAddon align=\"inline-start\">\n <SearchIcon />\n </InputGroupAddon>\n </ComboboxInput>\n <ComboboxContent>\n <ComboboxEmpty>Nothing drawn by that name.</ComboboxEmpty>\n {collection.items.map((item) => (\n <ComboboxItem item={item} key={item.value}>\n <span className=\"min-w-0 truncate\">{item.label}</span>\n <Show when={item.kind !== null}>\n <span className=\"ms-auto ps-2 text-muted-foreground text-xs\">{item.kind}</span>\n </Show>\n </ComboboxItem>\n ))}\n <Show when={collection.items.length === limit}>\n <p className=\"border-t px-2 py-1.5 text-muted-foreground text-xs\">First {limit}. Keep typing to narrow it.</p>\n </Show>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;AAqCA;AAOE;AACA;AACE;AAEF;AACA;AACE;AACA;AAA6C;AAE/C;AAAwB;AACN;AACW;AAC2B;AAE1D;AAYO;AACL;;AAMoC;AAAqB;AAMzD;AACE;AACA;AACA;AAAwD;AACoB;AAExE;AAEsC;AACxC;AAEmB;AAGvB;AAWE;AACA;AACE;AACA;AAA8D;AAEhE;AAA8C;AAGhD;AACE;AAAC;AAAA;AACC;AACsB;AACsC;AAE1D;AACA;AAAmD;AACrD;AAEA;AAAA;AAAC;AAAA;AACqC;AACpC;AACmE;AACnE;AAIA;AAAA;AAAA;AAGA;AAA0C;AAGtC;AAA+C;AAG/C;AAEH;AAEmE;AAAA;AAAO;AAAM;AACjF;AACF;AAAA;AAAA;AAGN;;;;"}
@@ -1,4 +1,4 @@
1
- import { Look } from '../render/graph-looks';
1
+ import { Look, LookPreset } from '../render/graph-looks';
2
2
  import { Sim } from '../render/graph-sim';
3
3
  /**
4
4
  * The graph section's resolved preferences, as the two objects `useGraph` takes.
@@ -9,11 +9,15 @@ import { Sim } from '../render/graph-sim';
9
9
  * the keys, the defaults and the bounds are all `GRAPH_SECTION`'s, and the resolution chain is the
10
10
  * provider's. What reaches `lookFrom`/`simFrom` is the resolved value, never the stored one.
11
11
  *
12
+ * `preset` is the named look the axes are, for a host that pairs channels with it — Ink spends
13
+ * identity on shape — and `null` once a reader has customised past all three.
14
+ *
12
15
  * Needs `KanzoThemeProvider` with `GRAPH_SECTION` among its `sections`. Without it the namespace
13
- * resolves to nothing and both objects are the manifest's defaults.
16
+ * resolves to nothing, both objects are the manifest's defaults and the preset is Atlas.
14
17
  */
15
18
  export declare function useGraphPrefs(): {
16
19
  look: Look;
17
20
  sim: Sim;
21
+ preset: LookPreset | null;
18
22
  };
19
23
  //# sourceMappingURL=use-graph-prefs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-prefs.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-prefs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAY,KAAK,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAW,KAAK,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAGxD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CASxD"}
1
+ {"version":3,"file":"use-graph-prefs.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-prefs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,IAAI,EAAE,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,EAAW,KAAK,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAGxD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,IAAI;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;CAAE,CASnF"}
@@ -1,17 +1,17 @@
1
1
  "use client";
2
2
  import { useMemo as t } from "react";
3
3
  import { useKanzoTheme as s } from "@kanzo-tech/ui";
4
- import { lookFrom as i } from "../render/graph-looks.js";
5
- import { simFrom as n } from "../render/graph-sim.js";
4
+ import { presetOf as i, lookFrom as n } from "../render/graph-looks.js";
5
+ import { simFrom as p } from "../render/graph-sim.js";
6
6
  import { GRAPH_SECTION as f } from "../section.js";
7
7
  function v() {
8
- const o = s().sectionPrefs[f.namespace];
8
+ const r = s().sectionPrefs[f.namespace];
9
9
  return t(() => {
10
- const r = Object.fromEntries(
11
- Object.entries(o ?? {}).map(([e, m]) => [e, m.value])
10
+ const e = Object.fromEntries(
11
+ Object.entries(r ?? {}).map(([o, m]) => [o, m.value])
12
12
  );
13
- return { look: i(r), sim: n(r) };
14
- }, [o]);
13
+ return { look: n(e), sim: p(e), preset: i(e) };
14
+ }, [r]);
15
15
  }
16
16
  export {
17
17
  v as useGraphPrefs
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-prefs.js","sources":["../../src/react/use-graph-prefs.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useKanzoTheme } from \"@kanzo-tech/ui\";\nimport { lookFrom, type Look } from \"../render/graph-looks\";\nimport { simFrom, type Sim } from \"../render/graph-sim\";\nimport { GRAPH_SECTION } from \"../section\";\n\n/**\n * The graph section's resolved preferences, as the two objects `useGraph` takes.\n *\n * The join between the provider's `sectionPrefs` and the two readers of this package's manifest.\n * Every host that drew a graph under a preferences panel wrote it — the docs' workspace and keasy's\n * discover page, the same eight lines each — and it knows nothing a host decides: the namespace,\n * the keys, the defaults and the bounds are all `GRAPH_SECTION`'s, and the resolution chain is the\n * provider's. What reaches `lookFrom`/`simFrom` is the resolved value, never the stored one.\n *\n * Needs `KanzoThemeProvider` with `GRAPH_SECTION` among its `sections`. Without it the namespace\n * resolves to nothing and both objects are the manifest's defaults.\n */\nexport function useGraphPrefs(): { look: Look; sim: Sim } {\n const resolved = useKanzoTheme().sectionPrefs[GRAPH_SECTION.namespace];\n\n return useMemo(() => {\n const values = Object.fromEntries(\n Object.entries(resolved ?? {}).map(([key, pref]) => [key, pref.value]),\n );\n return { look: lookFrom(values), sim: simFrom(values) };\n }, [resolved]);\n}\n"],"names":[],"mappings":";;;;;;AAoBO;AACL;AAEA;AACE;AAAsB;AACiD;AAEvE;AAAoD;AAExD;;;;"}
1
+ {"version":3,"file":"use-graph-prefs.js","sources":["../../src/react/use-graph-prefs.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useKanzoTheme } from \"@kanzo-tech/ui\";\nimport { lookFrom, presetOf, type Look, type LookPreset } from \"../render/graph-looks\";\nimport { simFrom, type Sim } from \"../render/graph-sim\";\nimport { GRAPH_SECTION } from \"../section\";\n\n/**\n * The graph section's resolved preferences, as the two objects `useGraph` takes.\n *\n * The join between the provider's `sectionPrefs` and the two readers of this package's manifest.\n * Every host that drew a graph under a preferences panel wrote it — the docs' workspace and keasy's\n * discover page, the same eight lines each — and it knows nothing a host decides: the namespace,\n * the keys, the defaults and the bounds are all `GRAPH_SECTION`'s, and the resolution chain is the\n * provider's. What reaches `lookFrom`/`simFrom` is the resolved value, never the stored one.\n *\n * `preset` is the named look the axes are, for a host that pairs channels with it — Ink spends\n * identity on shape — and `null` once a reader has customised past all three.\n *\n * Needs `KanzoThemeProvider` with `GRAPH_SECTION` among its `sections`. Without it the namespace\n * resolves to nothing, both objects are the manifest's defaults and the preset is Atlas.\n */\nexport function useGraphPrefs(): { look: Look; sim: Sim; preset: LookPreset | null } {\n const resolved = useKanzoTheme().sectionPrefs[GRAPH_SECTION.namespace];\n\n return useMemo(() => {\n const values = Object.fromEntries(\n Object.entries(resolved ?? {}).map(([key, pref]) => [key, pref.value]),\n );\n return { look: lookFrom(values), sim: simFrom(values), preset: presetOf(values) };\n }, [resolved]);\n}\n"],"names":[],"mappings":";;;;;;AAuBO;AACL;AAEA;AACE;AAAsB;AACiD;AAEvE;AAA8E;AAElF;;;;"}
@@ -0,0 +1,26 @@
1
+ import { Graph } from '@cosmos.gl/graph';
2
+ export declare const FIT_DURATION = 420;
3
+ export interface Camera {
4
+ /** A corpus was placed: frame these corners now, and again whenever the canvas changes size. */
5
+ frame(corners: readonly number[]): void;
6
+ /** The reader asked for the whole graph: frame it, and keep it framed through a resize. */
7
+ fit(): void;
8
+ /** A layout started moving the points: the corners are history, and the camera follows. */
9
+ run(): void;
10
+ tick(): void;
11
+ settle(): void;
12
+ destroy(): void;
13
+ }
14
+ /**
15
+ * **Who holds the camera, and when it gives it back.** The camera frames the corpus once it is
16
+ * placed and keeps it framed — through cosmos.gl's device coming up and every resize of the canvas,
17
+ * since a host's split panel settles its width after mount and a frame taken at the old size opens
18
+ * zoomed into a corner — and while a layout runs it follows the moving points and frames them when
19
+ * they settle: Cosmograph's fit-on-settle, kept up. All of it stops the moment the reader zooms or
20
+ * pans, and comes back with Fit or the next run.
21
+ */
22
+ export declare function createCamera(graph: Graph, host: HTMLElement): {
23
+ camera: Camera;
24
+ taken: (userDriven: boolean) => void;
25
+ };
26
+ //# sourceMappingURL=camera.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../../src/render/camera.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,eAAO,MAAM,YAAY,MAAM,CAAC;AAMhC,MAAM,WAAW,MAAM;IACrB,gGAAgG;IAChG,KAAK,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACxC,2FAA2F;IAC3F,GAAG,IAAI,IAAI,CAAC;IACZ,2FAA2F;IAC3F,GAAG,IAAI,IAAI,CAAC;IACZ,IAAI,IAAI,IAAI,CAAC;IACb,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CA6DtH"}
@@ -0,0 +1,40 @@
1
+ const O = 420;
2
+ function F(o, a) {
3
+ let f = null, r = !1, n = !1, l = 0, i = 0;
4
+ const c = (e) => {
5
+ f ? o.fitViewByPointPositions([...f], e, 0.18) : o.fitView(e, 0.18);
6
+ }, s = () => {
7
+ i = 0, r && !n && c(0);
8
+ }, t = typeof ResizeObserver > "u" ? null : new ResizeObserver(() => {
9
+ i || (i = requestAnimationFrame(s));
10
+ });
11
+ return t == null || t.observe(a), o.ready.then(s, () => {
12
+ }), { camera: {
13
+ frame(e) {
14
+ f = e, r = !0, c(0);
15
+ },
16
+ fit() {
17
+ r = !0, c(420);
18
+ },
19
+ run() {
20
+ f = null, n = !0, l = performance.now();
21
+ },
22
+ tick() {
23
+ const e = performance.now();
24
+ !n || e - l < 900 || (l = e, o.fitView(700, 0.18));
25
+ },
26
+ settle() {
27
+ n && o.fitView(420, 0.18), n = !1;
28
+ },
29
+ destroy() {
30
+ t == null || t.disconnect(), i && cancelAnimationFrame(i);
31
+ }
32
+ }, taken: (e) => {
33
+ e && (r = !1, n = !1);
34
+ } };
35
+ }
36
+ export {
37
+ O as FIT_DURATION,
38
+ F as createCamera
39
+ };
40
+ //# sourceMappingURL=camera.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera.js","sources":["../../src/render/camera.ts"],"sourcesContent":["import type { Graph } from \"@cosmos.gl/graph\";\n\nexport const FIT_DURATION = 420;\nconst FIT_PADDING = 0.18;\n/** How often a running layout re-frames the camera, and how long each re-frame glides. */\nconst FOLLOW_EVERY = 900;\nconst FOLLOW_DURATION = 700;\n\nexport interface Camera {\n /** A corpus was placed: frame these corners now, and again whenever the canvas changes size. */\n frame(corners: readonly number[]): void;\n /** The reader asked for the whole graph: frame it, and keep it framed through a resize. */\n fit(): void;\n /** A layout started moving the points: the corners are history, and the camera follows. */\n run(): void;\n tick(): void;\n settle(): void;\n destroy(): void;\n}\n\n/**\n * **Who holds the camera, and when it gives it back.** The camera frames the corpus once it is\n * placed and keeps it framed — through cosmos.gl's device coming up and every resize of the canvas,\n * since a host's split panel settles its width after mount and a frame taken at the old size opens\n * zoomed into a corner — and while a layout runs it follows the moving points and frames them when\n * they settle: Cosmograph's fit-on-settle, kept up. All of it stops the moment the reader zooms or\n * pans, and comes back with Fit or the next run.\n */\nexport function createCamera(graph: Graph, host: HTMLElement): { camera: Camera; taken: (userDriven: boolean) => void } {\n let corners: readonly number[] | null = null;\n let framing = false;\n let following = false;\n let followed = 0;\n let pending = 0;\n\n const fitNow = (duration: number) => {\n if (corners) graph.fitViewByPointPositions([...corners], duration, FIT_PADDING);\n else graph.fitView(duration, FIT_PADDING);\n };\n const reframe = () => {\n pending = 0;\n if (framing && !following) fitNow(0);\n };\n // After cosmos.gl's own observer has resized the canvas, so the frame is taken at the new size.\n const resizes =\n typeof ResizeObserver === \"undefined\"\n ? null\n : new ResizeObserver(() => {\n if (!pending) pending = requestAnimationFrame(reframe);\n });\n resizes?.observe(host);\n void graph.ready.then(reframe, () => {});\n\n const camera: Camera = {\n frame(next) {\n corners = next;\n framing = true;\n fitNow(0);\n },\n fit() {\n framing = true;\n fitNow(FIT_DURATION);\n },\n run() {\n corners = null;\n following = true;\n followed = performance.now();\n },\n tick() {\n const now = performance.now();\n if (!following || now - followed < FOLLOW_EVERY) return;\n followed = now;\n graph.fitView(FOLLOW_DURATION, FIT_PADDING);\n },\n settle() {\n if (following) graph.fitView(FIT_DURATION, FIT_PADDING);\n following = false;\n },\n destroy() {\n resizes?.disconnect();\n if (pending) cancelAnimationFrame(pending);\n },\n };\n const taken = (userDriven: boolean) => {\n if (!userDriven) return;\n framing = false;\n following = false;\n };\n return { camera, taken };\n}\n"],"names":["FIT_DURATION","createCamera","graph","host","corners","framing","following","followed","pending","fitNow","duration","reframe","resizes","next","now","userDriven"],"mappings":"AAEO,MAAMA,IAAe;AA0BrB,SAASC,EAAaC,GAAcC,GAA6E;AACtH,MAAIC,IAAoC,MACpCC,IAAU,IACVC,IAAY,IACZC,IAAW,GACXC,IAAU;AAEd,QAAMC,IAAS,CAACC,MAAqB;AACnC,IAAIN,MAAe,wBAAwB,CAAC,GAAGA,CAAO,GAAGM,GAAU,IAAW,IACzER,EAAM,QAAQQ,GAAU,IAAW;AAAA,EAC1C,GACMC,IAAU,MAAM;AACpB,IAAAH,IAAU,GACNH,KAAW,CAACC,KAAWG,EAAO,CAAC;AAAA,EACrC,GAEMG,IACJ,OAAO,iBAAmB,MACtB,OACA,IAAI,eAAe,MAAM;AACvB,IAAKJ,MAASA,IAAU,sBAAsBG,CAAO;AAAA,EACvD,CAAC;AACP,SAAAC,KAAA,QAAAA,EAAS,QAAQT,IACZD,EAAM,MAAM,KAAKS,GAAS,MAAM;AAAA,EAAC,CAAC,GAqChC,EAAE,QAnCc;AAAA,IACrB,MAAME,GAAM;AACV,MAAAT,IAAUS,GACVR,IAAU,IACVI,EAAO,CAAC;AAAA,IACV;AAAA,IACA,MAAM;AACJ,MAAAJ,IAAU,IACVI,EAAO,GAAY;AAAA,IACrB;AAAA,IACA,MAAM;AACJ,MAAAL,IAAU,MACVE,IAAY,IACZC,IAAW,YAAY,IAAA;AAAA,IACzB;AAAA,IACA,OAAO;AACL,YAAMO,IAAM,YAAY,IAAA;AACxB,MAAI,CAACR,KAAaQ,IAAMP,IAAW,QACnCA,IAAWO,GACXZ,EAAM,QAAQ,KAAiB,IAAW;AAAA,IAC5C;AAAA,IACA,SAAS;AACP,MAAII,KAAWJ,EAAM,QAAQ,KAAc,IAAW,GACtDI,IAAY;AAAA,IACd;AAAA,IACA,UAAU;AACR,MAAAM,KAAA,QAAAA,EAAS,cACLJ,0BAA8BA,CAAO;AAAA,IAC3C;AAAA,EAAA,GAOe,OALH,CAACO,MAAwB;AACrC,IAAKA,MACLV,IAAU,IACVC,IAAY;AAAA,EACd,EACiB;AACnB;"}
@@ -149,6 +149,16 @@ export interface Look {
149
149
  * default. Those defaults are what a graph drew before any of this existed.
150
150
  */
151
151
  export declare function lookFrom(values?: Readonly<Record<string, string | undefined>>): Look;
152
+ /** The three named looks. */
153
+ export type LookPreset = "nebula" | "atlas" | "ink";
154
+ /**
155
+ * **The three looks as values of the section's axes**, in the order a picker offers them. Each writes
156
+ * every axis any of them names, so wearing one leaves nothing of the last, and a look is worn exactly
157
+ * when the resolved axes equal its own — no name is stored beside them. Atlas is every default.
158
+ */
159
+ export declare const PRESETS: Record<LookPreset, Readonly<Record<string, string>>>;
160
+ /** The look the axes are — a missing one is the section's default — or `null` once customised past all three. */
161
+ export declare function presetOf(values: Readonly<Record<string, string | undefined>>): LookPreset | null;
152
162
  /**
153
163
  * What a canvas draws when nobody has chosen anything.
154
164
  *
@@ -1 +1 @@
1
- {"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/graph-looks.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,EAAgD,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAe,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvB,IAAI,EAAE;QACJ;;;;;WAKG;QACH,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;;;;;WAUG;QACH,KAAK,EAAE,OAAO,CAAC;QACf;;;;;;WAMG;QACH,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACxB,CAAC;IACF,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;CAsBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,IAAI,CA0BxF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,IAAiB,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAQ5C,CAAC"}
1
+ {"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/graph-looks.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,EAAgD,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAe,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvB,IAAI,EAAE;QACJ;;;;;WAKG;QACH,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;;;;;WAUG;QACH,KAAK,EAAE,OAAO,CAAC;QACf;;;;;;WAMG;QACH,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACxB,CAAC;IACF,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;CAsBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,IAAI,CA0BxF;AAED,6BAA6B;AAC7B,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAIxE,CAAC;AAEF,iHAAiH;AACjH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,UAAU,GAAG,IAAI,CAMhG;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,IAAiB,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAQ5C,CAAC"}
@@ -1,21 +1,22 @@
1
- const a = {
1
+ import { GRAPH_SECTION as d } from "../section.js";
2
+ const f = {
2
3
  circle: 0,
3
4
  square: 1,
4
5
  triangle: 2,
5
6
  diamond: 3,
6
7
  cross: 7
7
- }, d = ["circle", "square", "triangle", "diamond"], u = "cross";
8
+ }, k = ["circle", "square", "triangle", "diamond"], b = "cross";
8
9
  function c(e = {}) {
9
- const n = (o, s) => {
10
- const l = e[o];
11
- return l === void 0 ? s : l === "true";
12
- }, r = e.marks === "legible", t = Number.parseFloat(e.labels ?? "");
10
+ const n = (i, l) => {
11
+ const o = e[i];
12
+ return o === void 0 ? l : o === "true";
13
+ }, t = e.marks === "legible", s = Number.parseFloat(e.labels ?? "");
13
14
  return {
14
- size: r ? [4, 13] : [2, 8],
15
+ size: t ? [4, 13] : [2, 8],
15
16
  link: {
16
17
  render: n("links", !0),
17
- opacity: r ? 0.28 : 0.42,
18
- width: r ? 0.5 : 0.6,
18
+ opacity: t ? 0.28 : 0.42,
19
+ width: t ? 0.5 : 0.6,
19
20
  // A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default
20
21
  // of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two
21
22
  // pictures — straight, and told apart — not a number to tune.
@@ -25,16 +26,30 @@ function c(e = {}) {
25
26
  // definition of a field nobody chose.
26
27
  fade: [200, 1400]
27
28
  },
28
- labels: Number.isFinite(t) ? t : 26,
29
+ labels: Number.isFinite(s) ? s : 26,
29
30
  vignette: n("vignette", !1),
30
31
  grid: n("grid", !0)
31
32
  };
32
33
  }
33
- const i = c();
34
- function k(e) {
35
- return e ? { ...i, ...e, link: { ...i.link, ...e.link } } : i;
34
+ const a = {
35
+ nebula: { marks: "dense", "additive-links": "true", "bowed-links": "false", labels: "14", vignette: "true" },
36
+ atlas: { marks: "dense", "additive-links": "false", "bowed-links": "true", labels: "26", vignette: "false" },
37
+ ink: { marks: "legible", "additive-links": "false", "bowed-links": "false", labels: "40", vignette: "false" }
38
+ };
39
+ function v(e) {
40
+ const n = (s) => {
41
+ var i, l;
42
+ return e[s] ?? ((l = (i = d.prefs) == null ? void 0 : i[s]) == null ? void 0 : l.default);
43
+ };
44
+ return Object.keys(a).find(
45
+ (s) => Object.entries(a[s]).every(([i, l]) => n(i) === l)
46
+ ) ?? null;
47
+ }
48
+ const r = c();
49
+ function g(e) {
50
+ return e ? { ...r, ...e, link: { ...r.link, ...e.link } } : r;
36
51
  }
37
- const v = {
52
+ const m = {
38
53
  circle: "M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z",
39
54
  square: "M2 2h8v8H2Z",
40
55
  triangle: "M6 1.6 10.6 10H1.4Z",
@@ -44,12 +59,14 @@ const v = {
44
59
  cross: "M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z"
45
60
  };
46
61
  export {
47
- i as DEFAULT_LOOK,
48
- a as SHAPE_INDEX,
49
- d as SHAPE_ORDER,
50
- u as SHAPE_OTHER,
51
- v as SHAPE_PATH,
62
+ r as DEFAULT_LOOK,
63
+ a as PRESETS,
64
+ f as SHAPE_INDEX,
65
+ k as SHAPE_ORDER,
66
+ b as SHAPE_OTHER,
67
+ m as SHAPE_PATH,
52
68
  c as lookFrom,
53
- k as resolveLook
69
+ v as presetOf,
70
+ g as resolveLook
54
71
  };
55
72
  //# sourceMappingURL=graph-looks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-looks.js","sources":["../../src/render/graph-looks.ts"],"sourcesContent":["// Three registers the same graph can be *drawn* in. Not three palettes, and — since a channel is a\n// binding on the request — not three encodings either.\n//\n// **A look is form and nothing else.** No colours: colour belongs to the theme's categorical\n// scheme, the way it belongs to a chart, so every surface showing the same categories gets the same\n// answer. And no encoding: what a channel carries is bound by whoever draws the graph, under Plot's\n// names. A look that decided whether identity reached the GPU as colour or as shape was a theme\n// reaching into an encoding — Vega-Lite states the general rule, that `config` sets defaults for\n// marks, scales, axes and legends and may not touch `encoding` — and its concrete cost was a `fill`\n// binding that painted nothing under the look named Ink.\n//\n// This is the grammar-of-graphics split, and it is the reference model rather than a local idea:\n// in Observable Plot colour is a property of the **scale**, never of the mark, and the mark carries\n// the channels. A look is the mark's geometry.\n//\n// The three names survive as **recommended pairings** — a form plus the bindings that were bundled\n// with it — offered by the host that draws the graph. A host offering three arrangements it authored\n// is not the same act as a preference silently discarding the caller's binding.\n\n/**\n * The glyphs this canvas draws — **by name**, because a name is what the concept is.\n *\n * This was five exports of one idea: a `SHAPE` object mapping names to numbers, a `ShapeId` type\n * that was the union of those numbers, `SHAPE_ORDER`, `SHAPE_OTHER` and `SHAPE_PATH` keyed by them.\n * The numbers were never ours. They are cosmos.gl's `setPointShapes` enum indices, and the jump from\n * `3` (diamond) to `7` (cross) is what gives that away — there is no `4`, `5` or `6` here because\n * Pentagon, Hexagon and Star are members this canvas does not draw. Publishing them made the\n * renderer's internal numbering part of a contract, so a host held `3` where it meant *diamond*, and\n * an upstream enum that renumbered would have moved every legend on every page silently.\n *\n * A string union says the same thing, reads at the call site — `<ShapeGlyph shape=\"cross\" />` — and\n * makes the mapping this file's private business, which is what it always was.\n */\nexport type Shape = \"circle\" | \"square\" | \"triangle\" | \"diamond\" | \"cross\";\n\n/**\n * The name → cosmos.gl enum index, and the one place that translation happens.\n *\n * `None` (`8`) has no name here on purpose: the point fragment shader `discard`s a `NONE` point that\n * carries no image, so \"past capacity\" spelled as `None` would delete the node from the picture, and\n * a category nobody can name is still a node with edges. `obligations.ts` grades that.\n *\n * Module-scoped and off the barrel. `buffers` reads it on the way to the GPU; nothing else needs it,\n * and anything that did would be reaching for the enum this type exists to hide.\n */\nexport const SHAPE_INDEX: Record<Shape, number> = {\n circle: 0,\n square: 1,\n triangle: 2,\n diamond: 3,\n cross: 7,\n};\n\n/**\n * The shape scale, in slot order — the sibling of the colour scale.\n *\n * Plot calls this channel `symbol` and gives it its own legend, which is the tell that it is a peer\n * of colour rather than a decoration. Four slots and no cycling: a fifth category cannot wear circle\n * again without claiming to be the first one.\n *\n * **Four because the shape channel's measured capacity is five, not because small shapes stop being\n * distinguishable.** Giovannangeli et al. (arXiv 2103.06084) put the ceiling at **5 for shape**\n * against **7 for colour**; `SHAPE_ORDER`'s four plus `SHAPE_OTHER` is exactly five distinguishable\n * glyphs, and the two scales differ in cardinality because the channels do. The colour side already\n * says the same thing from the other end — Dracula's document publishes `--chart-capacity: 7`.\n *\n * **This comment used to give the size argument, and the size argument is wrong.** It said a\n * pentagon, a hexagon and a circle are one dot at Ink's floor. Smart & Szafir (CHI 2019,\n * doi:10.1145/3290605.3300899) measured 16 shapes across 6 mark sizes from 6 to 50 px and found\n * shape discrimination *robust* to size: the only significant variation is at 6 px, and it is 4.5\n * accuracy points against 50 px. The conclusion survived the argument that was given for it, which\n * is the most dangerous shape a comment can have — see `OBLIGATIONS` in `./obligations.ts` for what\n * the floor actually protects.\n */\nexport const SHAPE_ORDER: Shape[] = [\"circle\", \"square\", \"triangle\", \"diamond\"];\n\n/**\n * What a category past the scale wears — the shape channel's `--muted-foreground`.\n *\n * No slot wears it, which is the whole point: it says *not one of the four* rather than repeating\n * the first one. This is the half that used to be missing, and the comment above was false without\n * it — `SHAPE_ORDER[4]` is `undefined`, and the fallback was `circle`.\n */\nexport const SHAPE_OTHER: Shape = \"cross\";\n\n/**\n * The geometry a canvas draws — and nothing else.\n *\n * **No `id`, no `label`, no `blurb`.** Those three were a picker's metadata, and the picker was a\n * list of three names that turned out to be two pictures. What a person chooses is declared in\n * `section.ts` as axes and resolved by `lookFrom`; what a renderer consumes is this.\n */\nexport interface Look {\n /** Radius at the lowest degree in the corpus, and at the highest. */\n size: [number, number];\n link: {\n /**\n * Whether the edge layer is drawn at all.\n *\n * It was `Display.links`, a second vocabulary for the picture that sat beside this one with its\n * own default and its own switch. A form that draws no links is a form.\n */\n render: boolean;\n opacity: number;\n width: number;\n /**\n * How far a link bows off the straight line, as a fraction of its length. `0` is straight.\n *\n * Keep it small. Every link curves the same way, so at cosmos.gl's default of `0.5` a few\n * hundred of them read as one pinwheel and the picture looks like it is spinning — motion\n * where there is structure. A hint is enough to tell two parallel edges apart.\n */\n curve: number;\n /**\n * Whether links **add** where they overlap, instead of compositing over one another.\n *\n * cosmos.gl's default is on, which is a choice nobody here made and which the archive made\n * visible: 4,280 grey links at 0.45 summed to a white spray that swallowed 1,543 points of\n * 2–9 px. Every point was uploaded, none was legible, and the picture read as *the nodes are\n * not rendering*.\n *\n * On, it is a real register rather than a bug — additive light is what makes a dense graph read\n * as flow — so it belongs to the form that wants it and not to the renderer's defaults.\n */\n blend: boolean;\n /**\n * Screen lengths between which a link fades out — depth, for free.\n *\n * Keep the far end generous. cosmos.gl measures this in *screen* pixels, so a range that\n * looks reasonable while zoomed in erases the whole edge layer when you zoom out, which\n * reads as \"Show links stopped working\".\n */\n fade: [number, number];\n };\n /** How many of the highest-degree nodes carry a standing label. */\n labels: number;\n /** A darkened rim. Mood rather than a reading aid, which is why it is form and not display. */\n vignette: boolean;\n /**\n * The dot grid behind the graph, which pans and subdivides with the camera.\n *\n * Beside `vignette` because it is the same kind of thing — the backdrop a form sits on — and it\n * arrived from the same place the rim did not: a `Display` interface that spelled the backdrop,\n * the edge layer and two multipliers as a second set of appearance controls.\n */\n grid: boolean;\n // **There is no `pointScale` and no `linkOpacity`**, and they are the two fields `Display` had that\n // this does not. Each was a reader's multiplier over a number this builder already computes from\n // the axis that owns it — `size` from `marks`, `link.opacity` from `marks` — so the panel offered\n // two ways to say one thing and the second could always overrule the measurement. The measured\n // pair (0.28 legible, 0.42 dense) is the whole argument of `marks`; a slider on top of it is the\n // post-process `graph-model.ts` forbids one layer down, wearing a preference's clothes.\n //\n // What is lost with them is the fit-to-corpus case, and it was never a preference: a computed fit\n // belongs to the tenant's starting point — which is a policy, and which is now expressible.\n\n // There is no `filter`, and its absence is a rule rather than an omission. Nebula carried\n // `saturate(1.1)` on the canvas element — the one thing left in a Look that touched hue, and a\n // chroma multiplier is colour wearing geometry's clothes. Measured over Kanzo's eight slots\n // (2026-08-13, `saturate(1.1)` through the same filter engine the browser applies): it moved\n // every slot, by ΔE 0.85 to **8.05**, which is the size of the separation `deriveScheme`\n // *guarantees* between two different categories. It did not break that separation here —\n // the closest pair went from ΔE 32.36 to 30.38, with room to spare — so the reason it is gone\n // is not a failure it caused, it is that a look must not be able to cause one. The whole claim\n // of the colour layer is that what ships is what was derived and measured; a post-process on\n // the canvas voids it silently, and `graph-model.ts` already forbids the same move one layer\n // down (\"nudging one on the way to the GPU voids all three\").\n}\n\n/**\n * The form a canvas draws, from the axes a person chose.\n *\n * **One function and no table of constants**, which is the shape the axes forced and the reason\n * the three named looks are gone. They were three parallel tables of ten fields; six of those\n * fields separated two of the three by 7–17%, under this file's own threshold for a difference\n * meaning anything — a luminance JND of 6.48–11.30 ΔL*. What is left of them is this: every number\n * appears once, where the axis that owns it is read, and the axis is declared next door in\n * `section.ts` for a panel to draw.\n *\n * **Link opacity and width ride the mark**, and that is what the numbers said rather than a tidy\n * guess: 0.42 and 0.45 on the two dense forms against 0.28 on the legible one. A form that spends\n * more ink on points cannot also spend it on edges.\n *\n * **The legible mark's radius is the whole reason that mark exists.** Its floor protects the other\n * two channels from shape rather than shape from smallness, which is why it is the one to pair\n * `symbol` with: spending shape on identity *and* size on degree at once is what Giovannangeli et\n * al. (arXiv 2103.06084) measure as dropping performance drastically under even minor heterogeneity,\n * and smaller marks worsen it both ways — the luminance JND above, and a square reported larger than\n * any other shape at equal area in 82% of trials (Smart & Szafir, CHI 2019). Not \"a triangle and a\n * square are the same dot below four pixels\", which is what this said for months and is false; see\n * `SHAPE_ORDER` for the measurement that refutes it.\n *\n * **The values are strings because a contributed preference is a string**, in all three kinds, so an\n * unrecognised namespace rides through a write untouched. Parsing what a kind means is the reader's\n * job and it is two lines; `@kanzo-tech/theme` exports the same two, and importing them would add a\n * dependency to this package to carry no code — the same call `GRAPH_SECTION` already makes about the\n * manifest type.\n *\n * A key that is missing, or that carries a value the section never offered, takes the manifest's\n * default. Those defaults are what a graph drew before any of this existed.\n */\nexport function lookFrom(values: Readonly<Record<string, string | undefined>> = {}): Look {\n const on = (key: string, fallback: boolean) => {\n const value = values[key];\n return value === undefined ? fallback : value === \"true\";\n };\n const legible = values.marks === \"legible\";\n const labels = Number.parseFloat(values.labels ?? \"\");\n return {\n size: legible ? [4, 13] : [2, 8],\n link: {\n render: on(\"links\", true),\n opacity: legible ? 0.28 : 0.42,\n width: legible ? 0.5 : 0.6,\n // A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default\n // of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two\n // pictures — straight, and told apart — not a number to tune.\n curve: on(\"bowed-links\", true) ? 0.12 : 0,\n blend: on(\"additive-links\", false),\n // Shared by every form. It was three ranges within ±10% of each other, which is the\n // definition of a field nobody chose.\n fade: [200, 1400],\n },\n labels: Number.isFinite(labels) ? labels : 26,\n vignette: on(\"vignette\", false),\n grid: on(\"grid\", true),\n };\n}\n\n/**\n * What a canvas draws when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be.** It is literally `lookFrom()` — a second public name for\n * a value the package already hands out on request — and the reason it was exported is the one\n * `resolveLook` below removes: a host that wanted one field different had to start from the whole\n * object, because `look` took a whole `Look`. Spreading a default you were given is a copy of it,\n * and a copy is what stops tracking the original the next time a number here moves.\n */\nexport const DEFAULT_LOOK: Look = lookFrom();\n\n/**\n * A look in the pieces a caller wants different — everything else is this package's answer.\n *\n * Two levels, because a `Look` has exactly two: the fields, and `link`. Deep-merging arbitrarily\n * would be a guess about a shape that is right here in this file, and `size` and `fade` are tuples\n * that must be replaced whole rather than merged element-wise.\n */\nexport interface LookPatch extends Partial<Omit<Look, \"link\">> {\n link?: Partial<Look[\"link\"]>;\n}\n\n/**\n * A patch over the package's own default — the merge `DEFAULT_LOOK` existed so a host could do by\n * hand.\n *\n * **Nothing, and it is the shared constant rather than a copy of it.** That identity matters: the\n * buffers are rebuilt whenever the look's reference changes, so a fresh object per render would\n * mean a full colour/size/shape upload on every render of every canvas that never asked for one.\n */\nexport function resolveLook(patch?: LookPatch): Look {\n if (!patch) return DEFAULT_LOOK;\n return { ...DEFAULT_LOOK, ...patch, link: { ...DEFAULT_LOOK.link, ...patch.link } };\n}\n\n/**\n * The SVG path for a shape glyph inside a 12×12 box — the legend draws what the canvas draws.\n *\n * **Off the barrel, and `ShapeGlyph` is what replaced it.** One host imported this, to fill a\n * `<path>` in a legend key and a hover card. What that host actually wanted was *the glyph*, and\n * handing it the path data made it responsible for the viewBox, the fill and the fact that the box\n * is twelve units — three facts it had to keep in step with this file by reading the comment above.\n * A component carries all three and cannot fall out of step with itself.\n */\nexport const SHAPE_PATH: Record<Shape, string> = {\n circle: \"M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z\",\n square: \"M2 2h8v8H2Z\",\n triangle: \"M6 1.6 10.6 10H1.4Z\",\n diamond: \"M6 1 11 6l-5 5-5-5Z\",\n // The proportions are cosmos.gl's own `crossDistance`: a plus with arms at 0.8 of the radius and\n // a bar 0.3 thick, so the legend's glyph is the shape the shader draws.\n cross: \"M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z\",\n};\n"],"names":["SHAPE_INDEX","SHAPE_ORDER","SHAPE_OTHER","lookFrom","values","on","key","fallback","value","legible","labels","DEFAULT_LOOK","resolveLook","patch","SHAPE_PATH"],"mappings":"AA6CO,MAAMA,IAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AACT,GAuBaC,IAAuB,CAAC,UAAU,UAAU,YAAY,SAAS,GASjEC,IAAqB;AAsH3B,SAASC,EAASC,IAAuD,IAAU;AACxF,QAAMC,IAAK,CAACC,GAAaC,MAAsB;AAC7C,UAAMC,IAAQJ,EAAOE,CAAG;AACxB,WAAOE,MAAU,SAAYD,IAAWC,MAAU;AAAA,EACpD,GACMC,IAAUL,EAAO,UAAU,WAC3BM,IAAS,OAAO,WAAWN,EAAO,UAAU,EAAE;AACpD,SAAO;AAAA,IACL,MAAMK,IAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AAAA,IAC/B,MAAM;AAAA,MACJ,QAAQJ,EAAG,SAAS,EAAI;AAAA,MACxB,SAASI,IAAU,OAAO;AAAA,MAC1B,OAAOA,IAAU,MAAM;AAAA;AAAA;AAAA;AAAA,MAIvB,OAAOJ,EAAG,eAAe,EAAI,IAAI,OAAO;AAAA,MACxC,OAAOA,EAAG,kBAAkB,EAAK;AAAA;AAAA;AAAA,MAGjC,MAAM,CAAC,KAAK,IAAI;AAAA,IAAA;AAAA,IAElB,QAAQ,OAAO,SAASK,CAAM,IAAIA,IAAS;AAAA,IAC3C,UAAUL,EAAG,YAAY,EAAK;AAAA,IAC9B,MAAMA,EAAG,QAAQ,EAAI;AAAA,EAAA;AAEzB;AAWO,MAAMM,IAAqBR,EAAA;AAqB3B,SAASS,EAAYC,GAAyB;AACnD,SAAKA,IACE,EAAE,GAAGF,GAAc,GAAGE,GAAO,MAAM,EAAE,GAAGF,EAAa,MAAM,GAAGE,EAAM,OAAK,IAD7DF;AAErB;AAWO,MAAMG,IAAoC;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO;AACT;"}
1
+ {"version":3,"file":"graph-looks.js","sources":["../../src/render/graph-looks.ts"],"sourcesContent":["// Three registers the same graph can be *drawn* in. Not three palettes, and — since a channel is a\n// binding on the request — not three encodings either.\n//\n// **A look is form and nothing else.** No colours: colour belongs to the theme's categorical\n// scheme, the way it belongs to a chart, so every surface showing the same categories gets the same\n// answer. And no encoding: what a channel carries is bound by whoever draws the graph, under Plot's\n// names. A look that decided whether identity reached the GPU as colour or as shape was a theme\n// reaching into an encoding — Vega-Lite states the general rule, that `config` sets defaults for\n// marks, scales, axes and legends and may not touch `encoding` — and its concrete cost was a `fill`\n// binding that painted nothing under the look named Ink.\n//\n// This is the grammar-of-graphics split, and it is the reference model rather than a local idea:\n// in Observable Plot colour is a property of the **scale**, never of the mark, and the mark carries\n// the channels. A look is the mark's geometry.\n//\n// The three names survive as **presets over the axes** — `PRESETS`, worn through `GraphLooks` — and\n// the bindings once bundled with them stay the host's: `useGraphPrefs` says which preset the axes\n// are, and a host that wants Ink's shape-for-identity binds it. A preset never discards a binding.\n\nimport { GRAPH_SECTION } from \"../section\";\n\n/**\n * The glyphs this canvas draws — **by name**, because a name is what the concept is.\n *\n * This was five exports of one idea: a `SHAPE` object mapping names to numbers, a `ShapeId` type\n * that was the union of those numbers, `SHAPE_ORDER`, `SHAPE_OTHER` and `SHAPE_PATH` keyed by them.\n * The numbers were never ours. They are cosmos.gl's `setPointShapes` enum indices, and the jump from\n * `3` (diamond) to `7` (cross) is what gives that away — there is no `4`, `5` or `6` here because\n * Pentagon, Hexagon and Star are members this canvas does not draw. Publishing them made the\n * renderer's internal numbering part of a contract, so a host held `3` where it meant *diamond*, and\n * an upstream enum that renumbered would have moved every legend on every page silently.\n *\n * A string union says the same thing, reads at the call site — `<ShapeGlyph shape=\"cross\" />` — and\n * makes the mapping this file's private business, which is what it always was.\n */\nexport type Shape = \"circle\" | \"square\" | \"triangle\" | \"diamond\" | \"cross\";\n\n/**\n * The name → cosmos.gl enum index, and the one place that translation happens.\n *\n * `None` (`8`) has no name here on purpose: the point fragment shader `discard`s a `NONE` point that\n * carries no image, so \"past capacity\" spelled as `None` would delete the node from the picture, and\n * a category nobody can name is still a node with edges. `obligations.ts` grades that.\n *\n * Module-scoped and off the barrel. `buffers` reads it on the way to the GPU; nothing else needs it,\n * and anything that did would be reaching for the enum this type exists to hide.\n */\nexport const SHAPE_INDEX: Record<Shape, number> = {\n circle: 0,\n square: 1,\n triangle: 2,\n diamond: 3,\n cross: 7,\n};\n\n/**\n * The shape scale, in slot order — the sibling of the colour scale.\n *\n * Plot calls this channel `symbol` and gives it its own legend, which is the tell that it is a peer\n * of colour rather than a decoration. Four slots and no cycling: a fifth category cannot wear circle\n * again without claiming to be the first one.\n *\n * **Four because the shape channel's measured capacity is five, not because small shapes stop being\n * distinguishable.** Giovannangeli et al. (arXiv 2103.06084) put the ceiling at **5 for shape**\n * against **7 for colour**; `SHAPE_ORDER`'s four plus `SHAPE_OTHER` is exactly five distinguishable\n * glyphs, and the two scales differ in cardinality because the channels do. The colour side already\n * says the same thing from the other end — Dracula's document publishes `--chart-capacity: 7`.\n *\n * **This comment used to give the size argument, and the size argument is wrong.** It said a\n * pentagon, a hexagon and a circle are one dot at Ink's floor. Smart & Szafir (CHI 2019,\n * doi:10.1145/3290605.3300899) measured 16 shapes across 6 mark sizes from 6 to 50 px and found\n * shape discrimination *robust* to size: the only significant variation is at 6 px, and it is 4.5\n * accuracy points against 50 px. The conclusion survived the argument that was given for it, which\n * is the most dangerous shape a comment can have — see `OBLIGATIONS` in `./obligations.ts` for what\n * the floor actually protects.\n */\nexport const SHAPE_ORDER: Shape[] = [\"circle\", \"square\", \"triangle\", \"diamond\"];\n\n/**\n * What a category past the scale wears — the shape channel's `--muted-foreground`.\n *\n * No slot wears it, which is the whole point: it says *not one of the four* rather than repeating\n * the first one. This is the half that used to be missing, and the comment above was false without\n * it — `SHAPE_ORDER[4]` is `undefined`, and the fallback was `circle`.\n */\nexport const SHAPE_OTHER: Shape = \"cross\";\n\n/**\n * The geometry a canvas draws — and nothing else.\n *\n * **No `id`, no `label`, no `blurb`.** Those three were a picker's metadata, and the picker was a\n * list of three names that turned out to be two pictures. What a person chooses is declared in\n * `section.ts` as axes and resolved by `lookFrom`; what a renderer consumes is this.\n */\nexport interface Look {\n /** Radius at the lowest degree in the corpus, and at the highest. */\n size: [number, number];\n link: {\n /**\n * Whether the edge layer is drawn at all.\n *\n * It was `Display.links`, a second vocabulary for the picture that sat beside this one with its\n * own default and its own switch. A form that draws no links is a form.\n */\n render: boolean;\n opacity: number;\n width: number;\n /**\n * How far a link bows off the straight line, as a fraction of its length. `0` is straight.\n *\n * Keep it small. Every link curves the same way, so at cosmos.gl's default of `0.5` a few\n * hundred of them read as one pinwheel and the picture looks like it is spinning — motion\n * where there is structure. A hint is enough to tell two parallel edges apart.\n */\n curve: number;\n /**\n * Whether links **add** where they overlap, instead of compositing over one another.\n *\n * cosmos.gl's default is on, which is a choice nobody here made and which the archive made\n * visible: 4,280 grey links at 0.45 summed to a white spray that swallowed 1,543 points of\n * 2–9 px. Every point was uploaded, none was legible, and the picture read as *the nodes are\n * not rendering*.\n *\n * On, it is a real register rather than a bug — additive light is what makes a dense graph read\n * as flow — so it belongs to the form that wants it and not to the renderer's defaults.\n */\n blend: boolean;\n /**\n * Screen lengths between which a link fades out — depth, for free.\n *\n * Keep the far end generous. cosmos.gl measures this in *screen* pixels, so a range that\n * looks reasonable while zoomed in erases the whole edge layer when you zoom out, which\n * reads as \"Show links stopped working\".\n */\n fade: [number, number];\n };\n /** How many of the highest-degree nodes carry a standing label. */\n labels: number;\n /** A darkened rim. Mood rather than a reading aid, which is why it is form and not display. */\n vignette: boolean;\n /**\n * The dot grid behind the graph, which pans and subdivides with the camera.\n *\n * Beside `vignette` because it is the same kind of thing — the backdrop a form sits on — and it\n * arrived from the same place the rim did not: a `Display` interface that spelled the backdrop,\n * the edge layer and two multipliers as a second set of appearance controls.\n */\n grid: boolean;\n // **There is no `pointScale` and no `linkOpacity`**, and they are the two fields `Display` had that\n // this does not. Each was a reader's multiplier over a number this builder already computes from\n // the axis that owns it — `size` from `marks`, `link.opacity` from `marks` — so the panel offered\n // two ways to say one thing and the second could always overrule the measurement. The measured\n // pair (0.28 legible, 0.42 dense) is the whole argument of `marks`; a slider on top of it is the\n // post-process `graph-model.ts` forbids one layer down, wearing a preference's clothes.\n //\n // What is lost with them is the fit-to-corpus case, and it was never a preference: a computed fit\n // belongs to the tenant's starting point — which is a policy, and which is now expressible.\n\n // There is no `filter`, and its absence is a rule rather than an omission. Nebula carried\n // `saturate(1.1)` on the canvas element — the one thing left in a Look that touched hue, and a\n // chroma multiplier is colour wearing geometry's clothes. Measured over Kanzo's eight slots\n // (2026-08-13, `saturate(1.1)` through the same filter engine the browser applies): it moved\n // every slot, by ΔE 0.85 to **8.05**, which is the size of the separation `deriveScheme`\n // *guarantees* between two different categories. It did not break that separation here —\n // the closest pair went from ΔE 32.36 to 30.38, with room to spare — so the reason it is gone\n // is not a failure it caused, it is that a look must not be able to cause one. The whole claim\n // of the colour layer is that what ships is what was derived and measured; a post-process on\n // the canvas voids it silently, and `graph-model.ts` already forbids the same move one layer\n // down (\"nudging one on the way to the GPU voids all three\").\n}\n\n/**\n * The form a canvas draws, from the axes a person chose.\n *\n * **One function and no table of constants**, which is the shape the axes forced and the reason\n * the three named looks are gone. They were three parallel tables of ten fields; six of those\n * fields separated two of the three by 7–17%, under this file's own threshold for a difference\n * meaning anything — a luminance JND of 6.48–11.30 ΔL*. What is left of them is this: every number\n * appears once, where the axis that owns it is read, and the axis is declared next door in\n * `section.ts` for a panel to draw.\n *\n * **Link opacity and width ride the mark**, and that is what the numbers said rather than a tidy\n * guess: 0.42 and 0.45 on the two dense forms against 0.28 on the legible one. A form that spends\n * more ink on points cannot also spend it on edges.\n *\n * **The legible mark's radius is the whole reason that mark exists.** Its floor protects the other\n * two channels from shape rather than shape from smallness, which is why it is the one to pair\n * `symbol` with: spending shape on identity *and* size on degree at once is what Giovannangeli et\n * al. (arXiv 2103.06084) measure as dropping performance drastically under even minor heterogeneity,\n * and smaller marks worsen it both ways — the luminance JND above, and a square reported larger than\n * any other shape at equal area in 82% of trials (Smart & Szafir, CHI 2019). Not \"a triangle and a\n * square are the same dot below four pixels\", which is what this said for months and is false; see\n * `SHAPE_ORDER` for the measurement that refutes it.\n *\n * **The values are strings because a contributed preference is a string**, in all three kinds, so an\n * unrecognised namespace rides through a write untouched. Parsing what a kind means is the reader's\n * job and it is two lines; `@kanzo-tech/theme` exports the same two, and importing them would add a\n * dependency to this package to carry no code — the same call `GRAPH_SECTION` already makes about the\n * manifest type.\n *\n * A key that is missing, or that carries a value the section never offered, takes the manifest's\n * default. Those defaults are what a graph drew before any of this existed.\n */\nexport function lookFrom(values: Readonly<Record<string, string | undefined>> = {}): Look {\n const on = (key: string, fallback: boolean) => {\n const value = values[key];\n return value === undefined ? fallback : value === \"true\";\n };\n const legible = values.marks === \"legible\";\n const labels = Number.parseFloat(values.labels ?? \"\");\n return {\n size: legible ? [4, 13] : [2, 8],\n link: {\n render: on(\"links\", true),\n opacity: legible ? 0.28 : 0.42,\n width: legible ? 0.5 : 0.6,\n // A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default\n // of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two\n // pictures — straight, and told apart — not a number to tune.\n curve: on(\"bowed-links\", true) ? 0.12 : 0,\n blend: on(\"additive-links\", false),\n // Shared by every form. It was three ranges within ±10% of each other, which is the\n // definition of a field nobody chose.\n fade: [200, 1400],\n },\n labels: Number.isFinite(labels) ? labels : 26,\n vignette: on(\"vignette\", false),\n grid: on(\"grid\", true),\n };\n}\n\n/** The three named looks. */\nexport type LookPreset = \"nebula\" | \"atlas\" | \"ink\";\n\n/**\n * **The three looks as values of the section's axes**, in the order a picker offers them. Each writes\n * every axis any of them names, so wearing one leaves nothing of the last, and a look is worn exactly\n * when the resolved axes equal its own — no name is stored beside them. Atlas is every default.\n */\nexport const PRESETS: Record<LookPreset, Readonly<Record<string, string>>> = {\n nebula: { marks: \"dense\", \"additive-links\": \"true\", \"bowed-links\": \"false\", labels: \"14\", vignette: \"true\" },\n atlas: { marks: \"dense\", \"additive-links\": \"false\", \"bowed-links\": \"true\", labels: \"26\", vignette: \"false\" },\n ink: { marks: \"legible\", \"additive-links\": \"false\", \"bowed-links\": \"false\", labels: \"40\", vignette: \"false\" },\n};\n\n/** The look the axes are — a missing one is the section's default — or `null` once customised past all three. */\nexport function presetOf(values: Readonly<Record<string, string | undefined>>): LookPreset | null {\n const value = (key: string) => values[key] ?? GRAPH_SECTION.prefs?.[key]?.default;\n const found = (Object.keys(PRESETS) as LookPreset[]).find((id) =>\n Object.entries(PRESETS[id]).every(([key, wanted]) => value(key) === wanted),\n );\n return found ?? null;\n}\n\n/**\n * What a canvas draws when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be.** It is literally `lookFrom()` — a second public name for\n * a value the package already hands out on request — and the reason it was exported is the one\n * `resolveLook` below removes: a host that wanted one field different had to start from the whole\n * object, because `look` took a whole `Look`. Spreading a default you were given is a copy of it,\n * and a copy is what stops tracking the original the next time a number here moves.\n */\nexport const DEFAULT_LOOK: Look = lookFrom();\n\n/**\n * A look in the pieces a caller wants different — everything else is this package's answer.\n *\n * Two levels, because a `Look` has exactly two: the fields, and `link`. Deep-merging arbitrarily\n * would be a guess about a shape that is right here in this file, and `size` and `fade` are tuples\n * that must be replaced whole rather than merged element-wise.\n */\nexport interface LookPatch extends Partial<Omit<Look, \"link\">> {\n link?: Partial<Look[\"link\"]>;\n}\n\n/**\n * A patch over the package's own default — the merge `DEFAULT_LOOK` existed so a host could do by\n * hand.\n *\n * **Nothing, and it is the shared constant rather than a copy of it.** That identity matters: the\n * buffers are rebuilt whenever the look's reference changes, so a fresh object per render would\n * mean a full colour/size/shape upload on every render of every canvas that never asked for one.\n */\nexport function resolveLook(patch?: LookPatch): Look {\n if (!patch) return DEFAULT_LOOK;\n return { ...DEFAULT_LOOK, ...patch, link: { ...DEFAULT_LOOK.link, ...patch.link } };\n}\n\n/**\n * The SVG path for a shape glyph inside a 12×12 box — the legend draws what the canvas draws.\n *\n * **Off the barrel, and `ShapeGlyph` is what replaced it.** One host imported this, to fill a\n * `<path>` in a legend key and a hover card. What that host actually wanted was *the glyph*, and\n * handing it the path data made it responsible for the viewBox, the fill and the fact that the box\n * is twelve units — three facts it had to keep in step with this file by reading the comment above.\n * A component carries all three and cannot fall out of step with itself.\n */\nexport const SHAPE_PATH: Record<Shape, string> = {\n circle: \"M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z\",\n square: \"M2 2h8v8H2Z\",\n triangle: \"M6 1.6 10.6 10H1.4Z\",\n diamond: \"M6 1 11 6l-5 5-5-5Z\",\n // The proportions are cosmos.gl's own `crossDistance`: a plus with arms at 0.8 of the radius and\n // a bar 0.3 thick, so the legend's glyph is the shape the shader draws.\n cross: \"M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z\",\n};\n"],"names":["SHAPE_INDEX","SHAPE_ORDER","SHAPE_OTHER","lookFrom","values","on","key","fallback","value","legible","labels","PRESETS","presetOf","_b","_a","GRAPH_SECTION","id","wanted","DEFAULT_LOOK","resolveLook","patch","SHAPE_PATH"],"mappings":";AA+CO,MAAMA,IAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AACT,GAuBaC,IAAuB,CAAC,UAAU,UAAU,YAAY,SAAS,GASjEC,IAAqB;AAsH3B,SAASC,EAASC,IAAuD,IAAU;AACxF,QAAMC,IAAK,CAACC,GAAaC,MAAsB;AAC7C,UAAMC,IAAQJ,EAAOE,CAAG;AACxB,WAAOE,MAAU,SAAYD,IAAWC,MAAU;AAAA,EACpD,GACMC,IAAUL,EAAO,UAAU,WAC3BM,IAAS,OAAO,WAAWN,EAAO,UAAU,EAAE;AACpD,SAAO;AAAA,IACL,MAAMK,IAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AAAA,IAC/B,MAAM;AAAA,MACJ,QAAQJ,EAAG,SAAS,EAAI;AAAA,MACxB,SAASI,IAAU,OAAO;AAAA,MAC1B,OAAOA,IAAU,MAAM;AAAA;AAAA;AAAA;AAAA,MAIvB,OAAOJ,EAAG,eAAe,EAAI,IAAI,OAAO;AAAA,MACxC,OAAOA,EAAG,kBAAkB,EAAK;AAAA;AAAA;AAAA,MAGjC,MAAM,CAAC,KAAK,IAAI;AAAA,IAAA;AAAA,IAElB,QAAQ,OAAO,SAASK,CAAM,IAAIA,IAAS;AAAA,IAC3C,UAAUL,EAAG,YAAY,EAAK;AAAA,IAC9B,MAAMA,EAAG,QAAQ,EAAI;AAAA,EAAA;AAEzB;AAUO,MAAMM,IAAgE;AAAA,EAC3E,QAAQ,EAAE,OAAO,SAAS,kBAAkB,QAAQ,eAAe,SAAS,QAAQ,MAAM,UAAU,OAAA;AAAA,EACpG,OAAO,EAAE,OAAO,SAAS,kBAAkB,SAAS,eAAe,QAAQ,QAAQ,MAAM,UAAU,QAAA;AAAA,EACnG,KAAK,EAAE,OAAO,WAAW,kBAAkB,SAAS,eAAe,SAAS,QAAQ,MAAM,UAAU,QAAA;AACtG;AAGO,SAASC,EAASR,GAAyE;AAChG,QAAMI,IAAQ,CAACF,MAAA;;AAAgB,WAAAF,EAAOE,CAAG,OAAKO,KAAAC,IAAAC,EAAc,UAAd,gBAAAD,EAAsBR,OAAtB,gBAAAO,EAA4B;AAAA;AAI1E,SAHe,OAAO,KAAKF,CAAO,EAAmB;AAAA,IAAK,CAACK,MACzD,OAAO,QAAQL,EAAQK,CAAE,CAAC,EAAE,MAAM,CAAC,CAACV,GAAKW,CAAM,MAAMT,EAAMF,CAAG,MAAMW,CAAM;AAAA,EAAA,KAE5D;AAClB;AAWO,MAAMC,IAAqBf,EAAA;AAqB3B,SAASgB,EAAYC,GAAyB;AACnD,SAAKA,IACE,EAAE,GAAGF,GAAc,GAAGE,GAAO,MAAM,EAAE,GAAGF,EAAa,MAAM,GAAGE,EAAM,OAAK,IAD7DF;AAErB;AAWO,MAAMG,IAAoC;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO;AACT;"}
@@ -0,0 +1,23 @@
1
+ import { Geometry } from '../core/load';
2
+ type Extent = NonNullable<Geometry["extent"]>;
3
+ /**
4
+ * **Where the corpus's extent sits in cosmos.gl's square** — centred, at its own scale, and scaled
5
+ * down only past the box the device can simulate in. Drawing alone is translation-invariant, but the
6
+ * layout is not: gravity pulls toward the square's centre and many-body bins points on a grid over
7
+ * `[0, side]²`, so an extent left where it was contracts toward a corner and falls off the grid.
8
+ */
9
+ export interface Placement {
10
+ readonly side: number;
11
+ readonly k: number;
12
+ readonly dx: number;
13
+ readonly dy: number;
14
+ }
15
+ /** No extent — no row has a position — so nothing moves and nothing is drawn. */
16
+ export declare const UNPLACED: Placement;
17
+ export declare function placementOf(extent: Extent, box: number): Placement;
18
+ /** The loaded positions in the square, with `NaN` where the page's filter hides a vertex — cosmos.gl draws no such point. */
19
+ export declare function placed(geometry: Geometry, at: Placement, mask: Uint8Array | null): Float32Array;
20
+ /** The extent's corners in the square, for a camera that frames it. */
21
+ export declare function cornersOf(extent: Extent, at: Placement): number[];
22
+ export {};
23
+ //# sourceMappingURL=placement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../src/render/placement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,iFAAiF;AACjF,eAAO,MAAM,QAAQ,EAAE,SAA2C,CAAC;AAEnE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAKlE;AAED,6HAA6H;AAC7H,wBAAgB,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,YAAY,CAS/F;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,GAAG,MAAM,EAAE,CAEjE"}
@@ -0,0 +1,23 @@
1
+ const u = { side: 0, k: 1, dx: 0, dy: 0 };
2
+ function l(d, o) {
3
+ const s = Math.max(d.w, d.h, 2), n = s > o ? o / s : 1, c = s * n;
4
+ return { side: c, k: n, dx: (c - d.w * n) / 2 - d.x * n, dy: (c - d.h * n) / 2 - d.y * n };
5
+ }
6
+ function y(d, o, s) {
7
+ const { positions: n } = d, c = new Float32Array(n.length);
8
+ for (let r = 0; r < d.size; r++) {
9
+ const i = s !== null && !s[r];
10
+ c[r * 2] = i ? Number.NaN : n[r * 2] * o.k + o.dx, c[r * 2 + 1] = i ? Number.NaN : n[r * 2 + 1] * o.k + o.dy;
11
+ }
12
+ return c;
13
+ }
14
+ function k(d, o) {
15
+ return [d.x * o.k + o.dx, d.y * o.k + o.dy, (d.x + d.w) * o.k + o.dx, (d.y + d.h) * o.k + o.dy];
16
+ }
17
+ export {
18
+ u as UNPLACED,
19
+ k as cornersOf,
20
+ y as placed,
21
+ l as placementOf
22
+ };
23
+ //# sourceMappingURL=placement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placement.js","sources":["../../src/render/placement.ts"],"sourcesContent":["import type { Geometry } from \"../core/load\";\n\ntype Extent = NonNullable<Geometry[\"extent\"]>;\n\n/**\n * **Where the corpus's extent sits in cosmos.gl's square** — centred, at its own scale, and scaled\n * down only past the box the device can simulate in. Drawing alone is translation-invariant, but the\n * layout is not: gravity pulls toward the square's centre and many-body bins points on a grid over\n * `[0, side]²`, so an extent left where it was contracts toward a corner and falls off the grid.\n */\nexport interface Placement {\n readonly side: number;\n readonly k: number;\n readonly dx: number;\n readonly dy: number;\n}\n\n/** No extent — no row has a position — so nothing moves and nothing is drawn. */\nexport const UNPLACED: Placement = { side: 0, k: 1, dx: 0, dy: 0 };\n\nexport function placementOf(extent: Extent, box: number): Placement {\n const longest = Math.max(extent.w, extent.h, 2);\n const k = longest > box ? box / longest : 1;\n const side = longest * k;\n return { side, k, dx: (side - extent.w * k) / 2 - extent.x * k, dy: (side - extent.h * k) / 2 - extent.y * k };\n}\n\n/** The loaded positions in the square, with `NaN` where the page's filter hides a vertex — cosmos.gl draws no such point. */\nexport function placed(geometry: Geometry, at: Placement, mask: Uint8Array | null): Float32Array {\n const { positions } = geometry;\n const out = new Float32Array(positions.length);\n for (let id = 0; id < geometry.size; id++) {\n const hidden = mask !== null && !mask[id];\n out[id * 2] = hidden ? Number.NaN : (positions[id * 2] as number) * at.k + at.dx;\n out[id * 2 + 1] = hidden ? Number.NaN : (positions[id * 2 + 1] as number) * at.k + at.dy;\n }\n return out;\n}\n\n/** The extent's corners in the square, for a camera that frames it. */\nexport function cornersOf(extent: Extent, at: Placement): number[] {\n return [extent.x * at.k + at.dx, extent.y * at.k + at.dy, (extent.x + extent.w) * at.k + at.dx, (extent.y + extent.h) * at.k + at.dy];\n}\n"],"names":["UNPLACED","placementOf","extent","box","longest","k","side","placed","geometry","at","mask","positions","out","id","hidden","cornersOf"],"mappings":"AAkBO,MAAMA,IAAsB,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,EAAA;AAExD,SAASC,EAAYC,GAAgBC,GAAwB;AAClE,QAAMC,IAAU,KAAK,IAAIF,EAAO,GAAGA,EAAO,GAAG,CAAC,GACxCG,IAAID,IAAUD,IAAMA,IAAMC,IAAU,GACpCE,IAAOF,IAAUC;AACvB,SAAO,EAAE,MAAAC,GAAM,GAAAD,GAAG,KAAKC,IAAOJ,EAAO,IAAIG,KAAK,IAAIH,EAAO,IAAIG,GAAG,KAAKC,IAAOJ,EAAO,IAAIG,KAAK,IAAIH,EAAO,IAAIG,EAAA;AAC7G;AAGO,SAASE,EAAOC,GAAoBC,GAAeC,GAAuC;AAC/F,QAAM,EAAE,WAAAC,MAAcH,GAChBI,IAAM,IAAI,aAAaD,EAAU,MAAM;AAC7C,WAASE,IAAK,GAAGA,IAAKL,EAAS,MAAMK,KAAM;AACzC,UAAMC,IAASJ,MAAS,QAAQ,CAACA,EAAKG,CAAE;AACxC,IAAAD,EAAIC,IAAK,CAAC,IAAIC,IAAS,OAAO,MAAOH,EAAUE,IAAK,CAAC,IAAeJ,EAAG,IAAIA,EAAG,IAC9EG,EAAIC,IAAK,IAAI,CAAC,IAAIC,IAAS,OAAO,MAAOH,EAAUE,IAAK,IAAI,CAAC,IAAeJ,EAAG,IAAIA,EAAG;AAAA,EACxF;AACA,SAAOG;AACT;AAGO,SAASG,EAAUb,GAAgBO,GAAyB;AACjE,SAAO,CAACP,EAAO,IAAIO,EAAG,IAAIA,EAAG,IAAIP,EAAO,IAAIO,EAAG,IAAIA,EAAG,KAAKP,EAAO,IAAIA,EAAO,KAAKO,EAAG,IAAIA,EAAG,KAAKP,EAAO,IAAIA,EAAO,KAAKO,EAAG,IAAIA,EAAG,EAAE;AACtI;"}
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/render/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,eAAe,CAAC;AAM7D,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;CACvD;AAED,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,kFAAkF;IAClF,OAAO,IAAI,IAAI,CAAC;IAChB,uFAAuF;IACvF,GAAG,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;KAAE,GAAG,MAAM,EAAE,CAAC;IACvG,OAAO,IAAI,IAAI,CAAC;CACjB;AA0BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,GAAE,cAAmB,GAAG,QAAQ,GAAG,IAAI,CA4RpH"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/render/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,eAAe,CAAC;AAQ7D,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;CACvD;AAED,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,kFAAkF;IAClF,OAAO,IAAI,IAAI,CAAC;IAChB,uFAAuF;IACvF,GAAG,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;KAAE,GAAG,MAAM,EAAE,CAAC;IACvG,OAAO,IAAI,IAAI,CAAC;CACjB;AAaD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,GAAE,cAAmB,GAAG,QAAQ,GAAG,IAAI,CA+TpH"}