@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.
- package/dist/core/categories.d.ts +1 -1
- package/dist/core/categories.d.ts.map +1 -1
- package/dist/core/categories.js.map +1 -1
- package/dist/core/channels.d.ts +12 -6
- package/dist/core/channels.d.ts.map +1 -1
- package/dist/core/channels.js +15 -12
- package/dist/core/channels.js.map +1 -1
- package/dist/core/client.d.ts +26 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +62 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/error.d.ts +3 -3
- package/dist/core/error.d.ts.map +1 -1
- package/dist/core/error.js.map +1 -1
- package/dist/core/load.d.ts +30 -39
- package/dist/core/load.d.ts.map +1 -1
- package/dist/core/load.js +63 -117
- package/dist/core/load.js.map +1 -1
- package/dist/core/source.d.ts +90 -0
- package/dist/core/source.d.ts.map +1 -0
- package/dist/core/source.js +105 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/state.d.ts +19 -20
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +137 -157
- package/dist/core/store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/parts/graph-canvas.d.ts.map +1 -1
- package/dist/parts/graph-canvas.js +49 -48
- package/dist/parts/graph-canvas.js.map +1 -1
- package/dist/parts/graph-counts.js +4 -4
- package/dist/parts/graph-counts.js.map +1 -1
- package/dist/parts/graph-inspector.d.ts +3 -3
- package/dist/parts/graph-inspector.d.ts.map +1 -1
- package/dist/parts/graph-inspector.js +45 -43
- package/dist/parts/graph-inspector.js.map +1 -1
- package/dist/parts/graph-search.d.ts +7 -7
- package/dist/parts/graph-search.d.ts.map +1 -1
- package/dist/parts/graph-search.js +60 -69
- package/dist/parts/graph-search.js.map +1 -1
- package/dist/parts/graph-toolbar.js +1 -1
- package/dist/parts/graph-toolbar.js.map +1 -1
- package/dist/react/use-graph.js +31 -31
- package/dist/react/use-graph.js.map +1 -1
- package/dist/render/graph-sim.d.ts +1 -1
- package/dist/render/graph-sim.d.ts.map +1 -1
- package/dist/render/graph-sim.js.map +1 -1
- package/dist/render/motion.d.ts +33 -0
- package/dist/render/motion.d.ts.map +1 -0
- package/dist/render/motion.js +19 -0
- package/dist/render/motion.js.map +1 -0
- package/dist/render/placement.d.ts +2 -4
- package/dist/render/placement.d.ts.map +1 -1
- package/dist/render/placement.js +12 -16
- package/dist/render/placement.js.map +1 -1
- package/dist/render/renderer.d.ts +6 -5
- package/dist/render/renderer.d.ts.map +1 -1
- package/dist/render/renderer.js +124 -113
- package/dist/render/renderer.js.map +1 -1
- package/dist/section.js +1 -1
- package/dist/section.js.map +1 -1
- package/package.json +8 -10
- package/dist/core/detail.d.ts +0 -26
- package/dist/core/detail.d.ts.map +0 -1
- package/dist/core/detail.js +0 -63
- package/dist/core/detail.js.map +0 -1
- package/dist/core/filter.d.ts +0 -23
- package/dist/core/filter.d.ts.map +0 -1
- package/dist/core/filter.js +0 -95
- package/dist/core/filter.js.map +0 -1
|
@@ -1,81 +1,72 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as
|
|
3
|
-
import {
|
|
4
|
-
import { SearchIcon as
|
|
5
|
-
import { useState as
|
|
6
|
-
import { nameOf as
|
|
7
|
-
import { bindingOf as
|
|
8
|
-
import {
|
|
9
|
-
import { useGraphContext as
|
|
10
|
-
import { useGraphState as
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
53
|
-
disabled:
|
|
54
|
-
onInputValueChange: (e) =>
|
|
43
|
+
collection: m,
|
|
44
|
+
disabled: a === null,
|
|
45
|
+
onInputValueChange: (e) => T(e.inputValue),
|
|
55
46
|
onValueChange: (e) => {
|
|
56
|
-
const
|
|
57
|
-
|
|
47
|
+
const o = e.value[0];
|
|
48
|
+
o !== void 0 && l.reveal(Number(o));
|
|
58
49
|
},
|
|
59
50
|
children: [
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
|
|
51
|
+
/* @__PURE__ */ n(
|
|
52
|
+
j,
|
|
62
53
|
{
|
|
63
|
-
"aria-invalid":
|
|
64
|
-
className:
|
|
65
|
-
placeholder:
|
|
66
|
-
size:
|
|
67
|
-
children: /* @__PURE__ */
|
|
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__ */
|
|
71
|
-
/* @__PURE__ */
|
|
72
|
-
|
|
73
|
-
/* @__PURE__ */
|
|
74
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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;;;;"}
|
package/dist/react/use-graph.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useRef as
|
|
3
|
-
import { createGraph as
|
|
4
|
-
import { createRenderer as
|
|
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
|
|
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
|
|
12
|
-
const t =
|
|
11
|
+
function M(e) {
|
|
12
|
+
const t = N(e);
|
|
13
13
|
t.current = e;
|
|
14
|
-
const
|
|
15
|
-
(
|
|
16
|
-
...
|
|
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
|
-
), [
|
|
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(
|
|
31
|
-
}, [n,
|
|
32
|
-
}), [n]),
|
|
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
|
|
34
|
+
function A(e) {
|
|
35
35
|
let t = null;
|
|
36
|
-
const
|
|
36
|
+
const o = (n) => (...u) => {
|
|
37
37
|
var r;
|
|
38
|
-
return (r = t == null ? void 0 : t[n]) == null ? void 0 : r.call(t, ...
|
|
39
|
-
},
|
|
40
|
-
zoomBy:
|
|
41
|
-
fit:
|
|
42
|
-
pause:
|
|
43
|
-
resume:
|
|
44
|
-
restart:
|
|
45
|
-
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:
|
|
51
|
+
frameSelection: o("frameSelection"),
|
|
52
52
|
clear: () => {
|
|
53
53
|
e.select(null), e.focus(null);
|
|
54
54
|
},
|
|
55
|
-
select: (n,
|
|
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(
|
|
61
|
+
return m.set(c, {
|
|
62
62
|
store: e,
|
|
63
63
|
renderer: () => t,
|
|
64
|
-
attach(n,
|
|
64
|
+
attach(n, u) {
|
|
65
65
|
e.renderable();
|
|
66
66
|
let r = null;
|
|
67
67
|
try {
|
|
68
|
-
r =
|
|
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
|
-
}),
|
|
76
|
+
}), c;
|
|
77
77
|
}
|
|
78
78
|
export {
|
|
79
|
-
|
|
80
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
19
|
-
export declare function placed(
|
|
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,
|
|
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"}
|
package/dist/render/placement.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
7
|
-
const
|
|
8
|
-
for (let
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
15
|
-
return [
|
|
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
|
-
|
|
19
|
-
|
|
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\
|
|
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
|
|
27
|
-
* - a binding, a look or a theme change uploads colours, sizes and
|
|
28
|
-
* - a selection, a focus or a pin sets config and uploads nothing, and never calls
|
|
29
|
-
* `render()
|
|
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
|
|
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
|
*/
|