@kanzo-tech/graph 0.24.0 → 0.26.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 (71) hide show
  1. package/dist/core/categories.d.ts +1 -1
  2. package/dist/core/categories.d.ts.map +1 -1
  3. package/dist/core/categories.js.map +1 -1
  4. package/dist/core/channels.d.ts +12 -6
  5. package/dist/core/channels.d.ts.map +1 -1
  6. package/dist/core/channels.js +15 -12
  7. package/dist/core/channels.js.map +1 -1
  8. package/dist/core/client.d.ts +26 -0
  9. package/dist/core/client.d.ts.map +1 -0
  10. package/dist/core/client.js +62 -0
  11. package/dist/core/client.js.map +1 -0
  12. package/dist/core/error.d.ts +3 -3
  13. package/dist/core/error.d.ts.map +1 -1
  14. package/dist/core/error.js.map +1 -1
  15. package/dist/core/load.d.ts +30 -39
  16. package/dist/core/load.d.ts.map +1 -1
  17. package/dist/core/load.js +63 -117
  18. package/dist/core/load.js.map +1 -1
  19. package/dist/core/source.d.ts +90 -0
  20. package/dist/core/source.d.ts.map +1 -0
  21. package/dist/core/source.js +105 -0
  22. package/dist/core/source.js.map +1 -0
  23. package/dist/core/state.d.ts +19 -20
  24. package/dist/core/state.d.ts.map +1 -1
  25. package/dist/core/store.d.ts.map +1 -1
  26. package/dist/core/store.js +137 -157
  27. package/dist/core/store.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/parts/graph-canvas.d.ts.map +1 -1
  30. package/dist/parts/graph-canvas.js +49 -48
  31. package/dist/parts/graph-canvas.js.map +1 -1
  32. package/dist/parts/graph-counts.js +4 -4
  33. package/dist/parts/graph-counts.js.map +1 -1
  34. package/dist/parts/graph-inspector.d.ts +3 -3
  35. package/dist/parts/graph-inspector.d.ts.map +1 -1
  36. package/dist/parts/graph-inspector.js +45 -43
  37. package/dist/parts/graph-inspector.js.map +1 -1
  38. package/dist/parts/graph-search.d.ts +7 -7
  39. package/dist/parts/graph-search.d.ts.map +1 -1
  40. package/dist/parts/graph-search.js +60 -69
  41. package/dist/parts/graph-search.js.map +1 -1
  42. package/dist/parts/graph-toolbar.js +1 -1
  43. package/dist/parts/graph-toolbar.js.map +1 -1
  44. package/dist/react/use-graph.js +31 -31
  45. package/dist/react/use-graph.js.map +1 -1
  46. package/dist/render/graph-sim.d.ts +1 -1
  47. package/dist/render/graph-sim.d.ts.map +1 -1
  48. package/dist/render/graph-sim.js.map +1 -1
  49. package/dist/render/motion.d.ts +33 -0
  50. package/dist/render/motion.d.ts.map +1 -0
  51. package/dist/render/motion.js +19 -0
  52. package/dist/render/motion.js.map +1 -0
  53. package/dist/render/placement.d.ts +2 -4
  54. package/dist/render/placement.d.ts.map +1 -1
  55. package/dist/render/placement.js +12 -16
  56. package/dist/render/placement.js.map +1 -1
  57. package/dist/render/renderer.d.ts +6 -5
  58. package/dist/render/renderer.d.ts.map +1 -1
  59. package/dist/render/renderer.js +124 -113
  60. package/dist/render/renderer.js.map +1 -1
  61. package/dist/section.js +1 -1
  62. package/dist/section.js.map +1 -1
  63. package/package.json +8 -10
  64. package/dist/core/detail.d.ts +0 -26
  65. package/dist/core/detail.d.ts.map +0 -1
  66. package/dist/core/detail.js +0 -63
  67. package/dist/core/detail.js.map +0 -1
  68. package/dist/core/filter.d.ts +0 -23
  69. package/dist/core/filter.d.ts.map +0 -1
  70. package/dist/core/filter.js +0 -95
  71. package/dist/core/filter.js.map +0 -1
