@kanzo-tech/graph 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -7
- package/dist/core/detail.d.ts +3 -1
- package/dist/core/detail.d.ts.map +1 -1
- package/dist/core/detail.js +55 -30
- 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 +21 -6
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +107 -101
- package/dist/core/store.js.map +1 -1
- package/dist/core/types.d.ts +4 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -19
- 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-counts.d.ts +15 -0
- package/dist/parts/graph-counts.d.ts.map +1 -0
- package/dist/parts/graph-counts.js +41 -0
- package/dist/parts/graph-counts.js.map +1 -0
- 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/parts/graph-legend.d.ts +3 -2
- package/dist/parts/graph-legend.d.ts.map +1 -1
- package/dist/parts/graph-legend.js +30 -48
- package/dist/parts/graph-legend.js.map +1 -1
- package/dist/parts/graph-looks.d.ts +16 -0
- package/dist/parts/graph-looks.d.ts.map +1 -0
- package/dist/parts/graph-looks.js +138 -0
- package/dist/parts/graph-looks.js.map +1 -0
- package/dist/parts/graph-search.d.ts +17 -0
- package/dist/parts/graph-search.d.ts.map +1 -0
- package/dist/parts/graph-search.js +89 -0
- package/dist/parts/graph-search.js.map +1 -0
- package/dist/react/use-graph-prefs.d.ts +6 -2
- package/dist/react/use-graph-prefs.d.ts.map +1 -1
- package/dist/react/use-graph-prefs.js +7 -7
- package/dist/react/use-graph-prefs.js.map +1 -1
- package/dist/react/use-graph.js +61 -55
- package/dist/react/use-graph.js.map +1 -1
- package/dist/render/camera.d.ts +26 -0
- package/dist/render/camera.d.ts.map +1 -0
- package/dist/render/camera.js +40 -0
- package/dist/render/camera.js.map +1 -0
- package/dist/render/graph-looks.d.ts +10 -0
- package/dist/render/graph-looks.d.ts.map +1 -1
- package/dist/render/graph-looks.js +37 -20
- package/dist/render/graph-looks.js.map +1 -1
- package/dist/render/placement.d.ts +23 -0
- package/dist/render/placement.d.ts.map +1 -0
- package/dist/render/placement.js +23 -0
- package/dist/render/placement.js.map +1 -0
- package/dist/render/renderer.d.ts +1 -0
- package/dist/render/renderer.d.ts.map +1 -1
- package/dist/render/renderer.js +115 -96
- package/dist/render/renderer.js.map +1 -1
- package/dist/render/webgl.d.ts +5 -4
- package/dist/render/webgl.d.ts.map +1 -1
- package/dist/render/webgl.js +14 -11
- package/dist/render/webgl.js.map +1 -1
- package/package.json +10 -9
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;;;;;"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Graph } from '@cosmos.gl/graph';
|
|
2
|
+
export declare const FIT_DURATION = 420;
|
|
3
|
+
export interface Camera {
|
|
4
|
+
/** A corpus was placed: frame these corners now, and again whenever the canvas changes size. */
|
|
5
|
+
frame(corners: readonly number[]): void;
|
|
6
|
+
/** The reader asked for the whole graph: frame it, and keep it framed through a resize. */
|
|
7
|
+
fit(): void;
|
|
8
|
+
/** A layout started moving the points: the corners are history, and the camera follows. */
|
|
9
|
+
run(): void;
|
|
10
|
+
tick(): void;
|
|
11
|
+
settle(): void;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* **Who holds the camera, and when it gives it back.** The camera frames the corpus once it is
|
|
16
|
+
* placed and keeps it framed — through cosmos.gl's device coming up and every resize of the canvas,
|
|
17
|
+
* since a host's split panel settles its width after mount and a frame taken at the old size opens
|
|
18
|
+
* zoomed into a corner — and while a layout runs it follows the moving points and frames them when
|
|
19
|
+
* they settle: Cosmograph's fit-on-settle, kept up. All of it stops the moment the reader zooms or
|
|
20
|
+
* pans, and comes back with Fit or the next run.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createCamera(graph: Graph, host: HTMLElement): {
|
|
23
|
+
camera: Camera;
|
|
24
|
+
taken: (userDriven: boolean) => void;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=camera.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../../src/render/camera.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,eAAO,MAAM,YAAY,MAAM,CAAC;AAMhC,MAAM,WAAW,MAAM;IACrB,gGAAgG;IAChG,KAAK,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACxC,2FAA2F;IAC3F,GAAG,IAAI,IAAI,CAAC;IACZ,2FAA2F;IAC3F,GAAG,IAAI,IAAI,CAAC;IACZ,IAAI,IAAI,IAAI,CAAC;IACb,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CA6DtH"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const O = 420;
|
|
2
|
+
function F(o, a) {
|
|
3
|
+
let f = null, r = !1, n = !1, l = 0, i = 0;
|
|
4
|
+
const c = (e) => {
|
|
5
|
+
f ? o.fitViewByPointPositions([...f], e, 0.18) : o.fitView(e, 0.18);
|
|
6
|
+
}, s = () => {
|
|
7
|
+
i = 0, r && !n && c(0);
|
|
8
|
+
}, t = typeof ResizeObserver > "u" ? null : new ResizeObserver(() => {
|
|
9
|
+
i || (i = requestAnimationFrame(s));
|
|
10
|
+
});
|
|
11
|
+
return t == null || t.observe(a), o.ready.then(s, () => {
|
|
12
|
+
}), { camera: {
|
|
13
|
+
frame(e) {
|
|
14
|
+
f = e, r = !0, c(0);
|
|
15
|
+
},
|
|
16
|
+
fit() {
|
|
17
|
+
r = !0, c(420);
|
|
18
|
+
},
|
|
19
|
+
run() {
|
|
20
|
+
f = null, n = !0, l = performance.now();
|
|
21
|
+
},
|
|
22
|
+
tick() {
|
|
23
|
+
const e = performance.now();
|
|
24
|
+
!n || e - l < 900 || (l = e, o.fitView(700, 0.18));
|
|
25
|
+
},
|
|
26
|
+
settle() {
|
|
27
|
+
n && o.fitView(420, 0.18), n = !1;
|
|
28
|
+
},
|
|
29
|
+
destroy() {
|
|
30
|
+
t == null || t.disconnect(), i && cancelAnimationFrame(i);
|
|
31
|
+
}
|
|
32
|
+
}, taken: (e) => {
|
|
33
|
+
e && (r = !1, n = !1);
|
|
34
|
+
} };
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
O as FIT_DURATION,
|
|
38
|
+
F as createCamera
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=camera.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"camera.js","sources":["../../src/render/camera.ts"],"sourcesContent":["import type { Graph } from \"@cosmos.gl/graph\";\n\nexport const FIT_DURATION = 420;\nconst FIT_PADDING = 0.18;\n/** How often a running layout re-frames the camera, and how long each re-frame glides. */\nconst FOLLOW_EVERY = 900;\nconst FOLLOW_DURATION = 700;\n\nexport interface Camera {\n /** A corpus was placed: frame these corners now, and again whenever the canvas changes size. */\n frame(corners: readonly number[]): void;\n /** The reader asked for the whole graph: frame it, and keep it framed through a resize. */\n fit(): void;\n /** A layout started moving the points: the corners are history, and the camera follows. */\n run(): void;\n tick(): void;\n settle(): void;\n destroy(): void;\n}\n\n/**\n * **Who holds the camera, and when it gives it back.** The camera frames the corpus once it is\n * placed and keeps it framed — through cosmos.gl's device coming up and every resize of the canvas,\n * since a host's split panel settles its width after mount and a frame taken at the old size opens\n * zoomed into a corner — and while a layout runs it follows the moving points and frames them when\n * they settle: Cosmograph's fit-on-settle, kept up. All of it stops the moment the reader zooms or\n * pans, and comes back with Fit or the next run.\n */\nexport function createCamera(graph: Graph, host: HTMLElement): { camera: Camera; taken: (userDriven: boolean) => void } {\n let corners: readonly number[] | null = null;\n let framing = false;\n let following = false;\n let followed = 0;\n let pending = 0;\n\n const fitNow = (duration: number) => {\n if (corners) graph.fitViewByPointPositions([...corners], duration, FIT_PADDING);\n else graph.fitView(duration, FIT_PADDING);\n };\n const reframe = () => {\n pending = 0;\n if (framing && !following) fitNow(0);\n };\n // After cosmos.gl's own observer has resized the canvas, so the frame is taken at the new size.\n const resizes =\n typeof ResizeObserver === \"undefined\"\n ? null\n : new ResizeObserver(() => {\n if (!pending) pending = requestAnimationFrame(reframe);\n });\n resizes?.observe(host);\n void graph.ready.then(reframe, () => {});\n\n const camera: Camera = {\n frame(next) {\n corners = next;\n framing = true;\n fitNow(0);\n },\n fit() {\n framing = true;\n fitNow(FIT_DURATION);\n },\n run() {\n corners = null;\n following = true;\n followed = performance.now();\n },\n tick() {\n const now = performance.now();\n if (!following || now - followed < FOLLOW_EVERY) return;\n followed = now;\n graph.fitView(FOLLOW_DURATION, FIT_PADDING);\n },\n settle() {\n if (following) graph.fitView(FIT_DURATION, FIT_PADDING);\n following = false;\n },\n destroy() {\n resizes?.disconnect();\n if (pending) cancelAnimationFrame(pending);\n },\n };\n const taken = (userDriven: boolean) => {\n if (!userDriven) return;\n framing = false;\n following = false;\n };\n return { camera, taken };\n}\n"],"names":["FIT_DURATION","createCamera","graph","host","corners","framing","following","followed","pending","fitNow","duration","reframe","resizes","next","now","userDriven"],"mappings":"AAEO,MAAMA,IAAe;AA0BrB,SAASC,EAAaC,GAAcC,GAA6E;AACtH,MAAIC,IAAoC,MACpCC,IAAU,IACVC,IAAY,IACZC,IAAW,GACXC,IAAU;AAEd,QAAMC,IAAS,CAACC,MAAqB;AACnC,IAAIN,MAAe,wBAAwB,CAAC,GAAGA,CAAO,GAAGM,GAAU,IAAW,IACzER,EAAM,QAAQQ,GAAU,IAAW;AAAA,EAC1C,GACMC,IAAU,MAAM;AACpB,IAAAH,IAAU,GACNH,KAAW,CAACC,KAAWG,EAAO,CAAC;AAAA,EACrC,GAEMG,IACJ,OAAO,iBAAmB,MACtB,OACA,IAAI,eAAe,MAAM;AACvB,IAAKJ,MAASA,IAAU,sBAAsBG,CAAO;AAAA,EACvD,CAAC;AACP,SAAAC,KAAA,QAAAA,EAAS,QAAQT,IACZD,EAAM,MAAM,KAAKS,GAAS,MAAM;AAAA,EAAC,CAAC,GAqChC,EAAE,QAnCc;AAAA,IACrB,MAAME,GAAM;AACV,MAAAT,IAAUS,GACVR,IAAU,IACVI,EAAO,CAAC;AAAA,IACV;AAAA,IACA,MAAM;AACJ,MAAAJ,IAAU,IACVI,EAAO,GAAY;AAAA,IACrB;AAAA,IACA,MAAM;AACJ,MAAAL,IAAU,MACVE,IAAY,IACZC,IAAW,YAAY,IAAA;AAAA,IACzB;AAAA,IACA,OAAO;AACL,YAAMO,IAAM,YAAY,IAAA;AACxB,MAAI,CAACR,KAAaQ,IAAMP,IAAW,QACnCA,IAAWO,GACXZ,EAAM,QAAQ,KAAiB,IAAW;AAAA,IAC5C;AAAA,IACA,SAAS;AACP,MAAII,KAAWJ,EAAM,QAAQ,KAAc,IAAW,GACtDI,IAAY;AAAA,IACd;AAAA,IACA,UAAU;AACR,MAAAM,KAAA,QAAAA,EAAS,cACLJ,0BAA8BA,CAAO;AAAA,IAC3C;AAAA,EAAA,GAOe,OALH,CAACO,MAAwB;AACrC,IAAKA,MACLV,IAAU,IACVC,IAAY;AAAA,EACd,EACiB;AACnB;"}
|
|
@@ -149,6 +149,16 @@ export interface Look {
|
|
|
149
149
|
* default. Those defaults are what a graph drew before any of this existed.
|
|
150
150
|
*/
|
|
151
151
|
export declare function lookFrom(values?: Readonly<Record<string, string | undefined>>): Look;
|
|
152
|
+
/** The three named looks. */
|
|
153
|
+
export type LookPreset = "nebula" | "atlas" | "ink";
|
|
154
|
+
/**
|
|
155
|
+
* **The three looks as values of the section's axes**, in the order a picker offers them. Each writes
|
|
156
|
+
* every axis any of them names, so wearing one leaves nothing of the last, and a look is worn exactly
|
|
157
|
+
* when the resolved axes equal its own — no name is stored beside them. Atlas is every default.
|
|
158
|
+
*/
|
|
159
|
+
export declare const PRESETS: Record<LookPreset, Readonly<Record<string, string>>>;
|
|
160
|
+
/** The look the axes are — a missing one is the section's default — or `null` once customised past all three. */
|
|
161
|
+
export declare function presetOf(values: Readonly<Record<string, string | undefined>>): LookPreset | null;
|
|
152
162
|
/**
|
|
153
163
|
* What a canvas draws when nobody has chosen anything.
|
|
154
164
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/graph-looks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/graph-looks.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,EAAgD,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAe,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvB,IAAI,EAAE;QACJ;;;;;WAKG;QACH,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;;;;;WAUG;QACH,KAAK,EAAE,OAAO,CAAC;QACf;;;;;;WAMG;QACH,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACxB,CAAC;IACF,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;CAsBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,IAAI,CA0BxF;AAED,6BAA6B;AAC7B,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAIxE,CAAC;AAEF,iHAAiH;AACjH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,UAAU,GAAG,IAAI,CAMhG;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,IAAiB,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAQ5C,CAAC"}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
import { GRAPH_SECTION as d } from "../section.js";
|
|
2
|
+
const f = {
|
|
2
3
|
circle: 0,
|
|
3
4
|
square: 1,
|
|
4
5
|
triangle: 2,
|
|
5
6
|
diamond: 3,
|
|
6
7
|
cross: 7
|
|
7
|
-
},
|
|
8
|
+
}, k = ["circle", "square", "triangle", "diamond"], b = "cross";
|
|
8
9
|
function c(e = {}) {
|
|
9
|
-
const n = (
|
|
10
|
-
const
|
|
11
|
-
return
|
|
12
|
-
},
|
|
10
|
+
const n = (i, l) => {
|
|
11
|
+
const o = e[i];
|
|
12
|
+
return o === void 0 ? l : o === "true";
|
|
13
|
+
}, t = e.marks === "legible", s = Number.parseFloat(e.labels ?? "");
|
|
13
14
|
return {
|
|
14
|
-
size:
|
|
15
|
+
size: t ? [4, 13] : [2, 8],
|
|
15
16
|
link: {
|
|
16
17
|
render: n("links", !0),
|
|
17
|
-
opacity:
|
|
18
|
-
width:
|
|
18
|
+
opacity: t ? 0.28 : 0.42,
|
|
19
|
+
width: t ? 0.5 : 0.6,
|
|
19
20
|
// A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default
|
|
20
21
|
// of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two
|
|
21
22
|
// pictures — straight, and told apart — not a number to tune.
|
|
@@ -25,16 +26,30 @@ function c(e = {}) {
|
|
|
25
26
|
// definition of a field nobody chose.
|
|
26
27
|
fade: [200, 1400]
|
|
27
28
|
},
|
|
28
|
-
labels: Number.isFinite(
|
|
29
|
+
labels: Number.isFinite(s) ? s : 26,
|
|
29
30
|
vignette: n("vignette", !1),
|
|
30
31
|
grid: n("grid", !0)
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const a = {
|
|
35
|
+
nebula: { marks: "dense", "additive-links": "true", "bowed-links": "false", labels: "14", vignette: "true" },
|
|
36
|
+
atlas: { marks: "dense", "additive-links": "false", "bowed-links": "true", labels: "26", vignette: "false" },
|
|
37
|
+
ink: { marks: "legible", "additive-links": "false", "bowed-links": "false", labels: "40", vignette: "false" }
|
|
38
|
+
};
|
|
39
|
+
function v(e) {
|
|
40
|
+
const n = (s) => {
|
|
41
|
+
var i, l;
|
|
42
|
+
return e[s] ?? ((l = (i = d.prefs) == null ? void 0 : i[s]) == null ? void 0 : l.default);
|
|
43
|
+
};
|
|
44
|
+
return Object.keys(a).find(
|
|
45
|
+
(s) => Object.entries(a[s]).every(([i, l]) => n(i) === l)
|
|
46
|
+
) ?? null;
|
|
47
|
+
}
|
|
48
|
+
const r = c();
|
|
49
|
+
function g(e) {
|
|
50
|
+
return e ? { ...r, ...e, link: { ...r.link, ...e.link } } : r;
|
|
36
51
|
}
|
|
37
|
-
const
|
|
52
|
+
const m = {
|
|
38
53
|
circle: "M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z",
|
|
39
54
|
square: "M2 2h8v8H2Z",
|
|
40
55
|
triangle: "M6 1.6 10.6 10H1.4Z",
|
|
@@ -44,12 +59,14 @@ const v = {
|
|
|
44
59
|
cross: "M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z"
|
|
45
60
|
};
|
|
46
61
|
export {
|
|
47
|
-
|
|
48
|
-
a as
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
r as DEFAULT_LOOK,
|
|
63
|
+
a as PRESETS,
|
|
64
|
+
f as SHAPE_INDEX,
|
|
65
|
+
k as SHAPE_ORDER,
|
|
66
|
+
b as SHAPE_OTHER,
|
|
67
|
+
m as SHAPE_PATH,
|
|
52
68
|
c as lookFrom,
|
|
53
|
-
|
|
69
|
+
v as presetOf,
|
|
70
|
+
g as resolveLook
|
|
54
71
|
};
|
|
55
72
|
//# sourceMappingURL=graph-looks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-looks.js","sources":["../../src/render/graph-looks.ts"],"sourcesContent":["// Three registers the same graph can be *drawn* in. Not three palettes, and — since a channel is a\n// binding on the request — not three encodings either.\n//\n// **A look is form and nothing else.** No colours: colour belongs to the theme's categorical\n// scheme, the way it belongs to a chart, so every surface showing the same categories gets the same\n// answer. And no encoding: what a channel carries is bound by whoever draws the graph, under Plot's\n// names. A look that decided whether identity reached the GPU as colour or as shape was a theme\n// reaching into an encoding — Vega-Lite states the general rule, that `config` sets defaults for\n// marks, scales, axes and legends and may not touch `encoding` — and its concrete cost was a `fill`\n// binding that painted nothing under the look named Ink.\n//\n// This is the grammar-of-graphics split, and it is the reference model rather than a local idea:\n// in Observable Plot colour is a property of the **scale**, never of the mark, and the mark carries\n// the channels. A look is the mark's geometry.\n//\n// The three names survive as **recommended pairings** — a form plus the bindings that were bundled\n// with it — offered by the host that draws the graph. A host offering three arrangements it authored\n// is not the same act as a preference silently discarding the caller's binding.\n\n/**\n * The glyphs this canvas draws — **by name**, because a name is what the concept is.\n *\n * This was five exports of one idea: a `SHAPE` object mapping names to numbers, a `ShapeId` type\n * that was the union of those numbers, `SHAPE_ORDER`, `SHAPE_OTHER` and `SHAPE_PATH` keyed by them.\n * The numbers were never ours. They are cosmos.gl's `setPointShapes` enum indices, and the jump from\n * `3` (diamond) to `7` (cross) is what gives that away — there is no `4`, `5` or `6` here because\n * Pentagon, Hexagon and Star are members this canvas does not draw. Publishing them made the\n * renderer's internal numbering part of a contract, so a host held `3` where it meant *diamond*, and\n * an upstream enum that renumbered would have moved every legend on every page silently.\n *\n * A string union says the same thing, reads at the call site — `<ShapeGlyph shape=\"cross\" />` — and\n * makes the mapping this file's private business, which is what it always was.\n */\nexport type Shape = \"circle\" | \"square\" | \"triangle\" | \"diamond\" | \"cross\";\n\n/**\n * The name → cosmos.gl enum index, and the one place that translation happens.\n *\n * `None` (`8`) has no name here on purpose: the point fragment shader `discard`s a `NONE` point that\n * carries no image, so \"past capacity\" spelled as `None` would delete the node from the picture, and\n * a category nobody can name is still a node with edges. `obligations.ts` grades that.\n *\n * Module-scoped and off the barrel. `buffers` reads it on the way to the GPU; nothing else needs it,\n * and anything that did would be reaching for the enum this type exists to hide.\n */\nexport const SHAPE_INDEX: Record<Shape, number> = {\n circle: 0,\n square: 1,\n triangle: 2,\n diamond: 3,\n cross: 7,\n};\n\n/**\n * The shape scale, in slot order — the sibling of the colour scale.\n *\n * Plot calls this channel `symbol` and gives it its own legend, which is the tell that it is a peer\n * of colour rather than a decoration. Four slots and no cycling: a fifth category cannot wear circle\n * again without claiming to be the first one.\n *\n * **Four because the shape channel's measured capacity is five, not because small shapes stop being\n * distinguishable.** Giovannangeli et al. (arXiv 2103.06084) put the ceiling at **5 for shape**\n * against **7 for colour**; `SHAPE_ORDER`'s four plus `SHAPE_OTHER` is exactly five distinguishable\n * glyphs, and the two scales differ in cardinality because the channels do. The colour side already\n * says the same thing from the other end — Dracula's document publishes `--chart-capacity: 7`.\n *\n * **This comment used to give the size argument, and the size argument is wrong.** It said a\n * pentagon, a hexagon and a circle are one dot at Ink's floor. Smart & Szafir (CHI 2019,\n * doi:10.1145/3290605.3300899) measured 16 shapes across 6 mark sizes from 6 to 50 px and found\n * shape discrimination *robust* to size: the only significant variation is at 6 px, and it is 4.5\n * accuracy points against 50 px. The conclusion survived the argument that was given for it, which\n * is the most dangerous shape a comment can have — see `OBLIGATIONS` in `./obligations.ts` for what\n * the floor actually protects.\n */\nexport const SHAPE_ORDER: Shape[] = [\"circle\", \"square\", \"triangle\", \"diamond\"];\n\n/**\n * What a category past the scale wears — the shape channel's `--muted-foreground`.\n *\n * No slot wears it, which is the whole point: it says *not one of the four* rather than repeating\n * the first one. This is the half that used to be missing, and the comment above was false without\n * it — `SHAPE_ORDER[4]` is `undefined`, and the fallback was `circle`.\n */\nexport const SHAPE_OTHER: Shape = \"cross\";\n\n/**\n * The geometry a canvas draws — and nothing else.\n *\n * **No `id`, no `label`, no `blurb`.** Those three were a picker's metadata, and the picker was a\n * list of three names that turned out to be two pictures. What a person chooses is declared in\n * `section.ts` as axes and resolved by `lookFrom`; what a renderer consumes is this.\n */\nexport interface Look {\n /** Radius at the lowest degree in the corpus, and at the highest. */\n size: [number, number];\n link: {\n /**\n * Whether the edge layer is drawn at all.\n *\n * It was `Display.links`, a second vocabulary for the picture that sat beside this one with its\n * own default and its own switch. A form that draws no links is a form.\n */\n render: boolean;\n opacity: number;\n width: number;\n /**\n * How far a link bows off the straight line, as a fraction of its length. `0` is straight.\n *\n * Keep it small. Every link curves the same way, so at cosmos.gl's default of `0.5` a few\n * hundred of them read as one pinwheel and the picture looks like it is spinning — motion\n * where there is structure. A hint is enough to tell two parallel edges apart.\n */\n curve: number;\n /**\n * Whether links **add** where they overlap, instead of compositing over one another.\n *\n * cosmos.gl's default is on, which is a choice nobody here made and which the archive made\n * visible: 4,280 grey links at 0.45 summed to a white spray that swallowed 1,543 points of\n * 2–9 px. Every point was uploaded, none was legible, and the picture read as *the nodes are\n * not rendering*.\n *\n * On, it is a real register rather than a bug — additive light is what makes a dense graph read\n * as flow — so it belongs to the form that wants it and not to the renderer's defaults.\n */\n blend: boolean;\n /**\n * Screen lengths between which a link fades out — depth, for free.\n *\n * Keep the far end generous. cosmos.gl measures this in *screen* pixels, so a range that\n * looks reasonable while zoomed in erases the whole edge layer when you zoom out, which\n * reads as \"Show links stopped working\".\n */\n fade: [number, number];\n };\n /** How many of the highest-degree nodes carry a standing label. */\n labels: number;\n /** A darkened rim. Mood rather than a reading aid, which is why it is form and not display. */\n vignette: boolean;\n /**\n * The dot grid behind the graph, which pans and subdivides with the camera.\n *\n * Beside `vignette` because it is the same kind of thing — the backdrop a form sits on — and it\n * arrived from the same place the rim did not: a `Display` interface that spelled the backdrop,\n * the edge layer and two multipliers as a second set of appearance controls.\n */\n grid: boolean;\n // **There is no `pointScale` and no `linkOpacity`**, and they are the two fields `Display` had that\n // this does not. Each was a reader's multiplier over a number this builder already computes from\n // the axis that owns it — `size` from `marks`, `link.opacity` from `marks` — so the panel offered\n // two ways to say one thing and the second could always overrule the measurement. The measured\n // pair (0.28 legible, 0.42 dense) is the whole argument of `marks`; a slider on top of it is the\n // post-process `graph-model.ts` forbids one layer down, wearing a preference's clothes.\n //\n // What is lost with them is the fit-to-corpus case, and it was never a preference: a computed fit\n // belongs to the tenant's starting point — which is a policy, and which is now expressible.\n\n // There is no `filter`, and its absence is a rule rather than an omission. Nebula carried\n // `saturate(1.1)` on the canvas element — the one thing left in a Look that touched hue, and a\n // chroma multiplier is colour wearing geometry's clothes. Measured over Kanzo's eight slots\n // (2026-08-13, `saturate(1.1)` through the same filter engine the browser applies): it moved\n // every slot, by ΔE 0.85 to **8.05**, which is the size of the separation `deriveScheme`\n // *guarantees* between two different categories. It did not break that separation here —\n // the closest pair went from ΔE 32.36 to 30.38, with room to spare — so the reason it is gone\n // is not a failure it caused, it is that a look must not be able to cause one. The whole claim\n // of the colour layer is that what ships is what was derived and measured; a post-process on\n // the canvas voids it silently, and `graph-model.ts` already forbids the same move one layer\n // down (\"nudging one on the way to the GPU voids all three\").\n}\n\n/**\n * The form a canvas draws, from the axes a person chose.\n *\n * **One function and no table of constants**, which is the shape the axes forced and the reason\n * the three named looks are gone. They were three parallel tables of ten fields; six of those\n * fields separated two of the three by 7–17%, under this file's own threshold for a difference\n * meaning anything — a luminance JND of 6.48–11.30 ΔL*. What is left of them is this: every number\n * appears once, where the axis that owns it is read, and the axis is declared next door in\n * `section.ts` for a panel to draw.\n *\n * **Link opacity and width ride the mark**, and that is what the numbers said rather than a tidy\n * guess: 0.42 and 0.45 on the two dense forms against 0.28 on the legible one. A form that spends\n * more ink on points cannot also spend it on edges.\n *\n * **The legible mark's radius is the whole reason that mark exists.** Its floor protects the other\n * two channels from shape rather than shape from smallness, which is why it is the one to pair\n * `symbol` with: spending shape on identity *and* size on degree at once is what Giovannangeli et\n * al. (arXiv 2103.06084) measure as dropping performance drastically under even minor heterogeneity,\n * and smaller marks worsen it both ways — the luminance JND above, and a square reported larger than\n * any other shape at equal area in 82% of trials (Smart & Szafir, CHI 2019). Not \"a triangle and a\n * square are the same dot below four pixels\", which is what this said for months and is false; see\n * `SHAPE_ORDER` for the measurement that refutes it.\n *\n * **The values are strings because a contributed preference is a string**, in all three kinds, so an\n * unrecognised namespace rides through a write untouched. Parsing what a kind means is the reader's\n * job and it is two lines; `@kanzo-tech/theme` exports the same two, and importing them would add a\n * dependency to this package to carry no code — the same call `GRAPH_SECTION` already makes about the\n * manifest type.\n *\n * A key that is missing, or that carries a value the section never offered, takes the manifest's\n * default. Those defaults are what a graph drew before any of this existed.\n */\nexport function lookFrom(values: Readonly<Record<string, string | undefined>> = {}): Look {\n const on = (key: string, fallback: boolean) => {\n const value = values[key];\n return value === undefined ? fallback : value === \"true\";\n };\n const legible = values.marks === \"legible\";\n const labels = Number.parseFloat(values.labels ?? \"\");\n return {\n size: legible ? [4, 13] : [2, 8],\n link: {\n render: on(\"links\", true),\n opacity: legible ? 0.28 : 0.42,\n width: legible ? 0.5 : 0.6,\n // A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default\n // of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two\n // pictures — straight, and told apart — not a number to tune.\n curve: on(\"bowed-links\", true) ? 0.12 : 0,\n blend: on(\"additive-links\", false),\n // Shared by every form. It was three ranges within ±10% of each other, which is the\n // definition of a field nobody chose.\n fade: [200, 1400],\n },\n labels: Number.isFinite(labels) ? labels : 26,\n vignette: on(\"vignette\", false),\n grid: on(\"grid\", true),\n };\n}\n\n/**\n * What a canvas draws when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be.** It is literally `lookFrom()` — a second public name for\n * a value the package already hands out on request — and the reason it was exported is the one\n * `resolveLook` below removes: a host that wanted one field different had to start from the whole\n * object, because `look` took a whole `Look`. Spreading a default you were given is a copy of it,\n * and a copy is what stops tracking the original the next time a number here moves.\n */\nexport const DEFAULT_LOOK: Look = lookFrom();\n\n/**\n * A look in the pieces a caller wants different — everything else is this package's answer.\n *\n * Two levels, because a `Look` has exactly two: the fields, and `link`. Deep-merging arbitrarily\n * would be a guess about a shape that is right here in this file, and `size` and `fade` are tuples\n * that must be replaced whole rather than merged element-wise.\n */\nexport interface LookPatch extends Partial<Omit<Look, \"link\">> {\n link?: Partial<Look[\"link\"]>;\n}\n\n/**\n * A patch over the package's own default — the merge `DEFAULT_LOOK` existed so a host could do by\n * hand.\n *\n * **Nothing, and it is the shared constant rather than a copy of it.** That identity matters: the\n * buffers are rebuilt whenever the look's reference changes, so a fresh object per render would\n * mean a full colour/size/shape upload on every render of every canvas that never asked for one.\n */\nexport function resolveLook(patch?: LookPatch): Look {\n if (!patch) return DEFAULT_LOOK;\n return { ...DEFAULT_LOOK, ...patch, link: { ...DEFAULT_LOOK.link, ...patch.link } };\n}\n\n/**\n * The SVG path for a shape glyph inside a 12×12 box — the legend draws what the canvas draws.\n *\n * **Off the barrel, and `ShapeGlyph` is what replaced it.** One host imported this, to fill a\n * `<path>` in a legend key and a hover card. What that host actually wanted was *the glyph*, and\n * handing it the path data made it responsible for the viewBox, the fill and the fact that the box\n * is twelve units — three facts it had to keep in step with this file by reading the comment above.\n * A component carries all three and cannot fall out of step with itself.\n */\nexport const SHAPE_PATH: Record<Shape, string> = {\n circle: \"M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z\",\n square: \"M2 2h8v8H2Z\",\n triangle: \"M6 1.6 10.6 10H1.4Z\",\n diamond: \"M6 1 11 6l-5 5-5-5Z\",\n // The proportions are cosmos.gl's own `crossDistance`: a plus with arms at 0.8 of the radius and\n // a bar 0.3 thick, so the legend's glyph is the shape the shader draws.\n cross: \"M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z\",\n};\n"],"names":["SHAPE_INDEX","SHAPE_ORDER","SHAPE_OTHER","lookFrom","values","on","key","fallback","value","legible","labels","DEFAULT_LOOK","resolveLook","patch","SHAPE_PATH"],"mappings":"AA6CO,MAAMA,IAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AACT,GAuBaC,IAAuB,CAAC,UAAU,UAAU,YAAY,SAAS,GASjEC,IAAqB;AAsH3B,SAASC,EAASC,IAAuD,IAAU;AACxF,QAAMC,IAAK,CAACC,GAAaC,MAAsB;AAC7C,UAAMC,IAAQJ,EAAOE,CAAG;AACxB,WAAOE,MAAU,SAAYD,IAAWC,MAAU;AAAA,EACpD,GACMC,IAAUL,EAAO,UAAU,WAC3BM,IAAS,OAAO,WAAWN,EAAO,UAAU,EAAE;AACpD,SAAO;AAAA,IACL,MAAMK,IAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AAAA,IAC/B,MAAM;AAAA,MACJ,QAAQJ,EAAG,SAAS,EAAI;AAAA,MACxB,SAASI,IAAU,OAAO;AAAA,MAC1B,OAAOA,IAAU,MAAM;AAAA;AAAA;AAAA;AAAA,MAIvB,OAAOJ,EAAG,eAAe,EAAI,IAAI,OAAO;AAAA,MACxC,OAAOA,EAAG,kBAAkB,EAAK;AAAA;AAAA;AAAA,MAGjC,MAAM,CAAC,KAAK,IAAI;AAAA,IAAA;AAAA,IAElB,QAAQ,OAAO,SAASK,CAAM,IAAIA,IAAS;AAAA,IAC3C,UAAUL,EAAG,YAAY,EAAK;AAAA,IAC9B,MAAMA,EAAG,QAAQ,EAAI;AAAA,EAAA;AAEzB;AAWO,MAAMM,IAAqBR,EAAA;AAqB3B,SAASS,EAAYC,GAAyB;AACnD,SAAKA,IACE,EAAE,GAAGF,GAAc,GAAGE,GAAO,MAAM,EAAE,GAAGF,EAAa,MAAM,GAAGE,EAAM,OAAK,IAD7DF;AAErB;AAWO,MAAMG,IAAoC;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"graph-looks.js","sources":["../../src/render/graph-looks.ts"],"sourcesContent":["// Three registers the same graph can be *drawn* in. Not three palettes, and — since a channel is a\n// binding on the request — not three encodings either.\n//\n// **A look is form and nothing else.** No colours: colour belongs to the theme's categorical\n// scheme, the way it belongs to a chart, so every surface showing the same categories gets the same\n// answer. And no encoding: what a channel carries is bound by whoever draws the graph, under Plot's\n// names. A look that decided whether identity reached the GPU as colour or as shape was a theme\n// reaching into an encoding — Vega-Lite states the general rule, that `config` sets defaults for\n// marks, scales, axes and legends and may not touch `encoding` — and its concrete cost was a `fill`\n// binding that painted nothing under the look named Ink.\n//\n// This is the grammar-of-graphics split, and it is the reference model rather than a local idea:\n// in Observable Plot colour is a property of the **scale**, never of the mark, and the mark carries\n// the channels. A look is the mark's geometry.\n//\n// The three names survive as **presets over the axes** — `PRESETS`, worn through `GraphLooks` — and\n// the bindings once bundled with them stay the host's: `useGraphPrefs` says which preset the axes\n// are, and a host that wants Ink's shape-for-identity binds it. A preset never discards a binding.\n\nimport { GRAPH_SECTION } from \"../section\";\n\n/**\n * The glyphs this canvas draws — **by name**, because a name is what the concept is.\n *\n * This was five exports of one idea: a `SHAPE` object mapping names to numbers, a `ShapeId` type\n * that was the union of those numbers, `SHAPE_ORDER`, `SHAPE_OTHER` and `SHAPE_PATH` keyed by them.\n * The numbers were never ours. They are cosmos.gl's `setPointShapes` enum indices, and the jump from\n * `3` (diamond) to `7` (cross) is what gives that away — there is no `4`, `5` or `6` here because\n * Pentagon, Hexagon and Star are members this canvas does not draw. Publishing them made the\n * renderer's internal numbering part of a contract, so a host held `3` where it meant *diamond*, and\n * an upstream enum that renumbered would have moved every legend on every page silently.\n *\n * A string union says the same thing, reads at the call site — `<ShapeGlyph shape=\"cross\" />` — and\n * makes the mapping this file's private business, which is what it always was.\n */\nexport type Shape = \"circle\" | \"square\" | \"triangle\" | \"diamond\" | \"cross\";\n\n/**\n * The name → cosmos.gl enum index, and the one place that translation happens.\n *\n * `None` (`8`) has no name here on purpose: the point fragment shader `discard`s a `NONE` point that\n * carries no image, so \"past capacity\" spelled as `None` would delete the node from the picture, and\n * a category nobody can name is still a node with edges. `obligations.ts` grades that.\n *\n * Module-scoped and off the barrel. `buffers` reads it on the way to the GPU; nothing else needs it,\n * and anything that did would be reaching for the enum this type exists to hide.\n */\nexport const SHAPE_INDEX: Record<Shape, number> = {\n circle: 0,\n square: 1,\n triangle: 2,\n diamond: 3,\n cross: 7,\n};\n\n/**\n * The shape scale, in slot order — the sibling of the colour scale.\n *\n * Plot calls this channel `symbol` and gives it its own legend, which is the tell that it is a peer\n * of colour rather than a decoration. Four slots and no cycling: a fifth category cannot wear circle\n * again without claiming to be the first one.\n *\n * **Four because the shape channel's measured capacity is five, not because small shapes stop being\n * distinguishable.** Giovannangeli et al. (arXiv 2103.06084) put the ceiling at **5 for shape**\n * against **7 for colour**; `SHAPE_ORDER`'s four plus `SHAPE_OTHER` is exactly five distinguishable\n * glyphs, and the two scales differ in cardinality because the channels do. The colour side already\n * says the same thing from the other end — Dracula's document publishes `--chart-capacity: 7`.\n *\n * **This comment used to give the size argument, and the size argument is wrong.** It said a\n * pentagon, a hexagon and a circle are one dot at Ink's floor. Smart & Szafir (CHI 2019,\n * doi:10.1145/3290605.3300899) measured 16 shapes across 6 mark sizes from 6 to 50 px and found\n * shape discrimination *robust* to size: the only significant variation is at 6 px, and it is 4.5\n * accuracy points against 50 px. The conclusion survived the argument that was given for it, which\n * is the most dangerous shape a comment can have — see `OBLIGATIONS` in `./obligations.ts` for what\n * the floor actually protects.\n */\nexport const SHAPE_ORDER: Shape[] = [\"circle\", \"square\", \"triangle\", \"diamond\"];\n\n/**\n * What a category past the scale wears — the shape channel's `--muted-foreground`.\n *\n * No slot wears it, which is the whole point: it says *not one of the four* rather than repeating\n * the first one. This is the half that used to be missing, and the comment above was false without\n * it — `SHAPE_ORDER[4]` is `undefined`, and the fallback was `circle`.\n */\nexport const SHAPE_OTHER: Shape = \"cross\";\n\n/**\n * The geometry a canvas draws — and nothing else.\n *\n * **No `id`, no `label`, no `blurb`.** Those three were a picker's metadata, and the picker was a\n * list of three names that turned out to be two pictures. What a person chooses is declared in\n * `section.ts` as axes and resolved by `lookFrom`; what a renderer consumes is this.\n */\nexport interface Look {\n /** Radius at the lowest degree in the corpus, and at the highest. */\n size: [number, number];\n link: {\n /**\n * Whether the edge layer is drawn at all.\n *\n * It was `Display.links`, a second vocabulary for the picture that sat beside this one with its\n * own default and its own switch. A form that draws no links is a form.\n */\n render: boolean;\n opacity: number;\n width: number;\n /**\n * How far a link bows off the straight line, as a fraction of its length. `0` is straight.\n *\n * Keep it small. Every link curves the same way, so at cosmos.gl's default of `0.5` a few\n * hundred of them read as one pinwheel and the picture looks like it is spinning — motion\n * where there is structure. A hint is enough to tell two parallel edges apart.\n */\n curve: number;\n /**\n * Whether links **add** where they overlap, instead of compositing over one another.\n *\n * cosmos.gl's default is on, which is a choice nobody here made and which the archive made\n * visible: 4,280 grey links at 0.45 summed to a white spray that swallowed 1,543 points of\n * 2–9 px. Every point was uploaded, none was legible, and the picture read as *the nodes are\n * not rendering*.\n *\n * On, it is a real register rather than a bug — additive light is what makes a dense graph read\n * as flow — so it belongs to the form that wants it and not to the renderer's defaults.\n */\n blend: boolean;\n /**\n * Screen lengths between which a link fades out — depth, for free.\n *\n * Keep the far end generous. cosmos.gl measures this in *screen* pixels, so a range that\n * looks reasonable while zoomed in erases the whole edge layer when you zoom out, which\n * reads as \"Show links stopped working\".\n */\n fade: [number, number];\n };\n /** How many of the highest-degree nodes carry a standing label. */\n labels: number;\n /** A darkened rim. Mood rather than a reading aid, which is why it is form and not display. */\n vignette: boolean;\n /**\n * The dot grid behind the graph, which pans and subdivides with the camera.\n *\n * Beside `vignette` because it is the same kind of thing — the backdrop a form sits on — and it\n * arrived from the same place the rim did not: a `Display` interface that spelled the backdrop,\n * the edge layer and two multipliers as a second set of appearance controls.\n */\n grid: boolean;\n // **There is no `pointScale` and no `linkOpacity`**, and they are the two fields `Display` had that\n // this does not. Each was a reader's multiplier over a number this builder already computes from\n // the axis that owns it — `size` from `marks`, `link.opacity` from `marks` — so the panel offered\n // two ways to say one thing and the second could always overrule the measurement. The measured\n // pair (0.28 legible, 0.42 dense) is the whole argument of `marks`; a slider on top of it is the\n // post-process `graph-model.ts` forbids one layer down, wearing a preference's clothes.\n //\n // What is lost with them is the fit-to-corpus case, and it was never a preference: a computed fit\n // belongs to the tenant's starting point — which is a policy, and which is now expressible.\n\n // There is no `filter`, and its absence is a rule rather than an omission. Nebula carried\n // `saturate(1.1)` on the canvas element — the one thing left in a Look that touched hue, and a\n // chroma multiplier is colour wearing geometry's clothes. Measured over Kanzo's eight slots\n // (2026-08-13, `saturate(1.1)` through the same filter engine the browser applies): it moved\n // every slot, by ΔE 0.85 to **8.05**, which is the size of the separation `deriveScheme`\n // *guarantees* between two different categories. It did not break that separation here —\n // the closest pair went from ΔE 32.36 to 30.38, with room to spare — so the reason it is gone\n // is not a failure it caused, it is that a look must not be able to cause one. The whole claim\n // of the colour layer is that what ships is what was derived and measured; a post-process on\n // the canvas voids it silently, and `graph-model.ts` already forbids the same move one layer\n // down (\"nudging one on the way to the GPU voids all three\").\n}\n\n/**\n * The form a canvas draws, from the axes a person chose.\n *\n * **One function and no table of constants**, which is the shape the axes forced and the reason\n * the three named looks are gone. They were three parallel tables of ten fields; six of those\n * fields separated two of the three by 7–17%, under this file's own threshold for a difference\n * meaning anything — a luminance JND of 6.48–11.30 ΔL*. What is left of them is this: every number\n * appears once, where the axis that owns it is read, and the axis is declared next door in\n * `section.ts` for a panel to draw.\n *\n * **Link opacity and width ride the mark**, and that is what the numbers said rather than a tidy\n * guess: 0.42 and 0.45 on the two dense forms against 0.28 on the legible one. A form that spends\n * more ink on points cannot also spend it on edges.\n *\n * **The legible mark's radius is the whole reason that mark exists.** Its floor protects the other\n * two channels from shape rather than shape from smallness, which is why it is the one to pair\n * `symbol` with: spending shape on identity *and* size on degree at once is what Giovannangeli et\n * al. (arXiv 2103.06084) measure as dropping performance drastically under even minor heterogeneity,\n * and smaller marks worsen it both ways — the luminance JND above, and a square reported larger than\n * any other shape at equal area in 82% of trials (Smart & Szafir, CHI 2019). Not \"a triangle and a\n * square are the same dot below four pixels\", which is what this said for months and is false; see\n * `SHAPE_ORDER` for the measurement that refutes it.\n *\n * **The values are strings because a contributed preference is a string**, in all three kinds, so an\n * unrecognised namespace rides through a write untouched. Parsing what a kind means is the reader's\n * job and it is two lines; `@kanzo-tech/theme` exports the same two, and importing them would add a\n * dependency to this package to carry no code — the same call `GRAPH_SECTION` already makes about the\n * manifest type.\n *\n * A key that is missing, or that carries a value the section never offered, takes the manifest's\n * default. Those defaults are what a graph drew before any of this existed.\n */\nexport function lookFrom(values: Readonly<Record<string, string | undefined>> = {}): Look {\n const on = (key: string, fallback: boolean) => {\n const value = values[key];\n return value === undefined ? fallback : value === \"true\";\n };\n const legible = values.marks === \"legible\";\n const labels = Number.parseFloat(values.labels ?? \"\");\n return {\n size: legible ? [4, 13] : [2, 8],\n link: {\n render: on(\"links\", true),\n opacity: legible ? 0.28 : 0.42,\n width: legible ? 0.5 : 0.6,\n // A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default\n // of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two\n // pictures — straight, and told apart — not a number to tune.\n curve: on(\"bowed-links\", true) ? 0.12 : 0,\n blend: on(\"additive-links\", false),\n // Shared by every form. It was three ranges within ±10% of each other, which is the\n // definition of a field nobody chose.\n fade: [200, 1400],\n },\n labels: Number.isFinite(labels) ? labels : 26,\n vignette: on(\"vignette\", false),\n grid: on(\"grid\", true),\n };\n}\n\n/** The three named looks. */\nexport type LookPreset = \"nebula\" | \"atlas\" | \"ink\";\n\n/**\n * **The three looks as values of the section's axes**, in the order a picker offers them. Each writes\n * every axis any of them names, so wearing one leaves nothing of the last, and a look is worn exactly\n * when the resolved axes equal its own — no name is stored beside them. Atlas is every default.\n */\nexport const PRESETS: Record<LookPreset, Readonly<Record<string, string>>> = {\n nebula: { marks: \"dense\", \"additive-links\": \"true\", \"bowed-links\": \"false\", labels: \"14\", vignette: \"true\" },\n atlas: { marks: \"dense\", \"additive-links\": \"false\", \"bowed-links\": \"true\", labels: \"26\", vignette: \"false\" },\n ink: { marks: \"legible\", \"additive-links\": \"false\", \"bowed-links\": \"false\", labels: \"40\", vignette: \"false\" },\n};\n\n/** The look the axes are — a missing one is the section's default — or `null` once customised past all three. */\nexport function presetOf(values: Readonly<Record<string, string | undefined>>): LookPreset | null {\n const value = (key: string) => values[key] ?? GRAPH_SECTION.prefs?.[key]?.default;\n const found = (Object.keys(PRESETS) as LookPreset[]).find((id) =>\n Object.entries(PRESETS[id]).every(([key, wanted]) => value(key) === wanted),\n );\n return found ?? null;\n}\n\n/**\n * What a canvas draws when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be.** It is literally `lookFrom()` — a second public name for\n * a value the package already hands out on request — and the reason it was exported is the one\n * `resolveLook` below removes: a host that wanted one field different had to start from the whole\n * object, because `look` took a whole `Look`. Spreading a default you were given is a copy of it,\n * and a copy is what stops tracking the original the next time a number here moves.\n */\nexport const DEFAULT_LOOK: Look = lookFrom();\n\n/**\n * A look in the pieces a caller wants different — everything else is this package's answer.\n *\n * Two levels, because a `Look` has exactly two: the fields, and `link`. Deep-merging arbitrarily\n * would be a guess about a shape that is right here in this file, and `size` and `fade` are tuples\n * that must be replaced whole rather than merged element-wise.\n */\nexport interface LookPatch extends Partial<Omit<Look, \"link\">> {\n link?: Partial<Look[\"link\"]>;\n}\n\n/**\n * A patch over the package's own default — the merge `DEFAULT_LOOK` existed so a host could do by\n * hand.\n *\n * **Nothing, and it is the shared constant rather than a copy of it.** That identity matters: the\n * buffers are rebuilt whenever the look's reference changes, so a fresh object per render would\n * mean a full colour/size/shape upload on every render of every canvas that never asked for one.\n */\nexport function resolveLook(patch?: LookPatch): Look {\n if (!patch) return DEFAULT_LOOK;\n return { ...DEFAULT_LOOK, ...patch, link: { ...DEFAULT_LOOK.link, ...patch.link } };\n}\n\n/**\n * The SVG path for a shape glyph inside a 12×12 box — the legend draws what the canvas draws.\n *\n * **Off the barrel, and `ShapeGlyph` is what replaced it.** One host imported this, to fill a\n * `<path>` in a legend key and a hover card. What that host actually wanted was *the glyph*, and\n * handing it the path data made it responsible for the viewBox, the fill and the fact that the box\n * is twelve units — three facts it had to keep in step with this file by reading the comment above.\n * A component carries all three and cannot fall out of step with itself.\n */\nexport const SHAPE_PATH: Record<Shape, string> = {\n circle: \"M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z\",\n square: \"M2 2h8v8H2Z\",\n triangle: \"M6 1.6 10.6 10H1.4Z\",\n diamond: \"M6 1 11 6l-5 5-5-5Z\",\n // The proportions are cosmos.gl's own `crossDistance`: a plus with arms at 0.8 of the radius and\n // a bar 0.3 thick, so the legend's glyph is the shape the shader draws.\n cross: \"M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z\",\n};\n"],"names":["SHAPE_INDEX","SHAPE_ORDER","SHAPE_OTHER","lookFrom","values","on","key","fallback","value","legible","labels","PRESETS","presetOf","_b","_a","GRAPH_SECTION","id","wanted","DEFAULT_LOOK","resolveLook","patch","SHAPE_PATH"],"mappings":";AA+CO,MAAMA,IAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AACT,GAuBaC,IAAuB,CAAC,UAAU,UAAU,YAAY,SAAS,GASjEC,IAAqB;AAsH3B,SAASC,EAASC,IAAuD,IAAU;AACxF,QAAMC,IAAK,CAACC,GAAaC,MAAsB;AAC7C,UAAMC,IAAQJ,EAAOE,CAAG;AACxB,WAAOE,MAAU,SAAYD,IAAWC,MAAU;AAAA,EACpD,GACMC,IAAUL,EAAO,UAAU,WAC3BM,IAAS,OAAO,WAAWN,EAAO,UAAU,EAAE;AACpD,SAAO;AAAA,IACL,MAAMK,IAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AAAA,IAC/B,MAAM;AAAA,MACJ,QAAQJ,EAAG,SAAS,EAAI;AAAA,MACxB,SAASI,IAAU,OAAO;AAAA,MAC1B,OAAOA,IAAU,MAAM;AAAA;AAAA;AAAA;AAAA,MAIvB,OAAOJ,EAAG,eAAe,EAAI,IAAI,OAAO;AAAA,MACxC,OAAOA,EAAG,kBAAkB,EAAK;AAAA;AAAA;AAAA,MAGjC,MAAM,CAAC,KAAK,IAAI;AAAA,IAAA;AAAA,IAElB,QAAQ,OAAO,SAASK,CAAM,IAAIA,IAAS;AAAA,IAC3C,UAAUL,EAAG,YAAY,EAAK;AAAA,IAC9B,MAAMA,EAAG,QAAQ,EAAI;AAAA,EAAA;AAEzB;AAUO,MAAMM,IAAgE;AAAA,EAC3E,QAAQ,EAAE,OAAO,SAAS,kBAAkB,QAAQ,eAAe,SAAS,QAAQ,MAAM,UAAU,OAAA;AAAA,EACpG,OAAO,EAAE,OAAO,SAAS,kBAAkB,SAAS,eAAe,QAAQ,QAAQ,MAAM,UAAU,QAAA;AAAA,EACnG,KAAK,EAAE,OAAO,WAAW,kBAAkB,SAAS,eAAe,SAAS,QAAQ,MAAM,UAAU,QAAA;AACtG;AAGO,SAASC,EAASR,GAAyE;AAChG,QAAMI,IAAQ,CAACF,MAAA;;AAAgB,WAAAF,EAAOE,CAAG,OAAKO,KAAAC,IAAAC,EAAc,UAAd,gBAAAD,EAAsBR,OAAtB,gBAAAO,EAA4B;AAAA;AAI1E,SAHe,OAAO,KAAKF,CAAO,EAAmB;AAAA,IAAK,CAACK,MACzD,OAAO,QAAQL,EAAQK,CAAE,CAAC,EAAE,MAAM,CAAC,CAACV,GAAKW,CAAM,MAAMT,EAAMF,CAAG,MAAMW,CAAM;AAAA,EAAA,KAE5D;AAClB;AAWO,MAAMC,IAAqBf,EAAA;AAqB3B,SAASgB,EAAYC,GAAyB;AACnD,SAAKA,IACE,EAAE,GAAGF,GAAc,GAAGE,GAAO,MAAM,EAAE,GAAGF,EAAa,MAAM,GAAGE,EAAM,OAAK,IAD7DF;AAErB;AAWO,MAAMG,IAAoC;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO;AACT;"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Geometry } from '../core/load';
|
|
2
|
+
type Extent = NonNullable<Geometry["extent"]>;
|
|
3
|
+
/**
|
|
4
|
+
* **Where the corpus's extent sits in cosmos.gl's square** — centred, at its own scale, and scaled
|
|
5
|
+
* down only past the box the device can simulate in. Drawing alone is translation-invariant, but the
|
|
6
|
+
* layout is not: gravity pulls toward the square's centre and many-body bins points on a grid over
|
|
7
|
+
* `[0, side]²`, so an extent left where it was contracts toward a corner and falls off the grid.
|
|
8
|
+
*/
|
|
9
|
+
export interface Placement {
|
|
10
|
+
readonly side: number;
|
|
11
|
+
readonly k: number;
|
|
12
|
+
readonly dx: number;
|
|
13
|
+
readonly dy: number;
|
|
14
|
+
}
|
|
15
|
+
/** No extent — no row has a position — so nothing moves and nothing is drawn. */
|
|
16
|
+
export declare const UNPLACED: Placement;
|
|
17
|
+
export declare function placementOf(extent: Extent, box: number): Placement;
|
|
18
|
+
/** The loaded positions in the square, with `NaN` where the page's filter hides a vertex — cosmos.gl draws no such point. */
|
|
19
|
+
export declare function placed(geometry: Geometry, at: Placement, mask: Uint8Array | null): Float32Array;
|
|
20
|
+
/** The extent's corners in the square, for a camera that frames it. */
|
|
21
|
+
export declare function cornersOf(extent: Extent, at: Placement): number[];
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=placement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../src/render/placement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,iFAAiF;AACjF,eAAO,MAAM,QAAQ,EAAE,SAA2C,CAAC;AAEnE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAKlE;AAED,6HAA6H;AAC7H,wBAAgB,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,YAAY,CAS/F;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,GAAG,MAAM,EAAE,CAEjE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const u = { side: 0, k: 1, dx: 0, dy: 0 };
|
|
2
|
+
function l(d, o) {
|
|
3
|
+
const s = Math.max(d.w, d.h, 2), n = s > o ? o / s : 1, c = s * n;
|
|
4
|
+
return { side: c, k: n, dx: (c - d.w * n) / 2 - d.x * n, dy: (c - d.h * n) / 2 - d.y * n };
|
|
5
|
+
}
|
|
6
|
+
function y(d, o, s) {
|
|
7
|
+
const { positions: n } = d, c = new Float32Array(n.length);
|
|
8
|
+
for (let r = 0; r < d.size; r++) {
|
|
9
|
+
const i = s !== null && !s[r];
|
|
10
|
+
c[r * 2] = i ? Number.NaN : n[r * 2] * o.k + o.dx, c[r * 2 + 1] = i ? Number.NaN : n[r * 2 + 1] * o.k + o.dy;
|
|
11
|
+
}
|
|
12
|
+
return c;
|
|
13
|
+
}
|
|
14
|
+
function k(d, o) {
|
|
15
|
+
return [d.x * o.k + o.dx, d.y * o.k + o.dy, (d.x + d.w) * o.k + o.dx, (d.y + d.h) * o.k + o.dy];
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
u as UNPLACED,
|
|
19
|
+
k as cornersOf,
|
|
20
|
+
y as placed,
|
|
21
|
+
l as placementOf
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=placement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.js","sources":["../../src/render/placement.ts"],"sourcesContent":["import type { Geometry } from \"../core/load\";\n\ntype Extent = NonNullable<Geometry[\"extent\"]>;\n\n/**\n * **Where the corpus's extent sits in cosmos.gl's square** — centred, at its own scale, and scaled\n * down only past the box the device can simulate in. Drawing alone is translation-invariant, but the\n * layout is not: gravity pulls toward the square's centre and many-body bins points on a grid over\n * `[0, side]²`, so an extent left where it was contracts toward a corner and falls off the grid.\n */\nexport interface Placement {\n readonly side: number;\n readonly k: number;\n readonly dx: number;\n readonly dy: number;\n}\n\n/** No extent — no row has a position — so nothing moves and nothing is drawn. */\nexport const UNPLACED: Placement = { side: 0, k: 1, dx: 0, dy: 0 };\n\nexport function placementOf(extent: Extent, box: number): Placement {\n const longest = Math.max(extent.w, extent.h, 2);\n const k = longest > box ? box / longest : 1;\n const side = longest * k;\n return { side, k, dx: (side - extent.w * k) / 2 - extent.x * k, dy: (side - extent.h * k) / 2 - extent.y * k };\n}\n\n/** The loaded positions in the square, with `NaN` where the page's filter hides a vertex — cosmos.gl draws no such point. */\nexport function placed(geometry: Geometry, at: Placement, mask: Uint8Array | null): Float32Array {\n const { positions } = geometry;\n const out = new Float32Array(positions.length);\n for (let id = 0; id < geometry.size; id++) {\n const hidden = mask !== null && !mask[id];\n out[id * 2] = hidden ? Number.NaN : (positions[id * 2] as number) * at.k + at.dx;\n out[id * 2 + 1] = hidden ? Number.NaN : (positions[id * 2 + 1] as number) * at.k + at.dy;\n }\n return out;\n}\n\n/** The extent's corners in the square, for a camera that frames it. */\nexport function cornersOf(extent: Extent, at: Placement): number[] {\n return [extent.x * at.k + at.dx, extent.y * at.k + at.dy, (extent.x + extent.w) * at.k + at.dx, (extent.y + extent.h) * at.k + at.dy];\n}\n"],"names":["UNPLACED","placementOf","extent","box","longest","k","side","placed","geometry","at","mask","positions","out","id","hidden","cornersOf"],"mappings":"AAkBO,MAAMA,IAAsB,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,EAAA;AAExD,SAASC,EAAYC,GAAgBC,GAAwB;AAClE,QAAMC,IAAU,KAAK,IAAIF,EAAO,GAAGA,EAAO,GAAG,CAAC,GACxCG,IAAID,IAAUD,IAAMA,IAAMC,IAAU,GACpCE,IAAOF,IAAUC;AACvB,SAAO,EAAE,MAAAC,GAAM,GAAAD,GAAG,KAAKC,IAAOJ,EAAO,IAAIG,KAAK,IAAIH,EAAO,IAAIG,GAAG,KAAKC,IAAOJ,EAAO,IAAIG,KAAK,IAAIH,EAAO,IAAIG,EAAA;AAC7G;AAGO,SAASE,EAAOC,GAAoBC,GAAeC,GAAuC;AAC/F,QAAM,EAAE,WAAAC,MAAcH,GAChBI,IAAM,IAAI,aAAaD,EAAU,MAAM;AAC7C,WAASE,IAAK,GAAGA,IAAKL,EAAS,MAAMK,KAAM;AACzC,UAAMC,IAASJ,MAAS,QAAQ,CAACA,EAAKG,CAAE;AACxC,IAAAD,EAAIC,IAAK,CAAC,IAAIC,IAAS,OAAO,MAAOH,EAAUE,IAAK,CAAC,IAAeJ,EAAG,IAAIA,EAAG,IAC9EG,EAAIC,IAAK,IAAI,CAAC,IAAIC,IAAS,OAAO,MAAOH,EAAUE,IAAK,IAAI,CAAC,IAAeJ,EAAG,IAAIA,EAAG;AAAA,EACxF;AACA,SAAOG;AACT;AAGO,SAASG,EAAUb,GAAgBO,GAAyB;AACjE,SAAO,CAACP,EAAO,IAAIO,EAAG,IAAIA,EAAG,IAAIP,EAAO,IAAIO,EAAG,IAAIA,EAAG,KAAKP,EAAO,IAAIA,EAAO,KAAKO,EAAG,IAAIA,EAAG,KAAKP,EAAO,IAAIA,EAAO,KAAKO,EAAG,IAAIA,EAAG,EAAE;AACtI;"}
|
|
@@ -33,6 +33,7 @@ export interface Renderer extends GraphCommands {
|
|
|
33
33
|
* `transitionDuration` is 0: the default animates every upload for 800 ms and keeps the loop awake.
|
|
34
34
|
* The live layout is off unless `simulate` or the toolbar asks, and runs from the current positions.
|
|
35
35
|
* Every callback reads the store's latest options, so a host's inline `onFailure` never rebuilds it.
|
|
36
|
+
* A failure to draw at all marks the store `failed` through `unrenderable`, never `onFailure` alone.
|
|
36
37
|
*/
|
|
37
38
|
export declare function createRenderer(host: HTMLDivElement, store: GraphStore, events?: RendererEvents): Renderer | null;
|
|
38
39
|
//# sourceMappingURL=renderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/render/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/render/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,eAAe,CAAC;AAQ7D,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;CACvD;AAED,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,kFAAkF;IAClF,OAAO,IAAI,IAAI,CAAC;IAChB,uFAAuF;IACvF,GAAG,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;KAAE,GAAG,MAAM,EAAE,CAAC;IACvG,OAAO,IAAI,IAAI,CAAC;CACjB;AAaD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,GAAE,cAAmB,GAAG,QAAQ,GAAG,IAAI,CA+TpH"}
|