@kanzo-tech/graph 0.18.0 → 0.19.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/detail.d.ts +1 -1
- package/dist/core/detail.d.ts.map +1 -1
- package/dist/core/detail.js +30 -26
- package/dist/core/detail.js.map +1 -1
- package/dist/core/error.d.ts +16 -0
- package/dist/core/error.d.ts.map +1 -0
- package/dist/core/error.js +14 -0
- package/dist/core/error.js.map +1 -0
- package/dist/core/filter.d.ts +0 -13
- package/dist/core/filter.d.ts.map +1 -1
- package/dist/core/filter.js +47 -55
- package/dist/core/filter.js.map +1 -1
- package/dist/core/load.d.ts +5 -0
- package/dist/core/load.d.ts.map +1 -1
- package/dist/core/load.js +93 -78
- package/dist/core/load.js.map +1 -1
- package/dist/core/state.d.ts +14 -6
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +95 -93
- package/dist/core/store.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/parts/graph-canvas.js +45 -45
- package/dist/parts/graph-canvas.js.map +1 -1
- package/dist/parts/graph-inspector.d.ts.map +1 -1
- package/dist/parts/graph-inspector.js +48 -48
- package/dist/parts/graph-inspector.js.map +1 -1
- package/dist/react/use-graph.js +61 -55
- package/dist/react/use-graph.js.map +1 -1
- package/dist/render/renderer.d.ts +1 -0
- package/dist/render/renderer.d.ts.map +1 -1
- package/dist/render/renderer.js +89 -79
- package/dist/render/renderer.js.map +1 -1
- package/package.json +7 -7
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs as b, jsx as a } from "react/jsx-runtime";
|
|
3
|
-
import { useThemeTick as Y, categoricalCapacity as Z, Show as
|
|
3
|
+
import { useThemeTick as Y, categoricalCapacity as Z, Show as f, cn as ee } from "@kanzo-tech/ui";
|
|
4
4
|
import { useMemo as x, useCallback as $, useRef as te, useEffect as y, useState as oe } from "react";
|
|
5
5
|
import { nameOf as ne } from "../core/categories.js";
|
|
6
6
|
import { readTitles as re } from "../core/detail.js";
|
|
7
|
-
import { useGraphContext as
|
|
7
|
+
import { useGraphContext as D } from "../react/graph-root.js";
|
|
8
8
|
import { internalsOf as ae } from "../react/use-graph.js";
|
|
9
9
|
import { useGraphState as m, useGraphSnapshot as S } from "../react/use-graph-state.js";
|
|
10
10
|
import { resolveLook as se } from "../render/graph-looks.js";
|
|
@@ -12,28 +12,28 @@ import { scaleOf as le } from "../render/graph-model.js";
|
|
|
12
12
|
import { useGesture as ie, cursorChip as ce } from "./gesture.js";
|
|
13
13
|
import { useOverlays as pe } from "./overlays.js";
|
|
14
14
|
import { ShapeGlyph as de } from "./shape-glyph.js";
|
|
15
|
-
const
|
|
15
|
+
const F = "color-mix(in oklab, var(--primary) 17%, transparent)";
|
|
16
16
|
function me(u, d, i, p, s) {
|
|
17
|
-
const c = d.sizes,
|
|
17
|
+
const c = d.sizes, o = [];
|
|
18
18
|
if (c && p > 0)
|
|
19
19
|
for (let t = 0; t < u.size; t++) {
|
|
20
|
-
const
|
|
21
|
-
if (i && !i[t] || Number.isNaN(
|
|
22
|
-
let
|
|
23
|
-
for (;
|
|
24
|
-
|
|
20
|
+
const n = c[t];
|
|
21
|
+
if (i && !i[t] || Number.isNaN(n) || o.length === p && n <= c[o[p - 1]]) continue;
|
|
22
|
+
let r = o.length;
|
|
23
|
+
for (; r > 0 && c[o[r - 1]] < n; ) r--;
|
|
24
|
+
o.splice(r, 0, t), o.length > p && o.pop();
|
|
25
25
|
}
|
|
26
|
-
return s === null ||
|
|
26
|
+
return s === null || o.includes(s) ? o : [s, ...o];
|
|
27
27
|
}
|
|
28
|
-
function
|
|
29
|
-
const d =
|
|
28
|
+
function E(u) {
|
|
29
|
+
const d = D(), i = m((n) => n.corpus), p = m((n) => n.options.title), s = S((n) => n.geometry), [c, o] = oe(() => /* @__PURE__ */ new Map()), t = u.join(",");
|
|
30
30
|
return y(() => {
|
|
31
31
|
if (!i || !s || t === "") return;
|
|
32
|
-
const
|
|
33
|
-
return re(i, s, t.split(",").map(Number), p,
|
|
34
|
-
(
|
|
35
|
-
(
|
|
36
|
-
), () =>
|
|
32
|
+
const n = new AbortController();
|
|
33
|
+
return re(i, s, t.split(",").map(Number), p, n.signal).then(
|
|
34
|
+
(r) => !n.signal.aborted && o(r),
|
|
35
|
+
(r) => !n.signal.aborted && d.getState().options.onFailure(r)
|
|
36
|
+
), () => n.abort();
|
|
37
37
|
}, [d, i, s, t, p]), c;
|
|
38
38
|
}
|
|
39
39
|
const ue = {
|
|
@@ -41,13 +41,13 @@ const ue = {
|
|
|
41
41
|
loading: "Loading the graph…"
|
|
42
42
|
};
|
|
43
43
|
function Me({ children: u, className: d, slot: i, ...p }) {
|
|
44
|
-
const s =
|
|
44
|
+
const s = D(), { attach: c, renderer: o } = ae(s), t = m((e) => e.focus), n = m((e) => e.tool), r = m((e) => e.options), k = m((e) => {
|
|
45
45
|
var h;
|
|
46
46
|
return ((h = e.drawn) == null ? void 0 : h.domain) ?? e.domain;
|
|
47
|
-
}), w = m((e) => e.drawn === null ? ue[e.status] : void 0),
|
|
47
|
+
}), w = m((e) => e.drawn === null ? ue[e.status] : void 0), g = x(() => se(r.look), [r.look]), R = $(() => {
|
|
48
48
|
var e;
|
|
49
|
-
return ((e =
|
|
50
|
-
}, [
|
|
49
|
+
return ((e = o()) == null ? void 0 : e.graph) ?? null;
|
|
50
|
+
}, [o]), q = pe({ getGraph: R }), { cardRef: B, gridRef: J, hostRef: T, hoverAt: _, labelRef: K, schedule: v, setHovered: P, setLabelOrder: j, track: A } = q, I = te(null), G = S((e) => e.geometry), N = S((e) => e.encoding), W = S((e) => e.mask);
|
|
51
51
|
y(() => {
|
|
52
52
|
const e = I.current;
|
|
53
53
|
if (e)
|
|
@@ -56,25 +56,25 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
56
56
|
const Q = Y();
|
|
57
57
|
y(() => {
|
|
58
58
|
var e;
|
|
59
|
-
(e =
|
|
60
|
-
}, [
|
|
59
|
+
(e = o()) == null || e.repaint();
|
|
60
|
+
}, [o, Q]);
|
|
61
61
|
const C = x(
|
|
62
|
-
() => G && N ? me(G, N, W,
|
|
63
|
-
[G, N, W, t,
|
|
64
|
-
), M =
|
|
62
|
+
() => G && N ? me(G, N, W, g.labels, t) : [],
|
|
63
|
+
[G, N, W, t, g.labels]
|
|
64
|
+
), M = E(C), z = x(
|
|
65
65
|
() => C.flatMap((e) => M.get(e) ? [{ vertex: e, text: M.get(e) }] : []),
|
|
66
66
|
[C, M]
|
|
67
67
|
);
|
|
68
68
|
y(() => {
|
|
69
69
|
j(z.map((e) => e.vertex)), A(), v();
|
|
70
|
-
}, [z, v, j, A,
|
|
70
|
+
}, [z, v, j, A, g.grid]);
|
|
71
71
|
const H = ie({
|
|
72
72
|
getGraph: R,
|
|
73
73
|
getSelection: $(() => s.getState().selection, [s]),
|
|
74
74
|
commit: (e, h, X) => s.select(e ? [...e] : null, h, X),
|
|
75
75
|
setTool: s.setTool,
|
|
76
|
-
tool:
|
|
77
|
-
}), { active: U, drag: l, preview: O } = H, L = T.current ? Z(T.current) : void 0, V = x(() => le(
|
|
76
|
+
tool: n
|
|
77
|
+
}), { active: U, drag: l, preview: O } = H, L = T.current ? Z(T.current) : void 0, V = x(() => le(r, L), [r, L]);
|
|
78
78
|
return /* @__PURE__ */ b(
|
|
79
79
|
"div",
|
|
80
80
|
{
|
|
@@ -84,7 +84,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
84
84
|
ref: T,
|
|
85
85
|
children: [
|
|
86
86
|
/* @__PURE__ */ a("div", { className: "size-full", "data-slot": "graph-canvas-surface", ref: I }),
|
|
87
|
-
/* @__PURE__ */ a(
|
|
87
|
+
/* @__PURE__ */ a(f, { when: g.grid, children: /* @__PURE__ */ a(
|
|
88
88
|
"div",
|
|
89
89
|
{
|
|
90
90
|
className: "pointer-events-none absolute inset-0",
|
|
@@ -92,7 +92,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
92
92
|
style: { backgroundImage: "radial-gradient(var(--border) 1px, transparent 1px)" }
|
|
93
93
|
}
|
|
94
94
|
) }),
|
|
95
|
-
/* @__PURE__ */ a(
|
|
95
|
+
/* @__PURE__ */ a(f, { when: g.vignette, children: /* @__PURE__ */ a(
|
|
96
96
|
"div",
|
|
97
97
|
{
|
|
98
98
|
className: "pointer-events-none absolute inset-0",
|
|
@@ -115,14 +115,14 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
115
115
|
cardRef: B,
|
|
116
116
|
domain: k,
|
|
117
117
|
encoding: N,
|
|
118
|
-
options:
|
|
118
|
+
options: r,
|
|
119
119
|
scale: V,
|
|
120
120
|
schedule: v,
|
|
121
121
|
setHovered: P
|
|
122
122
|
}
|
|
123
123
|
)
|
|
124
124
|
] }),
|
|
125
|
-
/* @__PURE__ */ a(
|
|
125
|
+
/* @__PURE__ */ a(f, { when: U !== null, children: /* @__PURE__ */ b("div", { className: "absolute inset-0 cursor-crosshair", "data-slot": "graph-canvas-gesture", ...H.handlers, children: [
|
|
126
126
|
l && /* @__PURE__ */ a("svg", { className: "pointer-events-none size-full", children: l.tool === "rect" ? /* @__PURE__ */ a(
|
|
127
127
|
"rect",
|
|
128
128
|
{
|
|
@@ -130,7 +130,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
130
130
|
height: Math.abs(l.to[1] - l.from[1]),
|
|
131
131
|
strokeDasharray: "4 3",
|
|
132
132
|
strokeWidth: 1,
|
|
133
|
-
style: { fill:
|
|
133
|
+
style: { fill: F },
|
|
134
134
|
width: Math.abs(l.to[0] - l.from[0]),
|
|
135
135
|
x: Math.min(l.from[0], l.to[0]),
|
|
136
136
|
y: Math.min(l.from[1], l.to[1])
|
|
@@ -141,10 +141,10 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
141
141
|
className: "stroke-primary",
|
|
142
142
|
points: l.path.map(([e, h]) => `${e},${h}`).join(" "),
|
|
143
143
|
strokeWidth: 1,
|
|
144
|
-
style: { fill:
|
|
144
|
+
style: { fill: F }
|
|
145
145
|
}
|
|
146
146
|
) }),
|
|
147
|
-
/* @__PURE__ */ a(
|
|
147
|
+
/* @__PURE__ */ a(f, { when: O !== null, children: /* @__PURE__ */ b(
|
|
148
148
|
"span",
|
|
149
149
|
{
|
|
150
150
|
className: "pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums",
|
|
@@ -157,7 +157,7 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
157
157
|
}
|
|
158
158
|
) })
|
|
159
159
|
] }) }),
|
|
160
|
-
/* @__PURE__ */ a(
|
|
160
|
+
/* @__PURE__ */ a(f, { when: w !== void 0, children: /* @__PURE__ */ a(
|
|
161
161
|
"p",
|
|
162
162
|
{
|
|
163
163
|
className: "pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs",
|
|
@@ -170,12 +170,12 @@ function Me({ children: u, className: d, slot: i, ...p }) {
|
|
|
170
170
|
}
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
|
-
function he({ cardRef: u, domain: d, encoding: i, options: p, scale: s, schedule: c, setHovered:
|
|
174
|
-
const t = m((w) => w.hovered),
|
|
173
|
+
function he({ cardRef: u, domain: d, encoding: i, options: p, scale: s, schedule: c, setHovered: o }) {
|
|
174
|
+
const t = m((w) => w.hovered), n = E(x(() => t === null ? [] : [t], [t]));
|
|
175
175
|
if (y(() => {
|
|
176
|
-
|
|
177
|
-
}, [t, c,
|
|
178
|
-
const
|
|
176
|
+
o(t), c();
|
|
177
|
+
}, [t, c, o, n]), t === null || !i || t >= i.ranks.length) return null;
|
|
178
|
+
const r = i.ranks[t] ?? 0, k = r < d.length ? ne(d[r], p.categories) : null;
|
|
179
179
|
return /* @__PURE__ */ b(
|
|
180
180
|
"div",
|
|
181
181
|
{
|
|
@@ -183,9 +183,9 @@ function he({ cardRef: u, domain: d, encoding: i, options: p, scale: s, schedule
|
|
|
183
183
|
"data-slot": "graph-canvas-card",
|
|
184
184
|
ref: u,
|
|
185
185
|
children: [
|
|
186
|
-
/* @__PURE__ */ a(de, { className: "size-2.5 shrink-0", color: s.color(
|
|
187
|
-
/* @__PURE__ */ a("span", { className: "truncate font-medium text-popover-foreground text-sm leading-none", children:
|
|
188
|
-
/* @__PURE__ */ a(
|
|
186
|
+
/* @__PURE__ */ a(de, { className: "size-2.5 shrink-0", color: s.color(r), shape: s.shape(r) }),
|
|
187
|
+
/* @__PURE__ */ a("span", { className: "truncate font-medium text-popover-foreground text-sm leading-none", children: n.get(t) || `#${t}` }),
|
|
188
|
+
/* @__PURE__ */ a(f, { when: k !== null, children: /* @__PURE__ */ a("span", { className: "text-muted-foreground text-xs", children: k }) })
|
|
189
189
|
]
|
|
190
190
|
}
|
|
191
191
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-canvas.js","sources":["../../src/parts/graph-canvas.tsx"],"sourcesContent":["\"use client\";\n\nimport { categoricalCapacity, cn, Show, useThemeTick } from \"@kanzo-tech/ui\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { readTitles } from \"../core/detail\";\nimport type { Encoding, Geometry } from \"../core/load\";\nimport type { GraphOptions } from \"../core/state\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { internalsOf } from \"../react/use-graph\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { resolveLook } from \"../render/graph-looks\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { cursorChip, useGesture } from \"./gesture\";\nimport { useOverlays } from \"./overlays\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphCanvasProps extends React.ComponentProps<\"div\"> {\n /** The chrome — a toolbar, a legend, an inspector — positioned over the surface. */\n children?: ReactNode;\n}\n\nconst WASH = \"color-mix(in oklab, var(--primary) 17%, transparent)\";\n\n/**\n * The biggest `budget` surviving vertices by the ramp, the focused one first. One pass, keeping the\n * best few in order: a sort of every vertex to name twenty-six of them is the cost this avoids.\n */\nfunction labelled(geometry: Geometry, encoding: Encoding, mask: Uint8Array | null, budget: number, focus: VertexId | null): VertexId[] {\n const ramp = encoding.sizes;\n const best: VertexId[] = [];\n if (ramp && budget > 0) {\n for (let id = 0; id < geometry.size; id++) {\n const value = ramp[id] as number;\n if ((mask && !mask[id]) || Number.isNaN(value)) continue;\n if (best.length === budget && value <= (ramp[best[budget - 1] as number] as number)) continue;\n let at = best.length;\n while (at > 0 && (ramp[best[at - 1] as number] as number) < value) at--;\n best.splice(at, 0, id);\n if (best.length > budget) best.pop();\n }\n }\n return focus === null || best.includes(focus) ? best : [focus, ...best];\n}\n\n/** A label's text is read for the vertices that carry one, never carried for every vertex. */\nfunction useTitles(vertices: readonly VertexId[]): ReadonlyMap<VertexId, string> {\n const api = useGraphContext();\n const corpus = useGraphState((s) => s.corpus);\n const title = useGraphState((s) => s.options.title);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const [titles, setTitles] = useState<ReadonlyMap<VertexId, string>>(() => new Map());\n const key = vertices.join(\",\");\n useEffect(() => {\n if (!corpus || !geometry || key === \"\") return;\n const aborter = new AbortController();\n readTitles(corpus, geometry, key.split(\",\").map(Number), title, aborter.signal).then(\n (found) => !aborter.signal.aborted && setTitles(found),\n (error: unknown) => !aborter.signal.aborted && api.getState().options.onFailure(error instanceof Error ? error.message : String(error)),\n );\n return () => aborter.abort();\n }, [api, corpus, geometry, key, title]);\n return titles;\n}\n\nconst WAITING: Partial<Record<string, string>> = {\n opening: \"Opening the corpus…\",\n loading: \"Loading the graph…\",\n};\n\n/**\n * **The element, and what the look declares over it** — the grid, the vignette, the labels and the\n * hover card are fields of `Look`, so drawing them is the canvas's; the marquee and lasso gesture is\n * too. Two layers: cosmos.gl fills the surface with a canvas of its own, so chrome parented there\n * would be a sibling inside an element the renderer resizes. The frame is `isolate`, so a host's\n * `z-index` on a child cannot escape into the page.\n *\n * The surface is never gated on the first answer: rendered behind a placeholder it deadlocks — no\n * element, no renderer, no camera, no question. Waiting is an overlay over an empty canvas.\n */\nexport function GraphCanvas({ children, className, slot, ...rest }: GraphCanvasProps) {\n const api = useGraphContext();\n const { attach, renderer } = internalsOf(api);\n const focus = useGraphState((s) => s.focus);\n const tool = useGraphState((s) => s.tool);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const waiting = useGraphState((s) => (s.drawn === null ? WAITING[s.status] : undefined));\n const look = useMemo(() => resolveLook(options.look), [options.look]);\n const getGraph = useCallback(() => renderer()?.graph ?? null, [renderer]);\n const overlays = useOverlays({ getGraph });\n const { cardRef, gridRef, hostRef, hoverAt, labelRef, schedule, setHovered, setLabelOrder, track } = overlays;\n const surfaceRef = useRef<HTMLDivElement>(null);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const mask = useGraphSnapshot((s) => s.mask);\n\n useEffect(() => {\n const surface = surfaceRef.current;\n if (!surface) return;\n return attach(surface, { onFrame: schedule, onHover: hoverAt });\n }, [attach, hoverAt, schedule]);\n\n const themeTick = useThemeTick();\n useEffect(() => {\n renderer()?.repaint();\n }, [renderer, themeTick]);\n\n const labelledIds = useMemo(\n () => (geometry && encoding ? labelled(geometry, encoding, mask, look.labels, focus) : []),\n [geometry, encoding, mask, focus, look.labels],\n );\n const titles = useTitles(labelledIds);\n const labels = useMemo(\n () => labelledIds.flatMap((vertex) => (titles.get(vertex) ? [{ vertex, text: titles.get(vertex) as string }] : [])),\n [labelledIds, titles],\n );\n useEffect(() => {\n setLabelOrder(labels.map((label) => label.vertex));\n track();\n schedule();\n }, [labels, schedule, setLabelOrder, track, look.grid]);\n\n const gesture = useGesture({\n getGraph,\n getSelection: useCallback(() => api.getState().selection, [api]),\n commit: (vertices, source, label) => api.select(vertices ? [...vertices] : null, source, label),\n setTool: api.setTool,\n tool,\n });\n const { active, drag, preview } = gesture;\n\n const capacity = hostRef.current ? categoricalCapacity(hostRef.current) : undefined;\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n\n return (\n <div\n {...rest}\n className={cn(\"relative isolate size-full overflow-hidden bg-background\", className)}\n data-slot={slot ?? \"graph-canvas\"}\n ref={hostRef}\n >\n <div className=\"size-full\" data-slot=\"graph-canvas-surface\" ref={surfaceRef} />\n <Show when={look.grid}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n ref={gridRef}\n style={{ backgroundImage: \"radial-gradient(var(--border) 1px, transparent 1px)\" }}\n />\n </Show>\n <Show when={look.vignette}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n style={{ background: \"radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)\" }}\n />\n </Show>\n <div className=\"pointer-events-none absolute inset-0 overflow-hidden\">\n {labels.map((label) => (\n <span\n className=\"absolute top-0 left-0 whitespace-nowrap font-medium text-[10px] text-foreground leading-none opacity-0 transition-opacity [text-shadow:0_0_3px_var(--background),0_0_6px_var(--background)]\"\n key={String(label.vertex)}\n ref={labelRef(label.vertex)}\n >\n {label.text}\n </span>\n ))}\n <HoverCard\n cardRef={cardRef}\n domain={domain}\n encoding={encoding}\n options={options}\n scale={scale}\n schedule={schedule}\n setHovered={setHovered}\n />\n </div>\n <Show when={active !== null}>\n <div className=\"absolute inset-0 cursor-crosshair\" data-slot=\"graph-canvas-gesture\" {...gesture.handlers}>\n {drag && (\n <svg className=\"pointer-events-none size-full\">\n {drag.tool === \"rect\" ? (\n <rect\n className=\"stroke-primary\"\n height={Math.abs(drag.to[1] - drag.from[1])}\n strokeDasharray=\"4 3\"\n strokeWidth={1}\n style={{ fill: WASH }}\n width={Math.abs(drag.to[0] - drag.from[0])}\n x={Math.min(drag.from[0], drag.to[0])}\n y={Math.min(drag.from[1], drag.to[1])}\n />\n ) : (\n drag.path.length > 1 && (\n <polygon\n className=\"stroke-primary\"\n points={drag.path.map(([x, y]) => `${x},${y}`).join(\" \")}\n strokeWidth={1}\n style={{ fill: WASH }}\n />\n )\n )}\n </svg>\n )}\n <Show when={preview !== null}>\n <span\n className=\"pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums\"\n style={cursorChip(drag)}\n >\n {preview} node{preview === 1 ? \"\" : \"s\"}\n </span>\n </Show>\n </div>\n </Show>\n <Show when={waiting !== undefined}>\n <p\n className=\"pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs\"\n data-slot=\"graph-canvas-waiting\"\n >\n {waiting}\n </p>\n </Show>\n {children}\n </div>\n );\n}\n\ninterface HoverCardProps {\n cardRef: React.RefObject<HTMLDivElement | null>;\n domain: readonly unknown[];\n encoding: Encoding | null;\n options: GraphOptions;\n scale: ReturnType<typeof scaleOf>;\n schedule: () => void;\n setHovered: (vertex: VertexId | null) => void;\n}\n\n/** The only part that re-renders on a hover: the canvas and its labels do not. */\nfunction HoverCard({ cardRef, domain, encoding, options, scale, schedule, setHovered }: HoverCardProps) {\n const hovered = useGraphState((s) => s.hovered);\n const titles = useTitles(useMemo(() => (hovered === null ? [] : [hovered]), [hovered]));\n useEffect(() => {\n setHovered(hovered);\n schedule();\n }, [hovered, schedule, setHovered, titles]);\n\n if (hovered === null || !encoding || hovered >= encoding.ranks.length) return null;\n const rank = encoding.ranks[hovered] ?? 0;\n const category = rank < domain.length ? nameOf(domain[rank], options.categories) : null;\n return (\n <div\n className=\"absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg\"\n data-slot=\"graph-canvas-card\"\n ref={cardRef}\n >\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(rank)} shape={scale.shape(rank)} />\n <span className=\"truncate font-medium text-popover-foreground text-sm leading-none\">\n {titles.get(hovered) || `#${hovered}`}\n </span>\n <Show when={category !== null}>\n <span className=\"text-muted-foreground text-xs\">{category}</span>\n </Show>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAuBA;AAMA;AACE;AAEA;AACE;AACE;AAEA;AACA;AACA;AACA;AAC+B;AAGnC;AACF;AAGA;AACE;AAMA;AACE;AACA;AACA;AAAgF;AACzB;AACiF;AAEnH;AAGzB;AAEA;AAAiD;AACtC;AAEX;AAYO;AACL;;AAKoC;AAAqB;;AAGtB;AAAqB;AAQxD;AACE;AACA;AACA;AAA8D;AAGhE;AACA;;AACE;AAAY;AAGd;AAAoB;AACqE;AAC1C;AAGhC;AACqG;AAC9F;AAEtB;AACE;AAEA;AAGF;AAA2B;AACzB;AAC+D;AAC+B;AACjF;AACb;AAOF;AACE;AAAC;AAAA;AACK;AAC+E;AAChE;AACd;AAEL;AAA6E;AAE3E;AAAC;AAAA;AACW;AACL;AACqB;AAAsD;AAEpF;AAEE;AAAC;AAAA;AACW;AACW;AAAuF;AAEhH;AAEG;AACC;AAAC;AAAA;AACW;AAEgB;AAEnB;AAAA;AAHiB;AAK3B;AACD;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAEJ;AAGK;AAGK;AAAC;AAAA;AACW;AACgC;AAC1B;AACH;AACE;AAC0B;AACL;AACA;AAAA;AAIpC;AAAC;AAAA;AACW;AAC6C;AAC1C;AACE;AAAK;AAI5B;AAGA;AAAC;AAAA;AACW;AACY;AAErB;AAAA;AAAQ;AAA2B;AAAA;AAAA;AAExC;AAEJ;AAEE;AAAC;AAAA;AACW;AACA;AAET;AAAA;AAEL;AACC;AAAA;AAAA;AAGP;AAaA;AACE;AAOA;AAJE;AACA;AAIF;AAEA;AACE;AAAC;AAAA;AACW;AACA;AACL;AAEL;AAA8F;AAG9F;AAGA;AAAA;AAAA;AAGN;;;;"}
|
|
1
|
+
{"version":3,"file":"graph-canvas.js","sources":["../../src/parts/graph-canvas.tsx"],"sourcesContent":["\"use client\";\n\nimport { categoricalCapacity, cn, Show, useThemeTick } from \"@kanzo-tech/ui\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { readTitles } from \"../core/detail\";\nimport type { Encoding, Geometry } from \"../core/load\";\nimport type { GraphOptions } from \"../core/state\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { internalsOf } from \"../react/use-graph\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { resolveLook } from \"../render/graph-looks\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { cursorChip, useGesture } from \"./gesture\";\nimport { useOverlays } from \"./overlays\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphCanvasProps extends React.ComponentProps<\"div\"> {\n /** The chrome — a toolbar, a legend, an inspector — positioned over the surface. */\n children?: ReactNode;\n}\n\nconst WASH = \"color-mix(in oklab, var(--primary) 17%, transparent)\";\n\n/**\n * The biggest `budget` surviving vertices by the ramp, the focused one first. One pass, keeping the\n * best few in order: a sort of every vertex to name twenty-six of them is the cost this avoids.\n */\nfunction labelled(geometry: Geometry, encoding: Encoding, mask: Uint8Array | null, budget: number, focus: VertexId | null): VertexId[] {\n const ramp = encoding.sizes;\n const best: VertexId[] = [];\n if (ramp && budget > 0) {\n for (let id = 0; id < geometry.size; id++) {\n const value = ramp[id] as number;\n if ((mask && !mask[id]) || Number.isNaN(value)) continue;\n if (best.length === budget && value <= (ramp[best[budget - 1] as number] as number)) continue;\n let at = best.length;\n while (at > 0 && (ramp[best[at - 1] as number] as number) < value) at--;\n best.splice(at, 0, id);\n if (best.length > budget) best.pop();\n }\n }\n return focus === null || best.includes(focus) ? best : [focus, ...best];\n}\n\n/** A label's text is read for the vertices that carry one, never carried for every vertex. */\nfunction useTitles(vertices: readonly VertexId[]): ReadonlyMap<VertexId, string> {\n const api = useGraphContext();\n const corpus = useGraphState((s) => s.corpus);\n const title = useGraphState((s) => s.options.title);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const [titles, setTitles] = useState<ReadonlyMap<VertexId, string>>(() => new Map());\n const key = vertices.join(\",\");\n useEffect(() => {\n if (!corpus || !geometry || key === \"\") return;\n const aborter = new AbortController();\n readTitles(corpus, geometry, key.split(\",\").map(Number), title, aborter.signal).then(\n (found) => !aborter.signal.aborted && setTitles(found),\n (error: unknown) => !aborter.signal.aborted && api.getState().options.onFailure(error),\n );\n return () => aborter.abort();\n }, [api, corpus, geometry, key, title]);\n return titles;\n}\n\nconst WAITING: Partial<Record<string, string>> = {\n opening: \"Opening the corpus…\",\n loading: \"Loading the graph…\",\n};\n\n/**\n * **The element, and what the look declares over it** — the grid, the vignette, the labels and the\n * hover card are fields of `Look`, so drawing them is the canvas's; the marquee and lasso gesture is\n * too. Two layers: cosmos.gl fills the surface with a canvas of its own, so chrome parented there\n * would be a sibling inside an element the renderer resizes. The frame is `isolate`, so a host's\n * `z-index` on a child cannot escape into the page.\n *\n * The surface is never gated on the first answer: rendered behind a placeholder it deadlocks — no\n * element, no renderer, no camera, no question. Waiting is an overlay over an empty canvas.\n */\nexport function GraphCanvas({ children, className, slot, ...rest }: GraphCanvasProps) {\n const api = useGraphContext();\n const { attach, renderer } = internalsOf(api);\n const focus = useGraphState((s) => s.focus);\n const tool = useGraphState((s) => s.tool);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const waiting = useGraphState((s) => (s.drawn === null ? WAITING[s.status] : undefined));\n const look = useMemo(() => resolveLook(options.look), [options.look]);\n const getGraph = useCallback(() => renderer()?.graph ?? null, [renderer]);\n const overlays = useOverlays({ getGraph });\n const { cardRef, gridRef, hostRef, hoverAt, labelRef, schedule, setHovered, setLabelOrder, track } = overlays;\n const surfaceRef = useRef<HTMLDivElement>(null);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const encoding = useGraphSnapshot((s) => s.encoding);\n const mask = useGraphSnapshot((s) => s.mask);\n\n useEffect(() => {\n const surface = surfaceRef.current;\n if (!surface) return;\n return attach(surface, { onFrame: schedule, onHover: hoverAt });\n }, [attach, hoverAt, schedule]);\n\n const themeTick = useThemeTick();\n useEffect(() => {\n renderer()?.repaint();\n }, [renderer, themeTick]);\n\n const labelledIds = useMemo(\n () => (geometry && encoding ? labelled(geometry, encoding, mask, look.labels, focus) : []),\n [geometry, encoding, mask, focus, look.labels],\n );\n const titles = useTitles(labelledIds);\n const labels = useMemo(\n () => labelledIds.flatMap((vertex) => (titles.get(vertex) ? [{ vertex, text: titles.get(vertex) as string }] : [])),\n [labelledIds, titles],\n );\n useEffect(() => {\n setLabelOrder(labels.map((label) => label.vertex));\n track();\n schedule();\n }, [labels, schedule, setLabelOrder, track, look.grid]);\n\n const gesture = useGesture({\n getGraph,\n getSelection: useCallback(() => api.getState().selection, [api]),\n commit: (vertices, source, label) => api.select(vertices ? [...vertices] : null, source, label),\n setTool: api.setTool,\n tool,\n });\n const { active, drag, preview } = gesture;\n\n const capacity = hostRef.current ? categoricalCapacity(hostRef.current) : undefined;\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n\n return (\n <div\n {...rest}\n className={cn(\"relative isolate size-full overflow-hidden bg-background\", className)}\n data-slot={slot ?? \"graph-canvas\"}\n ref={hostRef}\n >\n <div className=\"size-full\" data-slot=\"graph-canvas-surface\" ref={surfaceRef} />\n <Show when={look.grid}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n ref={gridRef}\n style={{ backgroundImage: \"radial-gradient(var(--border) 1px, transparent 1px)\" }}\n />\n </Show>\n <Show when={look.vignette}>\n <div\n className=\"pointer-events-none absolute inset-0\"\n style={{ background: \"radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, var(--background) 100%)\" }}\n />\n </Show>\n <div className=\"pointer-events-none absolute inset-0 overflow-hidden\">\n {labels.map((label) => (\n <span\n className=\"absolute top-0 left-0 whitespace-nowrap font-medium text-[10px] text-foreground leading-none opacity-0 transition-opacity [text-shadow:0_0_3px_var(--background),0_0_6px_var(--background)]\"\n key={String(label.vertex)}\n ref={labelRef(label.vertex)}\n >\n {label.text}\n </span>\n ))}\n <HoverCard\n cardRef={cardRef}\n domain={domain}\n encoding={encoding}\n options={options}\n scale={scale}\n schedule={schedule}\n setHovered={setHovered}\n />\n </div>\n <Show when={active !== null}>\n <div className=\"absolute inset-0 cursor-crosshair\" data-slot=\"graph-canvas-gesture\" {...gesture.handlers}>\n {drag && (\n <svg className=\"pointer-events-none size-full\">\n {drag.tool === \"rect\" ? (\n <rect\n className=\"stroke-primary\"\n height={Math.abs(drag.to[1] - drag.from[1])}\n strokeDasharray=\"4 3\"\n strokeWidth={1}\n style={{ fill: WASH }}\n width={Math.abs(drag.to[0] - drag.from[0])}\n x={Math.min(drag.from[0], drag.to[0])}\n y={Math.min(drag.from[1], drag.to[1])}\n />\n ) : (\n drag.path.length > 1 && (\n <polygon\n className=\"stroke-primary\"\n points={drag.path.map(([x, y]) => `${x},${y}`).join(\" \")}\n strokeWidth={1}\n style={{ fill: WASH }}\n />\n )\n )}\n </svg>\n )}\n <Show when={preview !== null}>\n <span\n className=\"pointer-events-none absolute rounded-sm bg-primary px-1.5 py-0.5 font-medium text-[10px] text-primary-foreground tabular-nums\"\n style={cursorChip(drag)}\n >\n {preview} node{preview === 1 ? \"\" : \"s\"}\n </span>\n </Show>\n </div>\n </Show>\n <Show when={waiting !== undefined}>\n <p\n className=\"pointer-events-none absolute inset-0 grid place-items-center text-muted-foreground text-xs\"\n data-slot=\"graph-canvas-waiting\"\n >\n {waiting}\n </p>\n </Show>\n {children}\n </div>\n );\n}\n\ninterface HoverCardProps {\n cardRef: React.RefObject<HTMLDivElement | null>;\n domain: readonly unknown[];\n encoding: Encoding | null;\n options: GraphOptions;\n scale: ReturnType<typeof scaleOf>;\n schedule: () => void;\n setHovered: (vertex: VertexId | null) => void;\n}\n\n/** The only part that re-renders on a hover: the canvas and its labels do not. */\nfunction HoverCard({ cardRef, domain, encoding, options, scale, schedule, setHovered }: HoverCardProps) {\n const hovered = useGraphState((s) => s.hovered);\n const titles = useTitles(useMemo(() => (hovered === null ? [] : [hovered]), [hovered]));\n useEffect(() => {\n setHovered(hovered);\n schedule();\n }, [hovered, schedule, setHovered, titles]);\n\n if (hovered === null || !encoding || hovered >= encoding.ranks.length) return null;\n const rank = encoding.ranks[hovered] ?? 0;\n const category = rank < domain.length ? nameOf(domain[rank], options.categories) : null;\n return (\n <div\n className=\"absolute top-0 left-0 flex w-max max-w-60 items-center gap-2 rounded-lg border bg-popover px-2.5 py-2 opacity-0 shadow-lg\"\n data-slot=\"graph-canvas-card\"\n ref={cardRef}\n >\n <ShapeGlyph className=\"size-2.5 shrink-0\" color={scale.color(rank)} shape={scale.shape(rank)} />\n <span className=\"truncate font-medium text-popover-foreground text-sm leading-none\">\n {titles.get(hovered) || `#${hovered}`}\n </span>\n <Show when={category !== null}>\n <span className=\"text-muted-foreground text-xs\">{category}</span>\n </Show>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAuBA;AAMA;AACE;AAEA;AACE;AACE;AAEA;AACA;AACA;AACA;AAC+B;AAGnC;AACF;AAGA;AACE;AAMA;AACE;AACA;AACA;AAAgF;AACzB;AACgC;AAElE;AAGzB;AAEA;AAAiD;AACtC;AAEX;AAYO;AACL;;AAKoC;AAAqB;;AAGtB;AAAqB;AAQxD;AACE;AACA;AACA;AAA8D;AAGhE;AACA;;AACE;AAAY;AAGd;AAAoB;AACqE;AAC1C;AAGhC;AACqG;AAC9F;AAEtB;AACE;AAEA;AAGF;AAA2B;AACzB;AAC+D;AAC+B;AACjF;AACb;AAOF;AACE;AAAC;AAAA;AACK;AAC+E;AAChE;AACd;AAEL;AAA6E;AAE3E;AAAC;AAAA;AACW;AACL;AACqB;AAAsD;AAEpF;AAEE;AAAC;AAAA;AACW;AACW;AAAuF;AAEhH;AAEG;AACC;AAAC;AAAA;AACW;AAEgB;AAEnB;AAAA;AAHiB;AAK3B;AACD;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAEJ;AAGK;AAGK;AAAC;AAAA;AACW;AACgC;AAC1B;AACH;AACE;AAC0B;AACL;AACA;AAAA;AAIpC;AAAC;AAAA;AACW;AAC6C;AAC1C;AACE;AAAK;AAI5B;AAGA;AAAC;AAAA;AACW;AACY;AAErB;AAAA;AAAQ;AAA2B;AAAA;AAAA;AAExC;AAEJ;AAEE;AAAC;AAAA;AACW;AACA;AAET;AAAA;AAEL;AACC;AAAA;AAAA;AAGP;AAaA;AACE;AAOA;AAJE;AACA;AAIF;AAEA;AACE;AAAC;AAAA;AACW;AACA;AACL;AAEL;AAA8F;AAG9F;AAGA;AAAA;AAAA;AAGN;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-inspector.d.ts","sourceRoot":"","sources":["../../src/parts/graph-inspector.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrE,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAO/D,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IACxF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,SAAS,CAAC;CAChD;
|
|
1
|
+
{"version":3,"file":"graph-inspector.d.ts","sourceRoot":"","sources":["../../src/parts/graph-inspector.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrE,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAO/D,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IACxF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,SAAS,CAAC;CAChD;AAYD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,mBAAmB,+BA0FzF"}
|
|
@@ -1,82 +1,82 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as i, jsx as n, Fragment as
|
|
3
|
-
import { useChartCapacity as B, Show as
|
|
4
|
-
import { CrosshairIcon as
|
|
5
|
-
import { useMemo as
|
|
6
|
-
import { nameOf as
|
|
7
|
-
import { bindingOf as
|
|
8
|
-
import { readVertex as
|
|
9
|
-
import { useGraphContext as
|
|
10
|
-
import { useGraphState as
|
|
11
|
-
import { scaleOf as
|
|
12
|
-
import { ShapeGlyph as
|
|
13
|
-
const
|
|
14
|
-
function
|
|
15
|
-
var
|
|
16
|
-
const
|
|
17
|
-
var
|
|
18
|
-
return ((
|
|
19
|
-
}),
|
|
20
|
-
|
|
21
|
-
if (
|
|
2
|
+
import { jsxs as i, jsx as n, Fragment as T } from "react/jsx-runtime";
|
|
3
|
+
import { useChartCapacity as B, Show as d, Skeleton as F, Badge as M, Button as j, DataList as A, DataListItem as V, DataListItemLabel as E, DataListItemValue as $, cn as q } from "@kanzo-tech/ui";
|
|
4
|
+
import { CrosshairIcon as H } from "lucide-react";
|
|
5
|
+
import { useMemo as J, useState as K, useEffect as P } from "react";
|
|
6
|
+
import { nameOf as Q } from "../core/categories.js";
|
|
7
|
+
import { bindingOf as R } from "../core/channels.js";
|
|
8
|
+
import { readVertex as U } from "../core/detail.js";
|
|
9
|
+
import { useGraphContext as W } from "../react/graph-root.js";
|
|
10
|
+
import { useGraphState as p, useGraphSnapshot as X } from "../react/use-graph-state.js";
|
|
11
|
+
import { scaleOf as Y } from "../render/graph-model.js";
|
|
12
|
+
import { ShapeGlyph as Z } from "./shape-glyph.js";
|
|
13
|
+
const C = (a) => a instanceof Date ? a.toISOString().slice(0, 10) : a == null || a === "" ? "—" : String(a);
|
|
14
|
+
function mt({ children: a, className: I, slot: k, ...D }) {
|
|
15
|
+
var S;
|
|
16
|
+
const h = W(), s = p((t) => t.focus), u = p((t) => t.corpus), r = X((t) => t.geometry), l = p((t) => t.options), G = p((t) => {
|
|
17
|
+
var o;
|
|
18
|
+
return ((o = t.drawn) == null ? void 0 : o.domain) ?? t.domain;
|
|
19
|
+
}), v = B(), b = J(() => Y(l, v), [l, v]), [f, N] = K(null);
|
|
20
|
+
P(() => {
|
|
21
|
+
if (s === null || !u || !r) return;
|
|
22
22
|
const t = new AbortController();
|
|
23
|
-
return
|
|
24
|
-
(
|
|
25
|
-
(
|
|
26
|
-
t.signal.aborted || (
|
|
23
|
+
return U(u, r, s, t.signal).then(
|
|
24
|
+
(o) => !t.signal.aborted && N({ vertex: s, detail: o }),
|
|
25
|
+
(o) => {
|
|
26
|
+
t.signal.aborted || (N({ vertex: s, detail: !1 }), h.getState().options.onFailure(o));
|
|
27
27
|
}
|
|
28
28
|
), () => t.abort();
|
|
29
|
-
}, [
|
|
30
|
-
const
|
|
31
|
-
var
|
|
32
|
-
return (
|
|
33
|
-
},
|
|
34
|
-
return /* @__PURE__ */ i("div", { ...
|
|
35
|
-
/* @__PURE__ */ n(
|
|
36
|
-
/* @__PURE__ */ n(
|
|
37
|
-
/* @__PURE__ */ n(
|
|
38
|
-
e && /* @__PURE__ */ i(
|
|
29
|
+
}, [h, u, s, r]);
|
|
30
|
+
const c = f !== null && f.vertex === s ? f.detail : void 0, e = c || null, x = (t) => {
|
|
31
|
+
var o;
|
|
32
|
+
return (o = e == null ? void 0 : e.fields.find((O) => O.name === t)) == null ? void 0 : o.value;
|
|
33
|
+
}, m = R(l), w = m.byTable ? e == null ? void 0 : e.table : x(m.category), y = G.findIndex((t) => String(t) === String(w)), L = m.byTable || m.category !== void 0, z = (S = r == null ? void 0 : r.tables.find((t) => t.name === (e == null ? void 0 : e.table))) == null ? void 0 : S.identity, g = x(l.title) ?? x(z);
|
|
34
|
+
return /* @__PURE__ */ i("div", { ...D, className: q("space-y-3 text-sm", I), "data-slot": k ?? "graph-inspector", children: [
|
|
35
|
+
/* @__PURE__ */ n(d, { when: s === null, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: "Click a vertex on the canvas to inspect it." }) }),
|
|
36
|
+
/* @__PURE__ */ n(d, { when: s !== null && c === void 0, children: /* @__PURE__ */ n(F, { className: "h-24 w-full" }) }),
|
|
37
|
+
/* @__PURE__ */ n(d, { when: s !== null && c !== void 0 && !e, children: /* @__PURE__ */ n("p", { className: "text-muted-foreground text-xs", children: c === null ? "This vertex is not in the corpus." : "This vertex could not be read." }) }),
|
|
38
|
+
e && /* @__PURE__ */ i(T, { children: [
|
|
39
39
|
/* @__PURE__ */ i("div", { children: [
|
|
40
|
-
/* @__PURE__ */ n("p", { className: "truncate font-medium", "data-slot": "graph-inspector-title", children:
|
|
40
|
+
/* @__PURE__ */ n("p", { className: "truncate font-medium", "data-slot": "graph-inspector-title", children: g == null ? `#${e.vertex}` : C(g) }),
|
|
41
41
|
/* @__PURE__ */ i("div", { className: "mt-1 flex items-center gap-1.5", children: [
|
|
42
|
-
/* @__PURE__ */ n(
|
|
42
|
+
/* @__PURE__ */ n(d, { when: L, children: /* @__PURE__ */ i(M, { className: "gap-1 text-[10px]", size: "xs", variant: "outline", children: [
|
|
43
43
|
/* @__PURE__ */ n(
|
|
44
|
-
|
|
44
|
+
Z,
|
|
45
45
|
{
|
|
46
46
|
className: "size-2 shrink-0",
|
|
47
|
-
color:
|
|
48
|
-
shape:
|
|
47
|
+
color: b.color(Math.max(0, y)),
|
|
48
|
+
shape: b.shape(Math.max(0, y))
|
|
49
49
|
}
|
|
50
50
|
),
|
|
51
|
-
|
|
51
|
+
Q(w, l.categories)
|
|
52
52
|
] }) }),
|
|
53
53
|
/* @__PURE__ */ i(
|
|
54
|
-
|
|
54
|
+
j,
|
|
55
55
|
{
|
|
56
56
|
className: "h-5 gap-1 text-[10px]",
|
|
57
|
-
onClick: () =>
|
|
57
|
+
onClick: () => h.reveal(e.vertex),
|
|
58
58
|
size: "sm",
|
|
59
59
|
title: "Bring this vertex into view on the canvas",
|
|
60
60
|
variant: "ghost",
|
|
61
61
|
children: [
|
|
62
|
-
/* @__PURE__ */ n(
|
|
62
|
+
/* @__PURE__ */ n(H, { className: "size-3" }),
|
|
63
63
|
"Find on canvas"
|
|
64
64
|
]
|
|
65
65
|
}
|
|
66
66
|
)
|
|
67
67
|
] })
|
|
68
68
|
] }),
|
|
69
|
-
/* @__PURE__ */ i(
|
|
70
|
-
e.fields.map((t) => /* @__PURE__ */ i(
|
|
69
|
+
/* @__PURE__ */ i(A, { orientation: "vertical", children: [
|
|
70
|
+
e.fields.map((t) => /* @__PURE__ */ i(V, { className: "gap-0.5 py-0", children: [
|
|
71
71
|
/* @__PURE__ */ n(E, { className: "text-xs", children: t.name }),
|
|
72
|
-
/* @__PURE__ */ n(
|
|
72
|
+
/* @__PURE__ */ n($, { className: "break-all", children: C(t.value) })
|
|
73
73
|
] }, t.name)),
|
|
74
|
-
|
|
74
|
+
a == null ? void 0 : a(e)
|
|
75
75
|
] })
|
|
76
76
|
] })
|
|
77
77
|
] });
|
|
78
78
|
}
|
|
79
79
|
export {
|
|
80
|
-
|
|
80
|
+
mt as GraphInspector
|
|
81
81
|
};
|
|
82
82
|
//# sourceMappingURL=graph-inspector.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-inspector.js","sources":["../../src/parts/graph-inspector.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Badge,\n Button,\n cn,\n DataList,\n DataListItem,\n DataListItemLabel,\n DataListItemValue,\n Show,\n Skeleton,\n useChartCapacity,\n} from \"@kanzo-tech/ui\";\nimport { CrosshairIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { readVertex, type VertexDetail } from \"../core/detail\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphInspectorProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /**\n * More fields for the focused vertex, drawn inside its list after the corpus's own — a\n * `DataListItem` each. A render prop rather than a table of formatters: what a product adds is\n * markup, and the part never learns what any of it means.\n */\n children?: (detail: VertexDetail) => ReactNode;\n}\n\n/** A cell, as text — a `DATE` arrives as a `Date`, and a 64-bit column as a `bigint`. */\nconst text = (value: unknown): string => {\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n if (value === null || value === undefined || value === \"\") return \"—\";\n return String(value);\n};\n\ntype Answer = { vertex: VertexId; detail: VertexDetail | null } | null;\n\n/**\n * **The focused vertex, fetched and laid out by the corpus's own tables.** The loaded graph carries\n * what the channels project and nothing else, so the rest of the row is read when a reader focuses\n * it — a scan of its table, filtered to its key, cancelled when the focus moves on. The fields are the\n * table's, in the order the manifest declares them; the key and the position are the canvas's.\n */\nexport function GraphInspector({ children, className, slot, ...rest }: GraphInspectorProps) {\n const api = useGraphContext();\n const focus = useGraphState((s) => s.focus);\n const corpus = useGraphState((s) => s.corpus);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const capacity = useChartCapacity();\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n const [answer, setAnswer] = useState<Answer>(null);\n\n useEffect(() => {\n if (focus === null || !corpus || !geometry) return;\n const aborter = new AbortController();\n readVertex(corpus, geometry, focus, aborter.signal).then(\n (detail) => !aborter.signal.aborted && setAnswer({ vertex: focus, detail }),\n (error: unknown) => {\n if (aborter.signal.aborted) return;\n setAnswer({ vertex: focus, detail:
|
|
1
|
+
{"version":3,"file":"graph-inspector.js","sources":["../../src/parts/graph-inspector.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Badge,\n Button,\n cn,\n DataList,\n DataListItem,\n DataListItemLabel,\n DataListItemValue,\n Show,\n Skeleton,\n useChartCapacity,\n} from \"@kanzo-tech/ui\";\nimport { CrosshairIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { nameOf } from \"../core/categories\";\nimport { bindingOf } from \"../core/channels\";\nimport { readVertex, type VertexDetail } from \"../core/detail\";\nimport type { VertexId } from \"../core/types\";\nimport { useGraphContext } from \"../react/graph-root\";\nimport { useGraphSnapshot, useGraphState } from \"../react/use-graph-state\";\nimport { scaleOf } from \"../render/graph-model\";\nimport { ShapeGlyph } from \"./shape-glyph\";\n\nexport interface GraphInspectorProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /**\n * More fields for the focused vertex, drawn inside its list after the corpus's own — a\n * `DataListItem` each. A render prop rather than a table of formatters: what a product adds is\n * markup, and the part never learns what any of it means.\n */\n children?: (detail: VertexDetail) => ReactNode;\n}\n\n/** A cell, as text — a `DATE` arrives as a `Date`, and a 64-bit column as a `bigint`. */\nconst text = (value: unknown): string => {\n if (value instanceof Date) return value.toISOString().slice(0, 10);\n if (value === null || value === undefined || value === \"\") return \"—\";\n return String(value);\n};\n\n/** `null` is no such vertex in the corpus; `false` is a read that did not answer. */\ntype Answer = { vertex: VertexId; detail: VertexDetail | null | false } | null;\n\n/**\n * **The focused vertex, fetched and laid out by the corpus's own tables.** The loaded graph carries\n * what the channels project and nothing else, so the rest of the row is read when a reader focuses\n * it — a scan of its table, filtered to its key, cancelled when the focus moves on. The fields are the\n * table's, in the order the manifest declares them; the key and the position are the canvas's.\n */\nexport function GraphInspector({ children, className, slot, ...rest }: GraphInspectorProps) {\n const api = useGraphContext();\n const focus = useGraphState((s) => s.focus);\n const corpus = useGraphState((s) => s.corpus);\n const geometry = useGraphSnapshot((s) => s.geometry);\n const options = useGraphState((s) => s.options);\n const domain = useGraphState((s) => s.drawn?.domain ?? s.domain);\n const capacity = useChartCapacity();\n const scale = useMemo(() => scaleOf(options, capacity), [options, capacity]);\n const [answer, setAnswer] = useState<Answer>(null);\n\n useEffect(() => {\n if (focus === null || !corpus || !geometry) return;\n const aborter = new AbortController();\n readVertex(corpus, geometry, focus, aborter.signal).then(\n (detail) => !aborter.signal.aborted && setAnswer({ vertex: focus, detail }),\n (error: unknown) => {\n if (aborter.signal.aborted) return;\n setAnswer({ vertex: focus, detail: false });\n api.getState().options.onFailure(error);\n },\n );\n return () => aborter.abort();\n }, [api, corpus, focus, geometry]);\n\n const answered = answer !== null && answer.vertex === focus ? answer.detail : undefined;\n const current = answered || null;\n const field = (name: string | undefined) => current?.fields.find((f) => f.name === name)?.value;\n const binding = bindingOf(options);\n const category = binding.byTable ? current?.table : field(binding.category);\n const rank = domain.findIndex((value) => String(value) === String(category));\n const bound = binding.byTable || binding.category !== undefined;\n const identity = geometry?.tables.find((table) => table.name === current?.table)?.identity;\n const heading = field(options.title) ?? field(identity);\n\n return (\n <div {...rest} className={cn(\"space-y-3 text-sm\", className)} data-slot={slot ?? \"graph-inspector\"}>\n <Show when={focus === null}>\n <p className=\"text-muted-foreground text-xs\">Click a vertex on the canvas to inspect it.</p>\n </Show>\n <Show when={focus !== null && answered === undefined}>\n <Skeleton className=\"h-24 w-full\" />\n </Show>\n <Show when={focus !== null && answered !== undefined && !current}>\n <p className=\"text-muted-foreground text-xs\">\n {answered === null ? \"This vertex is not in the corpus.\" : \"This vertex could not be read.\"}\n </p>\n </Show>\n {current && (\n <>\n <div>\n <p className=\"truncate font-medium\" data-slot=\"graph-inspector-title\">\n {heading === undefined || heading === null ? `#${current.vertex}` : text(heading)}\n </p>\n <div className=\"mt-1 flex items-center gap-1.5\">\n <Show when={bound}>\n <Badge className=\"gap-1 text-[10px]\" size=\"xs\" variant=\"outline\">\n <ShapeGlyph\n className=\"size-2 shrink-0\"\n color={scale.color(Math.max(0, rank))}\n shape={scale.shape(Math.max(0, rank))}\n />\n {nameOf(category, options.categories)}\n </Badge>\n </Show>\n <Button\n className=\"h-5 gap-1 text-[10px]\"\n onClick={() => api.reveal(current.vertex)}\n size=\"sm\"\n title=\"Bring this vertex into view on the canvas\"\n variant=\"ghost\"\n >\n <CrosshairIcon className=\"size-3\" />\n Find on canvas\n </Button>\n </div>\n </div>\n <DataList orientation=\"vertical\">\n {current.fields.map((f) => (\n <DataListItem className=\"gap-0.5 py-0\" key={f.name}>\n <DataListItemLabel className=\"text-xs\">{f.name}</DataListItemLabel>\n <DataListItemValue className=\"break-all\">{text(f.value)}</DataListItemValue>\n </DataListItem>\n ))}\n {children?.(current)}\n </DataList>\n </>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAmCA;AAeO;;AACL;;AAKoC;AAAqB;AAKzD;AACE;AACA;AACA;AAAoD;AACwB;AAExE;AAEsC;AACxC;AAEmB;AAGvB;;AAE4C;AAA8C;AAQ1F;AAEI;AAEA;AAGA;AAKA;AAGI;AACE;AAEA;AAEE;AAEI;AAAA;AAAC;AAAA;AACW;AAC0B;AACA;AAAA;AAAA;AAEF;AAExC;AACA;AAAC;AAAA;AACW;AAC8B;AACnC;AACC;AACE;AAER;AAAkC;AAAE;AAAA;AAAA;AAAA;AAGxC;AACF;AAEG;AAEG;AAA+C;AACS;AAE3D;AACW;AACd;AACF;AAIR;;;;"}
|
package/dist/react/use-graph.js
CHANGED
|
@@ -1,76 +1,82 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useRef as k, useCallback as
|
|
3
|
-
import { createGraph as
|
|
4
|
-
import { createRenderer as
|
|
5
|
-
const
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
if (!
|
|
9
|
-
return
|
|
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";
|
|
5
|
+
const m = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
function R(e) {
|
|
7
|
+
const t = m.get(e);
|
|
8
|
+
if (!t) throw new Error("a graph part was given an api useGraph did not build");
|
|
9
|
+
return t;
|
|
10
10
|
}
|
|
11
|
-
function A(
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
11
|
+
function A(e) {
|
|
12
|
+
const t = k(e);
|
|
13
|
+
t.current = e;
|
|
14
|
+
const u = B(
|
|
15
15
|
(F) => ({
|
|
16
16
|
...F,
|
|
17
|
-
onFailure: (
|
|
18
|
-
onSelect: (
|
|
19
|
-
var
|
|
20
|
-
return (
|
|
17
|
+
onFailure: (l) => t.current.onFailure(l),
|
|
18
|
+
onSelect: (l) => {
|
|
19
|
+
var a, s;
|
|
20
|
+
return (s = (a = t.current).onSelect) == null ? void 0 : s.call(a, l);
|
|
21
21
|
},
|
|
22
|
-
onFocus: (
|
|
23
|
-
var
|
|
24
|
-
return (
|
|
22
|
+
onFocus: (l) => {
|
|
23
|
+
var a, s;
|
|
24
|
+
return (s = (a = t.current).onFocus) == null ? void 0 : s.call(a, l);
|
|
25
25
|
}
|
|
26
26
|
}),
|
|
27
27
|
[]
|
|
28
|
-
), [
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
}, [
|
|
32
|
-
}), [
|
|
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;
|
|
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;
|
|
33
33
|
}
|
|
34
|
-
function T(
|
|
35
|
-
let
|
|
36
|
-
const
|
|
37
|
-
var
|
|
38
|
-
return (
|
|
39
|
-
},
|
|
40
|
-
zoomBy:
|
|
41
|
-
fit:
|
|
42
|
-
pause:
|
|
43
|
-
resume:
|
|
44
|
-
restart:
|
|
45
|
-
unpin:
|
|
34
|
+
function T(e) {
|
|
35
|
+
let t = null;
|
|
36
|
+
const u = (n) => (...c) => {
|
|
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"),
|
|
46
46
|
// Selecting and focusing are state, and hold without a renderer; centring is the camera's.
|
|
47
|
-
reveal: (
|
|
48
|
-
if (
|
|
49
|
-
|
|
47
|
+
reveal: (n) => {
|
|
48
|
+
if (t) return t.reveal(n);
|
|
49
|
+
e.select([n], "node", "Node"), e.focus(n);
|
|
50
50
|
},
|
|
51
|
-
frameSelection:
|
|
51
|
+
frameSelection: u("frameSelection"),
|
|
52
52
|
clear: () => {
|
|
53
|
-
|
|
53
|
+
e.select(null), e.focus(null);
|
|
54
54
|
},
|
|
55
|
-
select: (
|
|
56
|
-
setFocus: (
|
|
57
|
-
setTool: (
|
|
58
|
-
subscribe: (
|
|
59
|
-
getState: () =>
|
|
55
|
+
select: (n, c, r) => e.select(n, c, r),
|
|
56
|
+
setFocus: (n) => e.focus(n),
|
|
57
|
+
setTool: (n) => e.setTool(n),
|
|
58
|
+
subscribe: (n) => e.subscribe(n),
|
|
59
|
+
getState: () => e.getSnapshot()
|
|
60
60
|
};
|
|
61
|
-
return
|
|
62
|
-
store:
|
|
63
|
-
renderer: () =>
|
|
64
|
-
attach(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
return m.set(o, {
|
|
62
|
+
store: e,
|
|
63
|
+
renderer: () => t,
|
|
64
|
+
attach(n, c) {
|
|
65
|
+
e.renderable();
|
|
66
|
+
let r = null;
|
|
67
|
+
try {
|
|
68
|
+
r = N(n, e, c);
|
|
69
|
+
} catch (i) {
|
|
70
|
+
e.unrenderable(i);
|
|
71
|
+
}
|
|
72
|
+
return t = r, () => {
|
|
73
|
+
r == null || r.destroy(), t === r && (t = null);
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
|
-
}),
|
|
76
|
+
}), o;
|
|
71
77
|
}
|
|
72
78
|
export {
|
|
73
|
-
|
|
79
|
+
R as internalsOf,
|
|
74
80
|
A as useGraph
|
|
75
81
|
};
|
|
76
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: (
|
|
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;;;;;"}
|