@@ -1,81 +1,72 @@
1
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,
2
+ import { jsxs as i, jsx as n } from "react/jsx-runtime";
3
+ import { createListCollection as E, Combobox as V, ComboboxInput as j, InputGroupAddon as q, ComboboxContent as A, ComboboxEmpty as O, ComboboxItem as K, Show as w } from "@kanzo-tech/ui";
4
+ import { SearchIcon as L } from "lucide-react";
5
+ import { useState as k, useEffect as M, useMemo as P } from "react";
6
+ import { nameOf as Q } from "../core/categories.js";
7
+ import { bindingOf as U } from "../core/channels.js";
8
+ import { searchTitles as $ } from "../core/source.js";
9
+ import { useGraphContext as z } from "../react/graph-root.js";
10
+ import { useGraphState as f, useGraphSnapshot as B } from "../react/use-graph-state.js";
11
+ const D = 150;
12
+ function ne({ className: G, limit: r = 50, placeholder: I = "Find a node…", size: N = "sm" }) {
13
+ const l = z(), a = f((e) => e.structure), b = f((e) => e.options), c = B((e) => e.encoding), g = f((e) => {
14
+ var o;
15
+ return ((o = e.drawn) == null ? void 0 : o.domain) ?? e.domain;
16
+ }), [u, x] = k(null), [t, T] = k(""), { title: C, categories: y, coordinator: d } = b, S = U(b), v = S.byTable || S.category !== void 0;
17
+ M(() => {
18
+ if (!a || !d || t === "") return;
19
+ let e = !0;
20
+ const o = setTimeout(() => {
21
+ $(d, a, t, C, r).then(
22
+ (p) => e && x({
23
+ query: t,
24
+ entries: p.map(({ id: h, text: F }) => ({
25
+ value: String(h),
26
+ label: F || `#${h}`,
27
+ kind: v && c ? Q(g[c.ranks[h]], y) : null
28
+ }))
29
+ }),
30
+ (p) => {
31
+ e && (x({ query: t, entries: !1 }), l.getState().options.onFailure(p));
32
+ }
33
+ );
34
+ }, D);
35
+ return () => {
36
+ e = !1, clearTimeout(o);
37
+ };
38
+ }, [l, a, d, t, C, r, v, c, g, y]);
39
+ const s = u && u.query === t ? u.entries : null, m = P(() => E({ items: s || [] }), [s]);
40
+ return /* @__PURE__ */ i(
41
+ V,
51
42
  {
52
- collection: C,
53
- disabled: y === null,
54
- onInputValueChange: (e) => j(e.inputValue),
43
+ collection: m,
44
+ disabled: a === null,
45
+ onInputValueChange: (e) => T(e.inputValue),
55
46
  onValueChange: (e) => {
56
- const n = e.value[0];
57
- n !== void 0 && a.reveal(Number(n));
47
+ const o = e.value[0];
48
+ o !== void 0 && l.reveal(Number(o));
58
49
  },
59
50
  children: [
60
- /* @__PURE__ */ r(
61
- q,
51
+ /* @__PURE__ */ n(
52
+ j,
62
53
  {
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, {}) })
54
+ "aria-invalid": s === !1 || void 0,
55
+ className: G,
56
+ placeholder: s === !1 ? "The names could not be read." : I,
57
+ size: N,
58
+ children: /* @__PURE__ */ n(q, { align: "inline-start", children: /* @__PURE__ */ n(L, {}) })
68
59
  }
69
60
  ),
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 }) })
61
+ /* @__PURE__ */ i(A, { children: [
62
+ /* @__PURE__ */ n(O, { children: "Nothing by that name." }),
63
+ m.items.map((e) => /* @__PURE__ */ i(K, { item: e, children: [
64
+ /* @__PURE__ */ n("span", { className: "min-w-0 truncate", children: e.label }),
65
+ /* @__PURE__ */ n(w, { when: e.kind !== null, children: /* @__PURE__ */ n("span", { className: "ms-auto ps-2 text-muted-foreground text-xs", children: e.kind }) })
75
66
  ] }, 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: [
67
+ /* @__PURE__ */ n(w, { when: m.items.length === r, children: /* @__PURE__ */ i("p", { className: "border-t px-2 py-1.5 text-muted-foreground text-xs", children: [
77
68
  "First ",
78
- s,
69
+ r,
79
70
  ". Keep typing to narrow it."
80
71
  ] }) })
81
72
  ] })
@@ -84,6 +75,6 @@ function oe({ className: m, limit: s = 50, placeholder: d = "Find a node…", si
84
75
  );
85
76
  }
86
77
  export {
87
- oe as GraphSearch
78
+ ne as GraphSearch
88
79
  };
89
80
  //# sourceMappingURL=graph-search.js.map
@@ -1 +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
+ {"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} 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 { searchTitles } from \"../core/source\";\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/** What one query answered, or `false` when the read failed. */\ntype Found = { query: string; entries: Entry[] | false } | null;\n\n/** How long a reader pauses before the text is asked for: one statement per pause, not per key. */\nconst PAUSE = 150;\n\n/**\n * **Find a vertex by its text and go to it** — Cosmograph's search: each pause in typing asks the\n * corpus for the first `limit` vertices whose `title` — or its table's `identity` — contains the text,\n * case-insensitively, one `ILIKE` over every vertex table through the page's coordinator. Nothing is\n * read before the reader types, and no vertex's text is held in the page. Each match is named by the\n * root's `categories`; picking one is `reveal`: the canvas frames it and selects it with its\n * neighbours, and `GraphInspector` reads it.\n *\n * A read that fails is handed to `onFailure` whole and the input says so.\n */\nexport function GraphSearch({ className, limit = 50, placeholder = \"Find a node…\", size = \"sm\" }: GraphSearchProps) {\n const api = useGraphContext();\n const structure = useGraphState((s) => s.structure);\n const options = useGraphState((s) => s.options);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const [found, setFound] = useState<Found>(null);\n const [query, setQuery] = useState(\"\");\n const { title, categories, coordinator } = options;\n const binding = bindingOf(options);\n const bound = binding.byTable || binding.category !== undefined;\n\n useEffect(() => {\n if (!structure || !coordinator || query === \"\") return;\n let current = true;\n const timer = setTimeout(() => {\n searchTitles(coordinator, structure, query, title, limit).then(\n (rows) =>\n current &&\n setFound({\n query,\n entries: rows.map(({ id, text }) => ({\n value: String(id),\n label: text || `#${id}`,\n kind: bound && encoding ? nameOf(domain[encoding.ranks[id] as number], categories) : null,\n })),\n }),\n (error: unknown) => {\n if (!current) return;\n setFound({ query, entries: false });\n api.getState().options.onFailure(error);\n },\n );\n }, PAUSE);\n return () => {\n current = false;\n clearTimeout(timer);\n };\n }, [api, structure, coordinator, query, title, limit, bound, encoding, domain, categories]);\n\n const answered = found && found.query === query ? found.entries : null;\n const collection = useMemo(() => createListCollection({ items: answered || [] }), [answered]);\n\n return (\n <Combobox\n collection={collection}\n disabled={structure === 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 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":";;;;;;;;;;AAmCA;AAYO;AACL;;AAIoC;AAAqB;AAOzD;AACE;AACA;AACA;AACE;AAA0D;AAG7C;AACP;AACqC;AACnB;AACK;AACgE;AACrF;AACH;AAED;AAEsC;AACxC;AAAA;AAGJ;AACE;AACkB;AACpB;AAGF;AAGA;AACE;AAAC;AAAA;AACC;AACwB;AACoC;AAE1D;AACA;AAAmD;AACrD;AAEA;AAAA;AAAC;AAAA;AACqC;AACpC;AACmE;AACnE;AAIA;AAAA;AAAA;AAGA;AAAoC;AAGhC;AAA+C;AAG/C;AAEH;AAEmE;AAAA;AAAO;AAAM;AACjF;AACF;AAAA;AAAA;AAGN;;;;"}
@@ -10,7 +10,7 @@ const m = "rounded-lg border bg-card shadow-sm", v = 1.4, j = {
10
10
  settled: { pause: !1, label: "Run the layout" }
11
11
  };
12
12
  function E({ className: z, orientation: c = "horizontal", slot: x, ...S }) {
13
- const a = Z(), t = r((l) => l.corpus !== null), n = r((l) => l.tool), d = r((l) => l.selection), g = r((l) => l.total), u = r((l) => l.motion), p = r((l) => l.pinned.length), b = j[u], f = `Release ${p} pinned ${p === 1 ? "node" : "nodes"}`;
13
+ const a = Z(), t = r((l) => l.structure !== null), n = r((l) => l.tool), d = r((l) => l.selection), g = r((l) => l.total), u = r((l) => l.motion), p = r((l) => l.pinned.length), b = j[u], f = `Release ${p} pinned ${p === 1 ? "node" : "nodes"}`;
14
14
  return /* @__PURE__ */ s(
15
15
  "div",
16
16
  {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-toolbar.js","sources":["../../src/parts/graph-toolbar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, cn, Show } from \"@kanzo-tech/ui\";\nimport {\n LassoIcon,\n MaximizeIcon,\n MinusIcon,\n PauseIcon,\n PinOffIcon,\n PlayIcon,\n PlusIcon,\n RotateCcwIcon,\n ScanIcon,\n SquareDashedIcon,\n XIcon,\n} from \"lucide-react\";\nimport type { Motion } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphToolbarProps extends React.ComponentProps<\"div\"> {\n /** How the clusters stack. Each cluster is a `ButtonGroup` in the same orientation. */\n orientation?: \"horizontal\" | \"vertical\";\n}\n\n/**\n * Solid, not `bg-card/85 backdrop-blur-md`: a diluted surface is a function of whatever the layout\n * put behind it — measured over the plane and the eight slots, `card/85` spans ΔE 9.4–11.1, against\n * the ramp's own `interchangeable` bound of 4. The buttons are `ghost` because the group carries the\n * surface, so the divisions come back as explicit separators.\n */\nconst CLUSTER = \"rounded-lg border bg-card shadow-sm\";\n\nconst ZOOM = 1.4;\n\n/** One action per state, named for what it does: a settled layout is not paused, so it is woken. */\nconst TRANSPORT: Record<Motion, { pause: boolean; label: string }> = {\n running: { pause: true, label: \"Pause the layout\" },\n paused: { pause: false, label: \"Resume the layout\" },\n settled: { pause: false, label: \"Run the layout\" },\n};\n\n/**\n * **The commands, drawn** — the selection tools, the selection and what to do with it, the camera, and\n * the live layout's transport, which starts a layout from the points where they are. Each cluster is a `ButtonGroup`, because a cluster\n * is a claim that its buttons do one job; the part declares no `toolbar` role, since a `role` of that\n * kind promises roving focus and each button here is its own tab stop.\n *\n * The tools are not a `SegmentGroup`: that always has one member selected, and no tool — drag pans —\n * is the posture a reader spends most of their time in.\n */\nexport function GraphToolbar({ className, orientation = \"horizontal\", slot, ...rest }: GraphToolbarProps) {\n const api = useGraphContext();\n const ready = useGraphState((s) => s.corpus !== null);\n const tool = useGraphState((s) => s.tool);\n const selection = useGraphState((s) => s.selection);\n const total = useGraphState((s) => s.total);\n const motion = useGraphState((s) => s.motion);\n const pinned = useGraphState((s) => s.pinned.length);\n const transport = TRANSPORT[motion];\n const release = `Release ${pinned} pinned ${pinned === 1 ? \"node\" : \"nodes\"}`;\n\n return (\n <div\n {...rest}\n className={cn(\"flex items-start gap-1.5\", orientation === \"vertical\" && \"flex-col items-end\", className)}\n data-slot={slot ?? \"graph-toolbar\"}\n >\n <ButtonGroup aria-label=\"Selection tool\" className={CLUSTER} orientation={orientation}>\n <Button\n aria-label=\"Marquee\"\n aria-pressed={tool === \"rect\"}\n disabled={!ready}\n onClick={() => api.setTool(tool === \"rect\" ? null : \"rect\")}\n size=\"icon-sm\"\n title=\"Drag a box — or just hold Shift\"\n variant={tool === \"rect\" ? \"default\" : \"ghost\"}\n >\n <SquareDashedIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button\n aria-label=\"Lasso\"\n aria-pressed={tool === \"lasso\"}\n disabled={!ready}\n onClick={() => api.setTool(tool === \"lasso\" ? null : \"lasso\")}\n size=\"icon-sm\"\n title=\"Draw a loop around a cluster\"\n variant={tool === \"lasso\" ? \"default\" : \"ghost\"}\n >\n <LassoIcon />\n </Button>\n </ButtonGroup>\n\n <Show when={selection !== null}>\n <ButtonGroup aria-label=\"Current selection\" className={CLUSTER} data-slot=\"graph-toolbar-selection\">\n <ButtonGroupText className=\"gap-1 ps-2 pe-2 text-xs tabular-nums\">\n <span className=\"font-medium text-foreground\">{selection?.vertices.length.toLocaleString()}</span>\n {total === undefined ? \" selected\" : ` of ${total.toLocaleString()} selected`}\n </ButtonGroupText>\n <ButtonGroupSeparator />\n <Button aria-label=\"Frame the selection\" onClick={() => api.frameSelection()} size=\"icon-sm\" title=\"Frame the selection\" variant=\"ghost\">\n <ScanIcon />\n </Button>\n <Button aria-label=\"Clear the selection\" onClick={() => api.clear()} size=\"icon-sm\" title=\"Clear the selection\" variant=\"ghost\">\n <XIcon />\n </Button>\n </ButtonGroup>\n </Show>\n\n <ButtonGroup aria-label=\"Zoom and fit\" className={CLUSTER} orientation={orientation}>\n <Button aria-label=\"Zoom in\" disabled={!ready} onClick={() => api.zoomBy(ZOOM)} size=\"icon-sm\" variant=\"ghost\">\n <PlusIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Zoom out\" disabled={!ready} onClick={() => api.zoomBy(1 / ZOOM)} size=\"icon-sm\" variant=\"ghost\">\n <MinusIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Fit to view\" disabled={!ready} onClick={() => api.fit()} size=\"icon-sm\" variant=\"ghost\">\n <MaximizeIcon />\n </Button>\n </ButtonGroup>\n\n <ButtonGroup aria-label=\"Layout\" className={CLUSTER} orientation={orientation}>\n <Button\n aria-label={transport.label}\n aria-pressed={motion === \"paused\"}\n disabled={!ready}\n onClick={() => (transport.pause ? api.pause() : api.resume())}\n size=\"icon-sm\"\n title={transport.label}\n variant={motion === \"paused\" ? \"default\" : \"ghost\"}\n >\n {motion === \"running\" ? <PauseIcon /> : <PlayIcon />}\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Re-run the layout\" disabled={!ready} onClick={() => api.restart()} size=\"icon-sm\" title=\"Re-run the layout\" variant=\"ghost\">\n <RotateCcwIcon />\n </Button>\n <Show when={pinned > 0}>\n <ButtonGroupSeparator />\n <Button aria-label={release} onClick={() => api.unpin()} size=\"icon-sm\" title={release} variant=\"ghost\">\n <PinOffIcon />\n </Button>\n </Show>\n </ButtonGroup>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;AA+BA;AAKqE;AACpC;AACA;AAEjC;AAWO;AACL;AAUA;AACE;AAAC;AAAA;AACK;AACmG;AACpF;AAEnB;AACE;AAAA;AAAC;AAAA;AACY;AACY;AACZ;AAC+C;AACrD;AACC;AACiC;AAErB;AAAA;AAAA;AAEE;AACtB;AAAC;AAAA;AACY;AACY;AACZ;AACiD;AACvD;AACC;AACkC;AAE7B;AAAA;AAAA;AAEf;AAII;AACE;AAA2F;AACzB;AACpE;AACsB;AAGtB;AAGA;AAEJ;AAGE;AAEA;AACsB;AAGtB;AACsB;AAGtB;AACF;AAGE;AAAA;AAAC;AAAA;AACuB;AACG;AACd;AACyC;AAC/C;AACY;AAC0B;AAEO;AAAA;AAAA;AAE9B;AAGtB;AAEE;AAAsB;AAGtB;AACF;AACF;AAAA;AAAA;AAGN;;;;"}
1
+ {"version":3,"file":"graph-toolbar.js","sources":["../../src/parts/graph-toolbar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, cn, Show } from \"@kanzo-tech/ui\";\nimport {\n LassoIcon,\n MaximizeIcon,\n MinusIcon,\n PauseIcon,\n PinOffIcon,\n PlayIcon,\n PlusIcon,\n RotateCcwIcon,\n ScanIcon,\n SquareDashedIcon,\n XIcon,\n} from \"lucide-react\";\nimport type { Motion } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphState } from \"../react/use-graph-state\";\n\nexport interface GraphToolbarProps extends React.ComponentProps<\"div\"> {\n /** How the clusters stack. Each cluster is a `ButtonGroup` in the same orientation. */\n orientation?: \"horizontal\" | \"vertical\";\n}\n\n/**\n * Solid, not `bg-card/85 backdrop-blur-md`: a diluted surface is a function of whatever the layout\n * put behind it — measured over the plane and the eight slots, `card/85` spans ΔE 9.4–11.1, against\n * the ramp's own `interchangeable` bound of 4. The buttons are `ghost` because the group carries the\n * surface, so the divisions come back as explicit separators.\n */\nconst CLUSTER = \"rounded-lg border bg-card shadow-sm\";\n\nconst ZOOM = 1.4;\n\n/** One action per state, named for what it does: a settled layout is not paused, so it is woken. */\nconst TRANSPORT: Record<Motion, { pause: boolean; label: string }> = {\n running: { pause: true, label: \"Pause the layout\" },\n paused: { pause: false, label: \"Resume the layout\" },\n settled: { pause: false, label: \"Run the layout\" },\n};\n\n/**\n * **The commands, drawn** — the selection tools, the selection and what to do with it, the camera, and\n * the live layout's transport, which starts a layout from the points where they are. Each cluster is a `ButtonGroup`, because a cluster\n * is a claim that its buttons do one job; the part declares no `toolbar` role, since a `role` of that\n * kind promises roving focus and each button here is its own tab stop.\n *\n * The tools are not a `SegmentGroup`: that always has one member selected, and no tool — drag pans —\n * is the posture a reader spends most of their time in.\n */\nexport function GraphToolbar({ className, orientation = \"horizontal\", slot, ...rest }: GraphToolbarProps) {\n const api = useGraphContext();\n const ready = useGraphState((s) => s.structure !== null);\n const tool = useGraphState((s) => s.tool);\n const selection = useGraphState((s) => s.selection);\n const total = useGraphState((s) => s.total);\n const motion = useGraphState((s) => s.motion);\n const pinned = useGraphState((s) => s.pinned.length);\n const transport = TRANSPORT[motion];\n const release = `Release ${pinned} pinned ${pinned === 1 ? \"node\" : \"nodes\"}`;\n\n return (\n <div\n {...rest}\n className={cn(\"flex items-start gap-1.5\", orientation === \"vertical\" && \"flex-col items-end\", className)}\n data-slot={slot ?? \"graph-toolbar\"}\n >\n <ButtonGroup aria-label=\"Selection tool\" className={CLUSTER} orientation={orientation}>\n <Button\n aria-label=\"Marquee\"\n aria-pressed={tool === \"rect\"}\n disabled={!ready}\n onClick={() => api.setTool(tool === \"rect\" ? null : \"rect\")}\n size=\"icon-sm\"\n title=\"Drag a box — or just hold Shift\"\n variant={tool === \"rect\" ? \"default\" : \"ghost\"}\n >\n <SquareDashedIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button\n aria-label=\"Lasso\"\n aria-pressed={tool === \"lasso\"}\n disabled={!ready}\n onClick={() => api.setTool(tool === \"lasso\" ? null : \"lasso\")}\n size=\"icon-sm\"\n title=\"Draw a loop around a cluster\"\n variant={tool === \"lasso\" ? \"default\" : \"ghost\"}\n >\n <LassoIcon />\n </Button>\n </ButtonGroup>\n\n <Show when={selection !== null}>\n <ButtonGroup aria-label=\"Current selection\" className={CLUSTER} data-slot=\"graph-toolbar-selection\">\n <ButtonGroupText className=\"gap-1 ps-2 pe-2 text-xs tabular-nums\">\n <span className=\"font-medium text-foreground\">{selection?.vertices.length.toLocaleString()}</span>\n {total === undefined ? \" selected\" : ` of ${total.toLocaleString()} selected`}\n </ButtonGroupText>\n <ButtonGroupSeparator />\n <Button aria-label=\"Frame the selection\" onClick={() => api.frameSelection()} size=\"icon-sm\" title=\"Frame the selection\" variant=\"ghost\">\n <ScanIcon />\n </Button>\n <Button aria-label=\"Clear the selection\" onClick={() => api.clear()} size=\"icon-sm\" title=\"Clear the selection\" variant=\"ghost\">\n <XIcon />\n </Button>\n </ButtonGroup>\n </Show>\n\n <ButtonGroup aria-label=\"Zoom and fit\" className={CLUSTER} orientation={orientation}>\n <Button aria-label=\"Zoom in\" disabled={!ready} onClick={() => api.zoomBy(ZOOM)} size=\"icon-sm\" variant=\"ghost\">\n <PlusIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Zoom out\" disabled={!ready} onClick={() => api.zoomBy(1 / ZOOM)} size=\"icon-sm\" variant=\"ghost\">\n <MinusIcon />\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Fit to view\" disabled={!ready} onClick={() => api.fit()} size=\"icon-sm\" variant=\"ghost\">\n <MaximizeIcon />\n </Button>\n </ButtonGroup>\n\n <ButtonGroup aria-label=\"Layout\" className={CLUSTER} orientation={orientation}>\n <Button\n aria-label={transport.label}\n aria-pressed={motion === \"paused\"}\n disabled={!ready}\n onClick={() => (transport.pause ? api.pause() : api.resume())}\n size=\"icon-sm\"\n title={transport.label}\n variant={motion === \"paused\" ? \"default\" : \"ghost\"}\n >\n {motion === \"running\" ? <PauseIcon /> : <PlayIcon />}\n </Button>\n <ButtonGroupSeparator />\n <Button aria-label=\"Re-run the layout\" disabled={!ready} onClick={() => api.restart()} size=\"icon-sm\" title=\"Re-run the layout\" variant=\"ghost\">\n <RotateCcwIcon />\n </Button>\n <Show when={pinned > 0}>\n <ButtonGroupSeparator />\n <Button aria-label={release} onClick={() => api.unpin()} size=\"icon-sm\" title={release} variant=\"ghost\">\n <PinOffIcon />\n </Button>\n </Show>\n </ButtonGroup>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;AA+BA;AAKqE;AACpC;AACA;AAEjC;AAWO;AACL;AAUA;AACE;AAAC;AAAA;AACK;AACmG;AACpF;AAEnB;AACE;AAAA;AAAC;AAAA;AACY;AACY;AACZ;AAC+C;AACrD;AACC;AACiC;AAErB;AAAA;AAAA;AAEE;AACtB;AAAC;AAAA;AACY;AACY;AACZ;AACiD;AACvD;AACC;AACkC;AAE7B;AAAA;AAAA;AAEf;AAII;AACE;AAA2F;AACzB;AACpE;AACsB;AAGtB;AAGA;AAEJ;AAGE;AAEA;AACsB;AAGtB;AACsB;AAGtB;AACF;AAGE;AAAA;AAAC;AAAA;AACuB;AACG;AACd;AACyC;AAC/C;AACY;AAC0B;AAEO;AAAA;AAAA;AAE9B;AAGtB;AAEE;AAAsB;AAGtB;AACF;AACF;AAAA;AAAA;AAGN;;;;"}
@@ -1,19 +1,19 @@
1
1
  "use client";
2
- import { useRef as k, useCallback as B, useState as E, useEffect as f } from "react";
3
- import { createGraph as G } from "../core/store.js";
4
- import { createRenderer as N } from "../render/renderer.js";
2
+ import { useRef as N, useCallback as R, useState as T, useEffect as f } from "react";
3
+ import { createGraph as z } from "../core/store.js";
4
+ import { createRenderer as O } from "../render/renderer.js";
5
5
  const m = /* @__PURE__ */ new WeakMap();
6
- function R(e) {
6
+ function v(e) {
7
7
  const t = m.get(e);
8
8
  if (!t) throw new Error("a graph part was given an api useGraph did not build");
9
9
  return t;
10
10
  }
11
- function A(e) {
12
- const t = k(e);
11
+ function M(e) {
12
+ const t = N(e);
13
13
  t.current = e;
14
- const u = B(
15
- (F) => ({
16
- ...F,
14
+ const o = R(
15
+ (G) => ({
16
+ ...G,
17
17
  onFailure: (l) => t.current.onFailure(l),
18
18
  onSelect: (l) => {
19
19
  var a, s;
@@ -25,47 +25,47 @@ function A(e) {
25
25
  }
26
26
  }),
27
27
  []
28
- ), [o] = E(() => T(G(u(e)))), { store: n } = R(o), { categories: c, corpus: r, fill: i, filterBy: p, look: b, r: d, sim: h, simulate: S, stroke: g, symbol: y, title: w } = e;
28
+ ), [c] = T(() => A(z(o(e)))), { store: n } = v(c), { categories: u, cluster: r, coordinator: i, fill: p, filterBy: b, from: d, look: h, r: S, sim: y, simulate: g, stroke: w, symbol: F, title: k, x: B, y: E } = e;
29
29
  return f(() => {
30
- n.setOptions(u(t.current));
31
- }, [n, u, c, r, i, p, b, d, h, S, g, y, w]), f(() => n.subscribe(() => {
32
- }), [n]), o;
30
+ n.setOptions(o(t.current));
31
+ }, [n, o, u, r, i, p, b, d, h, S, y, g, w, F, k, B, E]), f(() => n.subscribe(() => {
32
+ }), [n]), c;
33
33
  }
34
- function T(e) {
34
+ function A(e) {
35
35
  let t = null;
36
- const u = (n) => (...c) => {
36
+ const o = (n) => (...u) => {
37
37
  var r;
38
- return (r = t == null ? void 0 : t[n]) == null ? void 0 : r.call(t, ...c);
39
- }, o = {
40
- zoomBy: u("zoomBy"),
41
- fit: u("fit"),
42
- pause: u("pause"),
43
- resume: u("resume"),
44
- restart: u("restart"),
45
- unpin: u("unpin"),
38
+ return (r = t == null ? void 0 : t[n]) == null ? void 0 : r.call(t, ...u);
39
+ }, c = {
40
+ zoomBy: o("zoomBy"),
41
+ fit: o("fit"),
42
+ pause: o("pause"),
43
+ resume: o("resume"),
44
+ restart: o("restart"),
45
+ unpin: o("unpin"),
46
46
  // Selecting and focusing are state, and hold without a renderer; centring is the camera's.
47
47
  reveal: (n) => {
48
48
  if (t) return t.reveal(n);
49
49
  e.select([n], "node", "Node"), e.focus(n);
50
50
  },
51
- frameSelection: u("frameSelection"),
51
+ frameSelection: o("frameSelection"),
52
52
  clear: () => {
53
53
  e.select(null), e.focus(null);
54
54
  },
55
- select: (n, c, r) => e.select(n, c, r),
55
+ select: (n, u, r) => e.select(n, u, r),
56
56
  setFocus: (n) => e.focus(n),
57
57
  setTool: (n) => e.setTool(n),
58
58
  subscribe: (n) => e.subscribe(n),
59
59
  getState: () => e.getSnapshot()
60
60
  };
61
- return m.set(o, {
61
+ return m.set(c, {
62
62
  store: e,
63
63
  renderer: () => t,
64
- attach(n, c) {
64
+ attach(n, u) {
65
65
  e.renderable();
66
66
  let r = null;
67
67
  try {
68
- r = N(n, e, c);
68
+ r = O(n, e, u);
69
69
  } catch (i) {
70
70
  e.unrenderable(i);
71
71
  }
@@ -73,10 +73,10 @@ function T(e) {
73
73
  r == null || r.destroy(), t === r && (t = null);
74
74
  };
75
75
  }
76
- }), o;
76
+ }), c;
77
77
  }
78
78
  export {
79
- R as internalsOf,
80
- A as useGraph
79
+ v as internalsOf,
80
+ M as useGraph
81
81
  };
82
82
  //# sourceMappingURL=use-graph.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool, VertexId } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (error) => latest.current.onFailure(error),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title]);\n\n // Subscribed here as well as by the parts, so the store's first-subscriber and last-subscriber\n // moves follow the root's lifetime and not whichever part happened to mount first.\n useEffect(() => store.subscribe(() => {}), [store]);\n\n return api;\n}\n\nfunction build(store: GraphStore): GraphApi {\n let renderer: Renderer | null = null;\n const on =\n <K extends keyof GraphCommands>(name: K) =>\n (...args: Parameters<GraphCommands[K]>) =>\n (renderer?.[name] as ((...a: Parameters<GraphCommands[K]>) => void) | undefined)?.(...args);\n const api: GraphApi = {\n zoomBy: on(\"zoomBy\"),\n fit: on(\"fit\"),\n pause: on(\"pause\"),\n resume: on(\"resume\"),\n restart: on(\"restart\"),\n unpin: on(\"unpin\"),\n // Selecting and focusing are state, and hold without a renderer; centring is the camera's.\n reveal: (vertex) => {\n if (renderer) return renderer.reveal(vertex);\n store.select([vertex], \"node\", \"Node\");\n store.focus(vertex);\n },\n frameSelection: on(\"frameSelection\"),\n clear: () => {\n store.select(null);\n store.focus(null);\n },\n select: (vertices, source, label) => store.select(vertices, source, label),\n setFocus: (vertex) => store.focus(vertex),\n setTool: (tool) => store.setTool(tool),\n subscribe: (listener) => store.subscribe(listener),\n getState: () => store.getSnapshot(),\n };\n INTERNALS.set(api, {\n store,\n renderer: () => renderer,\n attach(host, events) {\n store.renderable();\n let mounted: Renderer | null = null;\n try {\n mounted = createRenderer(host, store, events);\n } catch (error) {\n store.unrenderable(error);\n }\n renderer = mounted;\n return () => {\n mounted?.destroy();\n if (renderer === mounted) renderer = null;\n };\n },\n });\n return api;\n}\n\n"],"names":[],"mappings":";;;;AA+BA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACA;AACA;AAAgB;AAC8B;AACvC;AACiD;;AAC3B;AAA0B;AAAA;;AAC9B;AAAyB;AAAA;AAAM;AAEtD;AAMF;AACE;AAAwC;AAKJ;AAGxC;AAEA;AACE;AACA;;AAGK;AAAqF;AACpE;AACD;AACN;AACI;AACE;AACE;AACJ;AAAA;AAGf;AACA;AACkB;AACpB;AACmC;AAEjC;AACgB;AAClB;AACyE;AACjC;AACH;AACY;AAC3B;AAExB;AAAmB;AACjB;AACgB;AAEd;AACA;AACA;AACE;AAA4C;AAE5C;AAAwB;AAE1B;AAEE;AACqC;AACvC;AACF;AAGJ;;;;;"}
1
+ {"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool, VertexId } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (error) => latest.current.onFailure(error),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories, cluster, coordinator, fill, filterBy, from, look, r, sim, simulate, stroke, symbol, title, x, y } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, cluster, coordinator, fill, filterBy, from, look, r, sim, simulate, stroke, symbol, title, x, y]);\n\n // Subscribed here as well as by the parts, so the store's first-subscriber and last-subscriber\n // moves follow the root's lifetime and not whichever part happened to mount first.\n useEffect(() => store.subscribe(() => {}), [store]);\n\n return api;\n}\n\nfunction build(store: GraphStore): GraphApi {\n let renderer: Renderer | null = null;\n const on =\n <K extends keyof GraphCommands>(name: K) =>\n (...args: Parameters<GraphCommands[K]>) =>\n (renderer?.[name] as ((...a: Parameters<GraphCommands[K]>) => void) | undefined)?.(...args);\n const api: GraphApi = {\n zoomBy: on(\"zoomBy\"),\n fit: on(\"fit\"),\n pause: on(\"pause\"),\n resume: on(\"resume\"),\n restart: on(\"restart\"),\n unpin: on(\"unpin\"),\n // Selecting and focusing are state, and hold without a renderer; centring is the camera's.\n reveal: (vertex) => {\n if (renderer) return renderer.reveal(vertex);\n store.select([vertex], \"node\", \"Node\");\n store.focus(vertex);\n },\n frameSelection: on(\"frameSelection\"),\n clear: () => {\n store.select(null);\n store.focus(null);\n },\n select: (vertices, source, label) => store.select(vertices, source, label),\n setFocus: (vertex) => store.focus(vertex),\n setTool: (tool) => store.setTool(tool),\n subscribe: (listener) => store.subscribe(listener),\n getState: () => store.getSnapshot(),\n };\n INTERNALS.set(api, {\n store,\n renderer: () => renderer,\n attach(host, events) {\n store.renderable();\n let mounted: Renderer | null = null;\n try {\n mounted = createRenderer(host, store, events);\n } catch (error) {\n store.unrenderable(error);\n }\n renderer = mounted;\n return () => {\n mounted?.destroy();\n if (renderer === mounted) renderer = null;\n };\n },\n });\n return api;\n}\n\n"],"names":[],"mappings":";;;;AA+BA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACA;AACA;AAAgB;AAC8B;AACvC;AACiD;;AAC3B;AAA0B;AAAA;;AAC9B;AAAyB;AAAA;AAAM;AAEtD;AAMF;AACE;AAAwC;AAKJ;AAGxC;AAEA;AACE;AACA;;AAGK;AAAqF;AACpE;AACD;AACN;AACI;AACE;AACE;AACJ;AAAA;AAGf;AACA;AACkB;AACpB;AACmC;AAEjC;AACgB;AAClB;AACyE;AACjC;AACH;AACY;AAC3B;AAExB;AAAmB;AACjB;AACgB;AAEd;AACA;AACA;AACE;AAA4C;AAE5C;AAAwB;AAE1B;AAEE;AACqC;AACvC;AACF;AAGJ;;;;;"}
@@ -17,7 +17,7 @@ export interface Sim {
17
17
  linkSpring: number;
18
18
  linkDistance: number;
19
19
  friction: number;
20
- /** Pull toward the node's group position on the cluster ring. Zero lets the links decide alone. */
20
+ /** Pull toward the centre of the vertex's cluster, when a column is bound to `cluster`. Zero lets the links decide alone. */
21
21
  cluster: number;
22
22
  }
23
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"graph-sim.d.ts","sourceRoot":"","sources":["../../src/render/graph-sim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,iEAAiE;AACjE,MAAM,WAAW,GAAG;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,GAAG,CAatF;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,GAAe,CAAC;AAE1C;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAGpD"}
1
+ {"version":3,"file":"graph-sim.d.ts","sourceRoot":"","sources":["../../src/render/graph-sim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,iEAAiE;AACjE,MAAM,WAAW,GAAG;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,6HAA6H;IAC7H,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,GAAG,CAatF;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,GAAe,CAAC;AAE1C;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAGpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-sim.js","sources":["../../src/render/graph-sim.ts"],"sourcesContent":["/**\n * The force coefficients, and the one function that builds them from the axes a person chose.\n *\n * `graph-looks.ts`'s sibling, and deliberately its twin: a type the renderer consumes, one builder\n * that reads declared string values, and a default that is the builder called with nothing. What was\n * here before is what was there before — an interface, a `DEFAULT_SIM` table beside it, and a dock\n * that held six of them in React state and drew six sliders by hand.\n *\n * **A coefficient is not appearance, and it is still a preference.** `section.ts` says why it shares\n * the manifest: `tokens` are the colours a document may move, `prefs` are what the person on the\n * screen decides, and somebody tuning a layout until it settles is deciding.\n */\n\n/** Force coefficients, handed straight to the GPU simulation. */\nexport interface Sim {\n gravity: number;\n repulsion: number;\n linkSpring: number;\n linkDistance: number;\n friction: number;\n /** Pull toward the node's group position on the cluster ring. Zero lets the links decide alone. */\n cluster: number;\n}\n\n/**\n * Coefficients that settle a few-hundred-node graph into something readable.\n *\n * Chosen against a corpus of that size, and they are a starting point rather than a law: a graph\n * two orders of magnitude larger wants less repulsion and more friction, and the measurements in\n * `/docs/graph/benchmarks` say a live simulation is finished by around 200,000 points regardless. A\n * host that knows its corpus starts its users elsewhere through the tenant policy.\n *\n * The values are strings for the reason `lookFrom`'s are: a contributed preference is a string in\n * all three kinds, so an unrecognised namespace rides through a write untouched. A key that is\n * missing, or that carries a value the section never offered, takes the default below — which is\n * what a graph simulated before any of this existed.\n */\nexport function simFrom(values: Readonly<Record<string, string | undefined>> = {}): Sim {\n const num = (key: string, fallback: number) => {\n const value = Number.parseFloat(values[key] ?? \"\");\n return Number.isFinite(value) ? value : fallback;\n };\n return {\n gravity: num(\"gravity\", 0.14),\n repulsion: num(\"repulsion\", 1.1),\n linkSpring: num(\"link-spring\", 0.6),\n linkDistance: num(\"link-distance\", 18),\n friction: num(\"friction\", 0.86),\n cluster: num(\"cluster\", 0.1),\n };\n}\n\n/**\n * What a canvas simulates with when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be** — for the reason its twin was: it is `simFrom()`, a\n * second public name for a value already available on request, exported only because `sim` took a\n * whole `Sim` and a host changing `gravity` had to supply the other five.\n */\nexport const DEFAULT_SIM: Sim = simFrom();\n\n/**\n * The coefficients a caller wants different, over the ones this package chose.\n *\n * Flat, so a spread is the whole merge. `undefined` returns the shared constant rather than a copy:\n * the renderer re-heats when the forces change, so a fresh object\n * per render would put energy back into a settled layout on every render.\n */\nexport function resolveSim(patch?: Partial<Sim>): Sim {\n if (!patch) return DEFAULT_SIM;\n return { ...DEFAULT_SIM, ...patch };\n}\n"],"names":["simFrom","values","num","key","fallback","value","DEFAULT_SIM","resolveSim","patch"],"mappings":"AAqCO,SAASA,EAAQC,IAAuD,IAAS;AACtF,QAAMC,IAAM,CAACC,GAAaC,MAAqB;AAC7C,UAAMC,IAAQ,OAAO,WAAWJ,EAAOE,CAAG,KAAK,EAAE;AACjD,WAAO,OAAO,SAASE,CAAK,IAAIA,IAAQD;AAAA,EAC1C;AACA,SAAO;AAAA,IACL,SAASF,EAAI,WAAW,IAAI;AAAA,IAC5B,WAAWA,EAAI,aAAa,GAAG;AAAA,IAC/B,YAAYA,EAAI,eAAe,GAAG;AAAA,IAClC,cAAcA,EAAI,iBAAiB,EAAE;AAAA,IACrC,UAAUA,EAAI,YAAY,IAAI;AAAA,IAC9B,SAASA,EAAI,WAAW,GAAG;AAAA,EAAA;AAE/B;AASO,MAAMI,IAAmBN,EAAA;AASzB,SAASO,EAAWC,GAA2B;AACpD,SAAKA,IACE,EAAE,GAAGF,GAAa,GAAGE,EAAA,IADTF;AAErB;"}
1
+ {"version":3,"file":"graph-sim.js","sources":["../../src/render/graph-sim.ts"],"sourcesContent":["/**\n * The force coefficients, and the one function that builds them from the axes a person chose.\n *\n * `graph-looks.ts`'s sibling, and deliberately its twin: a type the renderer consumes, one builder\n * that reads declared string values, and a default that is the builder called with nothing. What was\n * here before is what was there before — an interface, a `DEFAULT_SIM` table beside it, and a dock\n * that held six of them in React state and drew six sliders by hand.\n *\n * **A coefficient is not appearance, and it is still a preference.** `section.ts` says why it shares\n * the manifest: `tokens` are the colours a document may move, `prefs` are what the person on the\n * screen decides, and somebody tuning a layout until it settles is deciding.\n */\n\n/** Force coefficients, handed straight to the GPU simulation. */\nexport interface Sim {\n gravity: number;\n repulsion: number;\n linkSpring: number;\n linkDistance: number;\n friction: number;\n /** Pull toward the centre of the vertex's cluster, when a column is bound to `cluster`. Zero lets the links decide alone. */\n cluster: number;\n}\n\n/**\n * Coefficients that settle a few-hundred-node graph into something readable.\n *\n * Chosen against a corpus of that size, and they are a starting point rather than a law: a graph\n * two orders of magnitude larger wants less repulsion and more friction, and the measurements in\n * `/docs/graph/benchmarks` say a live simulation is finished by around 200,000 points regardless. A\n * host that knows its corpus starts its users elsewhere through the tenant policy.\n *\n * The values are strings for the reason `lookFrom`'s are: a contributed preference is a string in\n * all three kinds, so an unrecognised namespace rides through a write untouched. A key that is\n * missing, or that carries a value the section never offered, takes the default below — which is\n * what a graph simulated before any of this existed.\n */\nexport function simFrom(values: Readonly<Record<string, string | undefined>> = {}): Sim {\n const num = (key: string, fallback: number) => {\n const value = Number.parseFloat(values[key] ?? \"\");\n return Number.isFinite(value) ? value : fallback;\n };\n return {\n gravity: num(\"gravity\", 0.14),\n repulsion: num(\"repulsion\", 1.1),\n linkSpring: num(\"link-spring\", 0.6),\n linkDistance: num(\"link-distance\", 18),\n friction: num(\"friction\", 0.86),\n cluster: num(\"cluster\", 0.1),\n };\n}\n\n/**\n * What a canvas simulates with when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be** — for the reason its twin was: it is `simFrom()`, a\n * second public name for a value already available on request, exported only because `sim` took a\n * whole `Sim` and a host changing `gravity` had to supply the other five.\n */\nexport const DEFAULT_SIM: Sim = simFrom();\n\n/**\n * The coefficients a caller wants different, over the ones this package chose.\n *\n * Flat, so a spread is the whole merge. `undefined` returns the shared constant rather than a copy:\n * the renderer re-heats when the forces change, so a fresh object\n * per render would put energy back into a settled layout on every render.\n */\nexport function resolveSim(patch?: Partial<Sim>): Sim {\n if (!patch) return DEFAULT_SIM;\n return { ...DEFAULT_SIM, ...patch };\n}\n"],"names":["simFrom","values","num","key","fallback","value","DEFAULT_SIM","resolveSim","patch"],"mappings":"AAqCO,SAASA,EAAQC,IAAuD,IAAS;AACtF,QAAMC,IAAM,CAACC,GAAaC,MAAqB;AAC7C,UAAMC,IAAQ,OAAO,WAAWJ,EAAOE,CAAG,KAAK,EAAE;AACjD,WAAO,OAAO,SAASE,CAAK,IAAIA,IAAQD;AAAA,EAC1C;AACA,SAAO;AAAA,IACL,SAASF,EAAI,WAAW,IAAI;AAAA,IAC5B,WAAWA,EAAI,aAAa,GAAG;AAAA,IAC/B,YAAYA,EAAI,eAAe,GAAG;AAAA,IAClC,cAAcA,EAAI,iBAAiB,EAAE;AAAA,IACrC,UAAUA,EAAI,YAAY,IAAI;AAAA,IAC9B,SAASA,EAAI,WAAW,GAAG;AAAA,EAAA;AAE/B;AASO,MAAMI,IAAmBN,EAAA;AASzB,SAASO,EAAWC,GAA2B;AACpD,SAAKA,IACE,EAAE,GAAGF,GAAa,GAAGE,EAAA,IADTF;AAErB;"}
@@ -0,0 +1,33 @@
1
+ import { Geometry } from '../core/load';
2
+ import { GraphOptions } from '../core/state';
3
+ /**
4
+ * **What the renderer decides from the data**, apart from cosmos.gl so it can be read and tested
5
+ * without a GPU: how long a layout cools, when it hides links, whether it runs, and what a filter
6
+ * or a pick leaves in full colour.
7
+ */
8
+ /**
9
+ * How long a layout cools, in frames: cosmos.gl's alpha reaches its floor after exactly this many.
10
+ * Measured on the gate in `/docs/graph/layout`: the shape is done in one to three seconds at every
11
+ * size, so a layout that runs on is spending frames, not reaching a better picture — 600 settles
12
+ * 200,000 vertices in about ten seconds at sixty frames, and past that 240 does the same at half the
13
+ * frame rate.
14
+ */
15
+ export declare const decayFor: (size: number) => number;
16
+ /**
17
+ * Past this many links a running layout does not draw them: drawing them is the cost, not the
18
+ * forces — at 800,000 links a layout ticked 14 times a second with them and 51 without (the gate).
19
+ * They come back when it settles.
20
+ */
21
+ export declare const LINKS_WHILE_RUNNING = 250000;
22
+ /** Whether a layout should run: as `simulate` says, else exactly when the positions are not the data's. */
23
+ export declare const simulating: (options: GraphOptions, geometry: Geometry | null) => boolean;
24
+ /**
25
+ * **What is in full colour**, and everything else is greyed out: the vertices the page's filter keeps
26
+ * and — when the reader picked some — that they picked; and the links whose two ends are both. cosmos.gl
27
+ * greys links apart from points, so the links are said out loud.
28
+ */
29
+ export declare function highlighted(geometry: Geometry, mask: Uint8Array | null, selection: readonly number[] | null): {
30
+ highlightedPointIndices: number[] | undefined;
31
+ highlightedLinkIndices: number[] | undefined;
32
+ };
33
+ //# sourceMappingURL=motion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../../src/render/motion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD;;;;GAIG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,MAAsC,CAAC;AAC/E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,2GAA2G;AAC3G,eAAO,MAAM,UAAU,GAAI,SAAS,YAAY,EAAE,UAAU,QAAQ,GAAG,IAAI,KAAG,OAClB,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,UAAU,GAAG,IAAI,EACvB,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAClC;IAAE,uBAAuB,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAAC,sBAAsB,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CAAE,CAWjG"}
@@ -0,0 +1,19 @@
1
+ const e = (n) => n > 2e5 ? 240 : 600, u = 25e4, c = (n, l) => n.simulate ?? (l !== null && !l.bound);
2
+ function f(n, l, s) {
3
+ if (l === null && s === null) return { highlightedPointIndices: void 0, highlightedLinkIndices: void 0 };
4
+ const t = new Uint8Array(n.size);
5
+ if (s === null) t.set(l);
6
+ else for (const i of s) i < n.size && (l === null || l[i]) && (t[i] = 1);
7
+ const d = [];
8
+ for (let i = 0; i < n.size; i++) t[i] && d.push(i);
9
+ const o = [], { links: h } = n;
10
+ for (let i = 0; i < h.length / 2; i++) t[h[2 * i]] && t[h[2 * i + 1]] && o.push(i);
11
+ return { highlightedPointIndices: d, highlightedLinkIndices: o };
12
+ }
13
+ export {
14
+ u as LINKS_WHILE_RUNNING,
15
+ e as decayFor,
16
+ f as highlighted,
17
+ c as simulating
18
+ };
19
+ //# sourceMappingURL=motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion.js","sources":["../../src/render/motion.ts"],"sourcesContent":["import type { Geometry } from \"../core/load\";\nimport type { GraphOptions } from \"../core/state\";\n\n/**\n * **What the renderer decides from the data**, apart from cosmos.gl so it can be read and tested\n * without a GPU: how long a layout cools, when it hides links, whether it runs, and what a filter\n * or a pick leaves in full colour.\n */\n\n/**\n * How long a layout cools, in frames: cosmos.gl's alpha reaches its floor after exactly this many.\n * Measured on the gate in `/docs/graph/layout`: the shape is done in one to three seconds at every\n * size, so a layout that runs on is spending frames, not reaching a better picture — 600 settles\n * 200,000 vertices in about ten seconds at sixty frames, and past that 240 does the same at half the\n * frame rate.\n */\nexport const decayFor = (size: number): number => (size > 200_000 ? 240 : 600);\n/**\n * Past this many links a running layout does not draw them: drawing them is the cost, not the\n * forces — at 800,000 links a layout ticked 14 times a second with them and 51 without (the gate).\n * They come back when it settles.\n */\nexport const LINKS_WHILE_RUNNING = 250_000;\n\n/** Whether a layout should run: as `simulate` says, else exactly when the positions are not the data's. */\nexport const simulating = (options: GraphOptions, geometry: Geometry | null): boolean =>\n options.simulate ?? (geometry !== null && !geometry.bound);\n\n/**\n * **What is in full colour**, and everything else is greyed out: the vertices the page's filter keeps\n * and — when the reader picked some — that they picked; and the links whose two ends are both. cosmos.gl\n * greys links apart from points, so the links are said out loud.\n */\nexport function highlighted(\n geometry: Geometry,\n mask: Uint8Array | null,\n selection: readonly number[] | null,\n): { highlightedPointIndices: number[] | undefined; highlightedLinkIndices: number[] | undefined } {\n if (mask === null && selection === null) return { highlightedPointIndices: undefined, highlightedLinkIndices: undefined };\n const on = new Uint8Array(geometry.size);\n if (selection === null) on.set(mask as Uint8Array);\n else for (const id of selection) if (id < geometry.size && (mask === null || mask[id])) on[id] = 1;\n const points: number[] = [];\n for (let id = 0; id < geometry.size; id++) if (on[id]) points.push(id);\n const linkIds: number[] = [];\n const { links } = geometry;\n for (let e = 0; e < links.length / 2; e++) if (on[links[2 * e] as number] && on[links[2 * e + 1] as number]) linkIds.push(e);\n return { highlightedPointIndices: points, highlightedLinkIndices: linkIds };\n}\n"],"names":["decayFor","size","LINKS_WHILE_RUNNING","simulating","options","geometry","highlighted","mask","selection","on","id","points","linkIds","links","e"],"mappings":"AAgBO,MAAMA,IAAW,CAACC,MAA0BA,IAAO,MAAU,MAAM,KAM7DC,IAAsB,MAGtBC,IAAa,CAACC,GAAuBC,MAChDD,EAAQ,aAAaC,MAAa,QAAQ,CAACA,EAAS;AAO/C,SAASC,EACdD,GACAE,GACAC,GACiG;AACjG,MAAID,MAAS,QAAQC,MAAc,aAAa,EAAE,yBAAyB,QAAW,wBAAwB,OAAA;AAC9G,QAAMC,IAAK,IAAI,WAAWJ,EAAS,IAAI;AACvC,MAAIG,MAAc,KAAM,CAAAC,EAAG,IAAIF,CAAkB;AAAA,MAC5C,YAAWG,KAAMF,EAAW,CAAIE,IAAKL,EAAS,SAASE,MAAS,QAAQA,EAAKG,CAAE,OAAID,EAAGC,CAAE,IAAI;AACjG,QAAMC,IAAmB,CAAA;AACzB,WAASD,IAAK,GAAGA,IAAKL,EAAS,MAAMK,IAAM,CAAID,EAAGC,CAAE,KAAGC,EAAO,KAAKD,CAAE;AACrE,QAAME,IAAoB,CAAA,GACpB,EAAE,OAAAC,MAAUR;AAClB,WAASS,IAAI,GAAGA,IAAID,EAAM,SAAS,GAAGC,IAAK,CAAIL,EAAGI,EAAM,IAAIC,CAAC,CAAW,KAAKL,EAAGI,EAAM,IAAIC,IAAI,CAAC,CAAW,KAAGF,EAAQ,KAAKE,CAAC;AAC3H,SAAO,EAAE,yBAAyBH,GAAQ,wBAAwBC,EAAA;AACpE;"}
@@ -12,11 +12,9 @@ export interface Placement {
12
12
  readonly dx: number;
13
13
  readonly dy: number;
14
14
  }
15
- /** No extent — no row has a position — so nothing moves and nothing is drawn. */
16
- export declare const UNPLACED: Placement;
17
15
  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;
16
+ /** Bound positions in the square. A vertex with no value stays `NaN`, which cosmos.gl does not draw. */
17
+ export declare function placed(positions: Float32Array, at: Placement): Float32Array;
20
18
  /** The extent's corners in the square, for a camera that frames it. */
21
19
  export declare function cornersOf(extent: Extent, at: Placement): number[];
22
20
  export {};
@@ -1 +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"}
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,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAKlE;AAED,wGAAwG;AACxG,wBAAgB,MAAM,CAAC,SAAS,EAAE,YAAY,EAAE,EAAE,EAAE,SAAS,GAAG,YAAY,CAO3E;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,GAAG,MAAM,EAAE,CAEjE"}
@@ -1,23 +1,19 @@
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 };
1
+ function l(r, c) {
2
+ const y = Math.max(r.w, r.h, 2), d = y > c ? c / y : 1, k = y * d;
3
+ return { side: k, k: d, dx: (k - r.w * d) / 2 - r.x * d, dy: (k - r.h * d) / 2 - r.y * d };
5
4
  }
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;
5
+ function u(r, c) {
6
+ const y = new Float32Array(r.length);
7
+ for (let d = 0; d < r.length; d += 2)
8
+ y[d] = r[d] * c.k + c.dx, y[d + 1] = r[d + 1] * c.k + c.dy;
9
+ return y;
13
10
  }
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];
11
+ function f(r, c) {
12
+ return [r.x * c.k + c.dx, r.y * c.k + c.dy, (r.x + r.w) * c.k + c.dx, (r.y + r.h) * c.k + c.dy];
16
13
  }
17
14
  export {
18
- u as UNPLACED,
19
- k as cornersOf,
20
- y as placed,
15
+ f as cornersOf,
16
+ u as placed,
21
17
  l as placementOf
22
18
  };
23
19
  //# sourceMappingURL=placement.js.map
@@ -1 +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
+ {"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\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/** Bound positions in the square. A vertex with no value stays `NaN`, which cosmos.gl does not draw. */\nexport function placed(positions: Float32Array, at: Placement): Float32Array {\n const out = new Float32Array(positions.length);\n for (let i = 0; i < positions.length; i += 2) {\n out[i] = (positions[i] as number) * at.k + at.dx;\n out[i + 1] = (positions[i + 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":["placementOf","extent","box","longest","k","side","placed","positions","at","out","i","cornersOf"],"mappings":"AAiBO,SAASA,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,GAAyBC,GAA6B;AAC3E,QAAMC,IAAM,IAAI,aAAaF,EAAU,MAAM;AAC7C,WAASG,IAAI,GAAGA,IAAIH,EAAU,QAAQG,KAAK;AACzC,IAAAD,EAAIC,CAAC,IAAKH,EAAUG,CAAC,IAAeF,EAAG,IAAIA,EAAG,IAC9CC,EAAIC,IAAI,CAAC,IAAKH,EAAUG,IAAI,CAAC,IAAeF,EAAG,IAAIA,EAAG;AAExD,SAAOC;AACT;AAGO,SAASE,EAAUV,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;"}
@@ -23,15 +23,16 @@ export interface Renderer extends GraphCommands {
23
23
  * **cosmos.gl's lifetime, and the frame that renders once.** Built once per element; it subscribes to
24
24
  * the store and discharges what changed in one `requestAnimationFrame`:
25
25
  *
26
- * - a new graph uploads positions and links, once per corpus; a filter uploads positions alone;
27
- * - a binding, a look or a theme change uploads colours, sizes and shapes and nothing else;
28
- * - a selection, a focus or a pin sets config and uploads nothing, and never calls `render()`:
29
- * `render()` walks every point and link in JS, and `setConfigPartial` asks for its own frame;
26
+ * - a new graph uploads positions and links, once per corpus and per position binding;
27
+ * - a binding, a look or a theme change uploads colours, sizes, shapes and clusters and nothing else;
28
+ * - a filter, a selection, a focus or a pin sets config and uploads nothing, and never calls
29
+ * `render()`: a vertex the page's filter does not keep is greyed out, Cosmograph's way, so the
30
+ * points never move because a chart was brushed;
30
31
  * - the camera reaches nothing: cosmos.gl moves it, and the overlays follow.
31
32
  *
32
33
  * A vertex's id is its index here, so nothing is resolved between the store and the buffers.
33
34
  * `transitionDuration` is 0: the default animates every upload for 800 ms and keeps the loop awake.
34
- * The live layout is off unless `simulate` or the toolbar asks, and runs from the current positions.
35
+ * The layout runs on its own when `x` and `y` are unbound, and from the current positions.
35
36
  * Every callback reads the store's latest options, so a host's inline `onFailure` never rebuilds it.
36
37
  * A failure to draw at all marks the store `failed` through `unrenderable`, never `onFailure` alone.
37
38
  */