@kanzo-tech/graph 0.10.0 → 0.11.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 +60 -95
- package/dist/core/categories.d.ts +12 -0
- package/dist/core/categories.d.ts.map +1 -0
- package/dist/core/categories.js +14 -0
- package/dist/core/categories.js.map +1 -0
- package/dist/core/channels.d.ts +34 -0
- package/dist/core/channels.d.ts.map +1 -0
- package/dist/core/channels.js +19 -0
- package/dist/core/channels.js.map +1 -0
- package/dist/core/detail.d.ts +18 -0
- package/dist/core/detail.d.ts.map +1 -0
- package/dist/core/detail.js +20 -0
- package/dist/core/detail.js.map +1 -0
- package/dist/core/filter.d.ts +34 -0
- package/dist/core/filter.d.ts.map +1 -0
- package/dist/core/filter.js +99 -0
- package/dist/core/filter.js.map +1 -0
- package/dist/core/refine.d.ts +14 -0
- package/dist/core/refine.d.ts.map +1 -0
- package/dist/core/refine.js +20 -0
- package/dist/core/refine.js.map +1 -0
- package/dist/{resident.d.ts → core/resident.d.ts} +7 -7
- package/dist/core/resident.d.ts.map +1 -0
- package/dist/core/resident.js +44 -0
- package/dist/core/resident.js.map +1 -0
- package/dist/core/scheduler.d.ts +42 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +77 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/state.d.ts +132 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +235 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/tile-matrix.d.ts +37 -0
- package/dist/core/tile-matrix.d.ts.map +1 -0
- package/dist/core/tile-matrix.js +49 -0
- package/dist/core/tile-matrix.js.map +1 -0
- package/dist/core/tile.d.ts +54 -0
- package/dist/core/tile.d.ts.map +1 -0
- package/dist/core/tile.js +76 -0
- package/dist/core/tile.js.map +1 -0
- package/dist/core/tileset.d.ts +59 -0
- package/dist/core/tileset.d.ts.map +1 -0
- package/dist/core/tileset.js +177 -0
- package/dist/core/tileset.js.map +1 -0
- package/dist/{types.d.ts → core/types.d.ts} +9 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +37 -70
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -34
- package/dist/index.js.map +1 -1
- package/dist/{use-graph-selection.d.ts → parts/gesture.d.ts} +4 -4
- package/dist/parts/gesture.d.ts.map +1 -0
- package/dist/{use-graph-selection.js → parts/gesture.js} +17 -17
- package/dist/parts/gesture.js.map +1 -0
- package/dist/parts/graph-canvas.d.ts +17 -0
- package/dist/parts/graph-canvas.d.ts.map +1 -0
- package/dist/parts/graph-canvas.js +174 -0
- package/dist/parts/graph-canvas.js.map +1 -0
- package/dist/parts/graph-inspector.d.ts +18 -0
- package/dist/parts/graph-inspector.d.ts.map +1 -0
- package/dist/parts/graph-inspector.js +84 -0
- package/dist/parts/graph-inspector.js.map +1 -0
- package/dist/parts/graph-legend.d.ts +12 -0
- package/dist/parts/graph-legend.d.ts.map +1 -0
- package/dist/parts/graph-legend.js +61 -0
- package/dist/parts/graph-legend.js.map +1 -0
- package/dist/parts/graph-toolbar.d.ts +15 -0
- package/dist/parts/graph-toolbar.d.ts.map +1 -0
- package/dist/parts/graph-toolbar.js +94 -0
- package/dist/parts/graph-toolbar.js.map +1 -0
- package/dist/parts/overlays.d.ts +33 -0
- package/dist/parts/overlays.d.ts.map +1 -0
- package/dist/{use-graph-overlays.js → parts/overlays.js} +8 -8
- package/dist/parts/overlays.js.map +1 -0
- package/dist/{shape-glyph.d.ts → parts/shape-glyph.d.ts} +1 -1
- package/dist/parts/shape-glyph.d.ts.map +1 -0
- package/dist/{shape-glyph.js → parts/shape-glyph.js} +1 -1
- package/dist/parts/shape-glyph.js.map +1 -0
- package/dist/react/graph-root.d.ts +15 -0
- package/dist/react/graph-root.d.ts.map +1 -0
- package/dist/react/graph-root.js +23 -0
- package/dist/react/graph-root.js.map +1 -0
- package/dist/{use-graph-prefs.d.ts → react/use-graph-prefs.d.ts} +2 -2
- package/dist/react/use-graph-prefs.d.ts.map +1 -0
- package/dist/{use-graph-prefs.js → react/use-graph-prefs.js} +3 -3
- package/dist/react/use-graph-prefs.js.map +1 -0
- package/dist/react/use-graph-state.d.ts +11 -0
- package/dist/react/use-graph-state.d.ts.map +1 -0
- package/dist/react/use-graph-state.js +16 -0
- package/dist/react/use-graph-state.js.map +1 -0
- package/dist/react/use-graph.d.ts +38 -0
- package/dist/react/use-graph.d.ts.map +1 -0
- package/dist/react/use-graph.js +87 -0
- package/dist/react/use-graph.js.map +1 -0
- package/dist/{adaptive.d.ts → render/adaptive.d.ts} +1 -1
- package/dist/render/adaptive.d.ts.map +1 -0
- package/dist/render/adaptive.js.map +1 -0
- package/dist/render/compose.d.ts +51 -0
- package/dist/render/compose.d.ts.map +1 -0
- package/dist/render/compose.js +107 -0
- package/dist/render/compose.js.map +1 -0
- package/dist/render/css-color.d.ts.map +1 -0
- package/dist/render/css-color.js.map +1 -0
- package/dist/render/encode.d.ts +42 -0
- package/dist/render/encode.d.ts.map +1 -0
- package/dist/render/encode.js +68 -0
- package/dist/render/encode.js.map +1 -0
- package/dist/render/graph-looks.d.ts.map +1 -0
- package/dist/render/graph-looks.js.map +1 -0
- package/dist/render/graph-model.d.ts +44 -0
- package/dist/render/graph-model.d.ts.map +1 -0
- package/dist/render/graph-model.js +80 -0
- package/dist/render/graph-model.js.map +1 -0
- package/dist/{graph-sim.d.ts → render/graph-sim.d.ts} +1 -1
- package/dist/render/graph-sim.d.ts.map +1 -0
- package/dist/render/graph-sim.js.map +1 -0
- package/dist/{obligations.d.ts → render/obligations.d.ts} +1 -1
- package/dist/render/obligations.d.ts.map +1 -0
- package/dist/render/renderer.d.ts +38 -0
- package/dist/render/renderer.d.ts.map +1 -0
- package/dist/render/renderer.js +235 -0
- package/dist/render/renderer.js.map +1 -0
- package/dist/render/webgl.d.ts +14 -0
- package/dist/render/webgl.d.ts.map +1 -0
- package/dist/render/webgl.js +19 -0
- package/dist/render/webgl.js.map +1 -0
- package/dist/render/when-ready.d.ts.map +1 -0
- package/dist/render/when-ready.js.map +1 -0
- package/package.json +11 -21
- package/dist/adaptive.d.ts.map +0 -1
- package/dist/adaptive.js.map +0 -1
- package/dist/bounded.d.ts +0 -364
- package/dist/bounded.d.ts.map +0 -1
- package/dist/bounded.js +0 -18
- package/dist/bounded.js.map +0 -1
- package/dist/cluster-ring.d.ts +0 -25
- package/dist/cluster-ring.d.ts.map +0 -1
- package/dist/cluster-ring.js +0 -16
- package/dist/cluster-ring.js.map +0 -1
- package/dist/css-color.d.ts.map +0 -1
- package/dist/css-color.js.map +0 -1
- package/dist/duck-source.d.ts +0 -198
- package/dist/duck-source.d.ts.map +0 -1
- package/dist/duck-source.js +0 -320
- package/dist/duck-source.js.map +0 -1
- package/dist/graph-canvas.d.ts +0 -50
- package/dist/graph-canvas.d.ts.map +0 -1
- package/dist/graph-canvas.js +0 -35
- package/dist/graph-canvas.js.map +0 -1
- package/dist/graph-looks.d.ts.map +0 -1
- package/dist/graph-looks.js.map +0 -1
- package/dist/graph-model.d.ts +0 -130
- package/dist/graph-model.d.ts.map +0 -1
- package/dist/graph-model.js +0 -104
- package/dist/graph-model.js.map +0 -1
- package/dist/graph-sim.d.ts.map +0 -1
- package/dist/graph-sim.js.map +0 -1
- package/dist/obligations.d.ts.map +0 -1
- package/dist/resident.d.ts.map +0 -1
- package/dist/resident.js +0 -44
- package/dist/resident.js.map +0 -1
- package/dist/shape-glyph.d.ts.map +0 -1
- package/dist/shape-glyph.js.map +0 -1
- package/dist/slice-client.d.ts +0 -78
- package/dist/slice-client.d.ts.map +0 -1
- package/dist/slice-client.js +0 -98
- package/dist/slice-client.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/use-graph-look.d.ts +0 -28
- package/dist/use-graph-look.d.ts.map +0 -1
- package/dist/use-graph-look.js +0 -28
- package/dist/use-graph-look.js.map +0 -1
- package/dist/use-graph-overlays.d.ts +0 -44
- package/dist/use-graph-overlays.d.ts.map +0 -1
- package/dist/use-graph-overlays.js.map +0 -1
- package/dist/use-graph-prefs.d.ts.map +0 -1
- package/dist/use-graph-prefs.js.map +0 -1
- package/dist/use-graph-selection.d.ts.map +0 -1
- package/dist/use-graph-selection.js.map +0 -1
- package/dist/use-graph.d.ts +0 -203
- package/dist/use-graph.d.ts.map +0 -1
- package/dist/use-graph.js +0 -102
- package/dist/use-graph.js.map +0 -1
- package/dist/use-query-loop.d.ts +0 -88
- package/dist/use-query-loop.d.ts.map +0 -1
- package/dist/use-query-loop.js +0 -148
- package/dist/use-query-loop.js.map +0 -1
- package/dist/use-renderer.d.ts +0 -86
- package/dist/use-renderer.d.ts.map +0 -1
- package/dist/use-renderer.js +0 -188
- package/dist/use-renderer.js.map +0 -1
- package/dist/when-ready.d.ts.map +0 -1
- package/dist/when-ready.js.map +0 -1
- /package/dist/{adaptive.js → render/adaptive.js} +0 -0
- /package/dist/{css-color.d.ts → render/css-color.d.ts} +0 -0
- /package/dist/{css-color.js → render/css-color.js} +0 -0
- /package/dist/{graph-looks.d.ts → render/graph-looks.d.ts} +0 -0
- /package/dist/{graph-looks.js → render/graph-looks.js} +0 -0
- /package/dist/{graph-sim.js → render/graph-sim.js} +0 -0
- /package/dist/{when-ready.d.ts → render/when-ready.d.ts} +0 -0
- /package/dist/{when-ready.js → render/when-ready.js} +0 -0
package/dist/adaptive.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adaptive.js","sources":["../src/adaptive.ts"],"sourcesContent":["import type { Sim } from \"./graph-sim\";\n\n/**\n * How a graph of *this size* should be drawn — force coefficients and two render\n * switches, interpolated continuously against node count.\n *\n * Absorbed from `@fossil-lang/viewer`'s `getAdaptiveConfig` when fossil stopped shipping a viewer, which filed it under\n * \"level-of-detail policy\". Reading it, that is not quite what it is, and the difference matters:\n * almost all of it is **simulation tuning** — repulsion, friction, spring, gravity — plus two\n * genuinely render-side switches. Level of detail in the bounded sense is the sample a source takes\n * when a window holds more than the limit; it lives in `duck-source.ts` and is a different\n * mechanism entirely.\n *\n * That means most of this only applies under `simulate: true`, which ADR-0001 made the opt-in case.\n * It is still worth having: the host with arrays in hand and no precomputed layout is exactly the\n * host that runs a live simulation, and one set of production-tuned numbers beats each call site\n * inventing its own.\n *\n * **The continuous interpolation is the design**, not an implementation detail. Breakpoints snap —\n * a graph crossing 10,000 nodes would visibly jump — and Cosmograph 1.x is the cautionary example.\n * The constants are tuned across 10 to 100,000 nodes and divergence from them is a defect.\n */\n\nconst clamp = (value: number, lo: number, hi: number): number =>\n Math.min(hi, Math.max(lo, value));\n\nconst lerp = (a: number, b: number, t: number): number => a + (b - a) * clamp(t, 0, 1);\n\n/** `0` at about ten nodes, `1` at about a hundred thousand — log₁₀, because node counts are. */\nfunction scaleOfCount(nodes: number): number {\n return clamp((Math.log10(Math.max(nodes, 1)) - 1) / 4, 0, 1);\n}\n\n/**\n * The coefficients and the drawing options a graph this size wants.\n *\n * Returned together because they are one judgement asked of one number, and splitting them into two\n * exports that must be called with the same argument is how they drift apart. The caller still keeps\n * them apart downstream — a `Sim` rebuilds nothing and `links` is a uniform — which is the split\n * that actually costs something.\n *\n * **The mark scale left with `Display`.** This used to answer a `pointScale` too, interpolated from\n * 1.5 to 0.5 with the corpus, and nothing ever called it: the field it fed was a reader's multiplier\n * over the radius ramp `lookFrom` computes from `marks`, and two ways to size a mark is one too\n * many. What a host with a large corpus wants is the tenant policy — *start your users here* — over\n * the declared axes, which is where a computed fit belongs.\n *\n * **`spaceSize` is deliberately not here.** The original scaled the simulation box from 2,048 to\n * 8,192 with the corpus, which is coherent when a layout is computed on the fly and incoherent once\n * positions are authority: the box is the coordinate space a source's positions are expressed in and\n * a spatial query is asked against, so resizing it by node count would move the index under the\n * camera. It is not a constant of ours either, for the same reason turned around — it is the\n * source's `extent()`, and `useQueryLoop` sets it from there.\n */\nexport function adaptive(nodes: number): { sim: Sim; links: boolean } {\n const t = scaleOfCount(nodes);\n return {\n sim: {\n // Bigger graphs need less push and more damping, or they never settle.\n repulsion: lerp(1.2, 0.4, t),\n friction: lerp(0.7, 0.92, t),\n linkSpring: lerp(0.5, 0.25, t),\n linkDistance: lerp(30, 12, t),\n gravity: lerp(0.35, 0.08, t),\n cluster: 0.15,\n },\n // Past a quarter of a million links the edge layer is fog, and fog costs a draw call per frame\n // to render. Below it, links are most of what a reader is actually looking at.\n links: nodes < 250_000,\n };\n}\n"],"names":["clamp","value","lo","hi","lerp","a","b","t","scaleOfCount","nodes","adaptive"],"mappings":"AAuBA,MAAMA,IAAQ,CAACC,GAAeC,GAAYC,MACxC,KAAK,IAAIA,GAAI,KAAK,IAAID,GAAID,CAAK,CAAC,GAE5BG,IAAO,CAACC,GAAWC,GAAWC,MAAsBF,KAAKC,IAAID,KAAKL,EAAMO,GAAG,GAAG,CAAC;AAGrF,SAASC,EAAaC,GAAuB;AAC3C,SAAOT,GAAO,KAAK,MAAM,KAAK,IAAIS,GAAO,CAAC,CAAC,IAAI,KAAK,GAAG,GAAG,CAAC;AAC7D;AAuBO,SAASC,EAASD,GAA6C;AACpE,QAAMF,IAAIC,EAAaC,CAAK;AAC5B,SAAO;AAAA,IACL,KAAK;AAAA;AAAA,MAEH,WAAWL,EAAK,KAAK,KAAKG,CAAC;AAAA,MAC3B,UAAUH,EAAK,KAAK,MAAMG,CAAC;AAAA,MAC3B,YAAYH,EAAK,KAAK,MAAMG,CAAC;AAAA,MAC7B,cAAcH,EAAK,IAAI,IAAIG,CAAC;AAAA,MAC5B,SAASH,EAAK,MAAM,MAAMG,CAAC;AAAA,MAC3B,SAAS;AAAA,IAAA;AAAA;AAAA;AAAA,IAIX,OAAOE,IAAQ;AAAA,EAAA;AAEnB;"}
|
package/dist/bounded.d.ts
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import { VertexId } from './resident';
|
|
2
|
-
/**
|
|
3
|
-
* What the camera is looking at, in the graph's own coordinate space.
|
|
4
|
-
*
|
|
5
|
-
* A rectangle and nothing else. It carried a `zoom` for one reader — the level-of-detail threshold
|
|
6
|
-
* a source compared it against to decide whether to answer with super-nodes — and that branch is
|
|
7
|
-
* gone, so the field went with it rather than staying as a number every caller has to invent and
|
|
8
|
-
* nothing reads.
|
|
9
|
-
*/
|
|
10
|
-
export interface Viewport {
|
|
11
|
-
xMin: number;
|
|
12
|
-
yMin: number;
|
|
13
|
-
xMax: number;
|
|
14
|
-
yMax: number;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* One answer. Every array is parallel and indexed densely from zero.
|
|
18
|
-
*
|
|
19
|
-
* `n` is what *matched*, before `limit` cut it — the difference between the two is how a view says
|
|
20
|
-
* "there is more here than I am showing you", which is the one honest thing a bounded renderer owes
|
|
21
|
-
* its reader. When a window holds more than `limit`, what comes back is a **sample** of it rather
|
|
22
|
-
* than its first `limit` rows; `n` reports the window either way.
|
|
23
|
-
*
|
|
24
|
-
* **A struct, and it used to be a tagged union.** `mode` picked between points and super-nodes and
|
|
25
|
-
* only the second branch carried `weights`. Both are gone — see `/docs/design/graph` — and with one
|
|
26
|
-
* branch left a discriminant is a
|
|
27
|
-
* field with one legal value.
|
|
28
|
-
*/
|
|
29
|
-
export interface Slice {
|
|
30
|
-
n: number;
|
|
31
|
-
/**
|
|
32
|
-
* How many of `positions` are **marks** — points a reader can see. Everything past it is an
|
|
33
|
-
* *anchor*.
|
|
34
|
-
*
|
|
35
|
-
* An anchor is a real vertex at its real coordinates that the window did not return: it is in the
|
|
36
|
-
* buffers so that an edge leaving the window has somewhere to end. It is never drawn — `buffers`
|
|
37
|
-
* gives it radius zero and alpha zero, and `residentOf` stops here, so nothing hovers, selects or
|
|
38
|
-
* frames one.
|
|
39
|
-
*
|
|
40
|
-
* **Why the far end is the vertex rather than a point on the border.** A stub clipped to the
|
|
41
|
-
* viewport carries the right direction and *lies about the distance*, and a reader cannot tell a
|
|
42
|
-
* stub that ends 1.1 window-widths out from one that ends 47 — measured over all the far ends a
|
|
43
|
-
* window loses, 20–22% are past four semi-widths and the worst is 47.1
|
|
44
|
-
* (`.planning/FAR-VIEW-AND-EDGES.md`). Drawing the vertex where it is cannot lie, and it is also
|
|
45
|
-
* the cheaper of the two: a clipped stub is one point *per edge*, an anchor is one point per far
|
|
46
|
-
* *vertex*.
|
|
47
|
-
*
|
|
48
|
-
* Equal to `positions.length / 2` for a source that answers with marks only, which is why it is
|
|
49
|
-
* required rather than optional — a caller that reads it always gets the count it meant.
|
|
50
|
-
*/
|
|
51
|
-
marks: number;
|
|
52
|
-
/**
|
|
53
|
-
* Who each returned point *is*, parallel to `positions` — the `(type_idx, dense_id)` pair packed
|
|
54
|
-
* by `vertexId`.
|
|
55
|
-
*
|
|
56
|
-
* Present because a buffer index is **not stable across answers**: index 7 is a different vertex
|
|
57
|
-
* after a pan. Anything that outlives one answer — a selection, a focused node, a pinned set — has
|
|
58
|
-
* to be held as an identity and re-resolved through `residentOf` each time. Leaving this out was
|
|
59
|
-
* how the first draft would have shipped a selection that silently pointed at the wrong nodes.
|
|
60
|
-
*
|
|
61
|
-
* `BigUint64Array` because the pair is 64 bits exactly — a `Uint32Array` cannot hold it at all,
|
|
62
|
-
* and a `Float64Array` holds it only while the type index stays under 2²¹, which is a ceiling
|
|
63
|
-
* nobody would find until they crossed it. Still a typed array, so it is still one allocation and
|
|
64
|
-
* still transferable; only what it carries changed. A dense id on its own is not an identity: it
|
|
65
|
-
* numbers within one vertex type, so a union of two types repeats every value.
|
|
66
|
-
*/
|
|
67
|
-
vertices: BigUint64Array;
|
|
68
|
-
/**
|
|
69
|
-
* The subject IRI of each returned point, parallel to `vertices` — **opt-in, and absent by
|
|
70
|
-
* default.**
|
|
71
|
-
*
|
|
72
|
-
* `vertices` says where a point *is*; this says which vertex it *is*. They are not the same thing
|
|
73
|
-
* and the corpus is explicit about it: redoing a layout renumbers every vertex, so a `dense_id`
|
|
74
|
-
* held outside the corpus names a different vertex after the next write. Anything that has to
|
|
75
|
-
* survive a recompile — a bookmark, a link out, a row in somebody else's database — keys on the
|
|
76
|
-
* IRI. A selection held as `VertexId` survives a pan and does not survive a rebuild.
|
|
77
|
-
*
|
|
78
|
-
* **Absent by default because it costs 1.87× the tile, measured on the corpus side.** Compressed
|
|
79
|
-
* bytes per row at five million: `subject` 8.016 against `dense_id` 4.000, `x` 2.717, `y` 2.501.
|
|
80
|
-
* The four drawing columns are 9.23 B/row and become 17.25 with it. So the drawing path carries
|
|
81
|
-
* addresses, and a host asks for names when something has to be *named* rather than painted.
|
|
82
|
-
*
|
|
83
|
-
* A `string[]` rather than a typed array, because that is what an IRI is. It is the one thing in a
|
|
84
|
-
* `Slice` that does not go to the GPU, which is exactly why it is optional: a host that never
|
|
85
|
-
* names a vertex should not pay to move it.
|
|
86
|
-
*/
|
|
87
|
-
subjects?: string[];
|
|
88
|
-
/** `[x0, y0, x1, y1, …]`, one pair per returned point — `marks` of them, then the anchors. */
|
|
89
|
-
positions: Float32Array;
|
|
90
|
-
/** `[src, dst, …]` as indices into `positions`. */
|
|
91
|
-
links: Float32Array;
|
|
92
|
-
/** Per-point category ordinal, for colour. */
|
|
93
|
-
categories: Uint16Array;
|
|
94
|
-
/**
|
|
95
|
-
* What the size ramp is spent on, per point — a degree, a count, whatever the corpus ranks by.
|
|
96
|
-
*
|
|
97
|
-
* Optional because a source may not have one, and a graph drawn at one radius is a legitimate
|
|
98
|
-
* picture. But without it a look's `form.size` range has only one end, so a source that can afford
|
|
99
|
-
* the column should send it: it is the difference between seeing a hub and counting one.
|
|
100
|
-
*/
|
|
101
|
-
sizes?: Float32Array;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a
|
|
105
|
-
* reader panning across a laid-out corpus, and it is what a corpus can answer by address.
|
|
106
|
-
*
|
|
107
|
-
* **It is the only question this contract asks, and that is a narrowing rather than the natural
|
|
108
|
-
* shape.** A network has no spatial "near"; it has topological near, and a rectangle cannot express
|
|
109
|
-
* "two hops from this node" no matter how it is positioned — so a contract that only speaks
|
|
110
|
-
* rectangles imposes the map metaphor on a network. `ExploringSource` said the second question here
|
|
111
|
-
* and is deleted with the source that answered it; `index.test.ts` carries the tombstone and the
|
|
112
|
-
* seam it named. What it comes back as is fossil's `expand`, addressed rather than joined — not a
|
|
113
|
-
* second variant of this call.
|
|
114
|
-
*/
|
|
115
|
-
export interface SliceRequest {
|
|
116
|
-
/** The rectangle. */
|
|
117
|
-
view: Viewport;
|
|
118
|
-
/**
|
|
119
|
-
* Which column colours a point — Plot's channel name, and Plot's meaning.
|
|
120
|
-
*
|
|
121
|
-
* **On the request rather than on the source, and that is the whole shape.** A source says *where
|
|
122
|
-
* the bytes are*; a request says *what I want to draw*, and which column colours is the second.
|
|
123
|
-
* Baked into a source at construction — which is where it used to live — changing what a graph is
|
|
124
|
-
* coloured by meant building a new source, and the two are not the same question.
|
|
125
|
-
*
|
|
126
|
-
* It is the reference's own arrangement: in Plot the **mark** carries the channels and the mark is
|
|
127
|
-
* what produces the query, while the data source only says where rows come from.
|
|
128
|
-
*
|
|
129
|
-
* Defaults to `community`, which every corpus has because the layout pass writes it.
|
|
130
|
-
*/
|
|
131
|
-
fill?: string;
|
|
132
|
-
/**
|
|
133
|
-
* Which column the size ramp is spent on — Plot's `r`.
|
|
134
|
-
*
|
|
135
|
-
* Omitted, every point is drawn at one radius, which is a legitimate picture: without a ramp a
|
|
136
|
-
* look's `form.size` range has only one end.
|
|
137
|
-
*/
|
|
138
|
-
r?: string;
|
|
139
|
-
/**
|
|
140
|
-
* Vertices that must come back whatever the query says.
|
|
141
|
-
*
|
|
142
|
-
* The set a reader has taken hold of — dragged, pinned, selected, focused. Their drawn positions
|
|
143
|
-
* are a view-local overlay on coordinates that never move, so the index cannot find them where
|
|
144
|
-
* they now appear. Carrying them explicitly is cheaper and more honest than making the index
|
|
145
|
-
* mutable: it is a handful of identities, and the alternative is a spatial structure that has to be
|
|
146
|
-
* rewritten every time somebody drags something.
|
|
147
|
-
*/
|
|
148
|
-
pinned?: VertexId[];
|
|
149
|
-
/**
|
|
150
|
-
* The most points the source may return.
|
|
151
|
-
*
|
|
152
|
-
* **Above it a source samples the window; it does not take the front of it.** Which is the second
|
|
153
|
-
* half of `n`'s honesty: `n` says how many matched, and this says the answer is a *sample* of
|
|
154
|
-
* those rather than whichever ones an `ORDER BY` happened to put first.
|
|
155
|
-
*
|
|
156
|
-
* Required, and always filled: a host's `limit` is optional all the way down to `useQueryLoop`,
|
|
157
|
-
* which resolves it before the question leaves. A source never has to know what this package
|
|
158
|
-
* would have chosen.
|
|
159
|
-
*/
|
|
160
|
-
limit: number;
|
|
161
|
-
/**
|
|
162
|
-
* The shortest edge worth a row, **in screen pixels** — multiply by `perPixel` for a length in the
|
|
163
|
-
* graph's own space.
|
|
164
|
-
*
|
|
165
|
-
* **What it buys is on `perPixel` below**: two of every three edges a five-million-node window
|
|
166
|
-
* draws are under one pixel long, and discarding everything under three sends a third of the rows
|
|
167
|
-
* for an identical picture.
|
|
168
|
-
*
|
|
169
|
-
* Required for the reason `limit` is, and it is the field this whole shape exists for. It used to
|
|
170
|
-
* live on an exported `BOUNDED_DEFAULTS` that a source read to find out what it was being asked —
|
|
171
|
-
* so a request arrived incomplete and every source finished it in its own words. There were two,
|
|
172
|
-
* one of them in another repository. Now the loop resolves it and a source reads it here.
|
|
173
|
-
*
|
|
174
|
-
* Meaningless without `perPixel`, and a source with no resolution discards nothing: a threshold in
|
|
175
|
-
* pixels with no pixels is not a threshold.
|
|
176
|
-
*/
|
|
177
|
-
minLinkPixels: number;
|
|
178
|
-
/**
|
|
179
|
-
* How much of the graph's own space one screen pixel covers — the resolution the answer is going
|
|
180
|
-
* to be looked at.
|
|
181
|
-
*
|
|
182
|
-
* **What it buys: an edge shorter than three pixels is not sent.** Two of every three edges a
|
|
183
|
-
* five-million-node window draws are under one pixel long — they are a dot on top of their own
|
|
184
|
-
* endpoints, which the point layer has already drawn. Discarding everything under 3 px sends
|
|
185
|
-
* 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical
|
|
186
|
-
* (`.planning/FAR-VIEW-AND-EDGES.md`, measured 2026-08-17).
|
|
187
|
-
*
|
|
188
|
-
* **A row discard, not a fade.** cosmos.gl's `linkVisibilityDistanceRange` already dims a short
|
|
189
|
-
* link, and dimming happens after the row has been joined, returned, uploaded and rasterised. This
|
|
190
|
-
* is the same picture without the work.
|
|
191
|
-
*
|
|
192
|
-
* The rectangle alone cannot say it: the same rectangle over a 400-pixel canvas and a 4,000-pixel
|
|
193
|
-
* one are different questions, and only the caller knows which. Omitted — a source is asked for
|
|
194
|
-
* everything, or by something with no canvas — nothing is discarded, because a threshold in pixels
|
|
195
|
-
* with no pixels is not a threshold.
|
|
196
|
-
*/
|
|
197
|
-
perPixel?: number;
|
|
198
|
-
/**
|
|
199
|
-
* Stop: the caller does not want this answer any more.
|
|
200
|
-
*
|
|
201
|
-
* **A cancelled question rejects with `signal.reason`, and that is the whole contract.** The
|
|
202
|
-
* camera moves faster than a database answers, so a source that can only hold one question at a
|
|
203
|
-
* time is routinely asked a second before the first has landed. The first caller is still holding
|
|
204
|
-
* a promise; dropping it leaves that caller's `finally` unrun, which in `useQueryLoop` reads as a
|
|
205
|
-
* query permanently in flight for the rest of the session. So the stale promise is **settled**,
|
|
206
|
-
* and this says with what.
|
|
207
|
-
*
|
|
208
|
-
* `AbortController.abort()` puts a `DOMException` named `AbortError` in `reason`, and `throw
|
|
209
|
-
* signal.reason` is the whole implementation. A source that cancels for its own reasons — one
|
|
210
|
-
* standing question re-aimed by a newer caller, a connection let go — throws an `AbortError` it
|
|
211
|
-
* builds itself, which is what `abortError()` below is for.
|
|
212
|
-
*
|
|
213
|
-
* **This replaced a sentinel of ours, and the argument for the sentinel was real.** `SUPERSEDED`
|
|
214
|
-
* was an exported `Symbol` with `isSuperseded` beside it, on the reasoning that *you moved on* and
|
|
215
|
-
* *the database said no* are the two things a query loop must tell apart, and a string comparison
|
|
216
|
-
* against a thrown value goes stale with nothing failing. All of that is true and none of it is an
|
|
217
|
-
* argument for a *private* sentinel: `AbortError` is the name the platform already gives that
|
|
218
|
-
* distinction, `fetch` rejects with it, and every third-party async primitive a source is written
|
|
219
|
-
* over — `fetch`, `AbortSignal.timeout`, a WHATWG stream — produces one without being told to.
|
|
220
|
-
* Ours meant a source had to import a symbol from us to be cancellable at all, and a source that
|
|
221
|
-
* simply passed the signal to `fetch` did the standard thing and was reported to the host as a
|
|
222
|
-
* failure. The comparison is against `error.name`, which is the platform's contract rather than a
|
|
223
|
-
* message.
|
|
224
|
-
*/
|
|
225
|
-
signal?: AbortSignal;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Anything that can answer "what is in this rectangle, at this zoom, in at most this many marks".
|
|
229
|
-
*
|
|
230
|
-
* One method, one question. A source that also wants search, aggregation or paths is describing a
|
|
231
|
-
* query surface rather than a render path, and there is already one of those — fossil's verbs. The
|
|
232
|
-
* line: this answers *what should I draw*, and nothing about *what does it mean*.
|
|
233
|
-
*/
|
|
234
|
-
export interface BoundedSource {
|
|
235
|
-
slice(request: SliceRequest): Promise<Slice>;
|
|
236
|
-
/**
|
|
237
|
-
* How many vertices there are in total, if the source knows cheaply.
|
|
238
|
-
*
|
|
239
|
-
* The reason this exists is the one real cost of bounding: **a graph that fits pays for nothing.**
|
|
240
|
-
* Unbounded loads once and then pans on the GPU for free; bounded issues a query per camera move.
|
|
241
|
-
* At 200,000 nodes that trade is overwhelmingly worth it — 1,017 ms of first paint against roughly
|
|
242
|
-
* 130 ms — but at 2,000 it is pure loss, tens of milliseconds of latency per pan buying a ceiling
|
|
243
|
-
* nobody was near.
|
|
244
|
-
*
|
|
245
|
-
* So a consumer asks first. Under the limit, take one slice covering everything and never ask
|
|
246
|
-
* again: same code path, and panning is free exactly when it can be.
|
|
247
|
-
*/
|
|
248
|
-
total?(): Promise<number>;
|
|
249
|
-
/**
|
|
250
|
-
* The rectangle the corpus occupies, when the source can say cheaply.
|
|
251
|
-
*
|
|
252
|
-
* **Framing the opening view is not the host's job, and treating it as one is measured.** The
|
|
253
|
-
* archive occupies `x ∈ [1843, 2253]` of a 4,096-wide space — 1% of its area — and a camera that
|
|
254
|
-
* opens on the space rather than on the data draws 1,543 points into a tenth of the viewport, at
|
|
255
|
-
* two to nine pixels each, under a fog of links. Every point is uploaded and none is legible, which
|
|
256
|
-
* a reader reports as *the nodes are not rendering*.
|
|
257
|
-
*
|
|
258
|
-
* It matters more for a bounded source than for a whole one: the first question a sliced graph
|
|
259
|
-
* asks is *what is the camera over*, so a camera pointing at empty space is a first paint of
|
|
260
|
-
* nothing. Framing before asking is the difference between one query and none.
|
|
261
|
-
*
|
|
262
|
-
* Optional, because a source over an unlaid-out relation has no answer — and cheap where it
|
|
263
|
-
* exists: a corpus reads it off tile footers it was going to read anyway, and a relation with
|
|
264
|
-
* `x`/`y` gets it from four aggregates.
|
|
265
|
-
*/
|
|
266
|
-
extent?(): Promise<Viewport>;
|
|
267
|
-
/**
|
|
268
|
-
* Say when the answer changes for a reason the camera cannot see, and hold the source's resources
|
|
269
|
-
* for as long as anybody is listening.
|
|
270
|
-
*
|
|
271
|
-
* **A whole slice rather than a nudge to ask again, and that is what makes it worth having.** The
|
|
272
|
-
* reason a bounded answer changes on its own is that the page filtered something — somebody
|
|
273
|
-
* brushed a histogram, a panel picked a value — and a source that lives inside a crossfilter is
|
|
274
|
-
* *told* by the coordinator, which has already re-run the reads with the new predicate by the time
|
|
275
|
-
* this fires. Asking again would issue the same two queries a second time to learn what is in hand.
|
|
276
|
-
*
|
|
277
|
-
* The returned function is also the release: it is where a source lets go of whatever it holds —
|
|
278
|
-
* a client registration, a connection, a cache — so a loop that calls this is a loop that cannot
|
|
279
|
-
* leak one. Optional, because a source over arrays holds nothing and changes for nothing.
|
|
280
|
-
*/
|
|
281
|
-
watch?(answered: (slice: Slice) => void): () => void;
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* A cancellation this source is raising itself, in the platform's own shape.
|
|
285
|
-
*
|
|
286
|
-
* For the case a `signal` cannot express: a source holding **one** standing question, re-aimed by a
|
|
287
|
-
* newer caller. There is no signal for the caller that lost — its request was never aborted, it was
|
|
288
|
-
* simply overtaken — so the source builds the rejection, and it builds the same kind the platform
|
|
289
|
-
* would have. `message` says what overtook it; `name` is what anybody tests.
|
|
290
|
-
*
|
|
291
|
-
* Not on the barrel. A source outside this package cancels by passing the `signal` it was handed to
|
|
292
|
-
* whatever it is waiting on, or by `throw signal.reason` — both of which produce an `AbortError`
|
|
293
|
-
* with nothing imported from us. This exists for the one case that has no signal to reach for, and
|
|
294
|
-
* `new DOMException(message, "AbortError")` is the whole of it if a third source ever needs it too.
|
|
295
|
-
*/
|
|
296
|
-
export declare function abortError(message: string): DOMException;
|
|
297
|
-
/**
|
|
298
|
-
* Whether a rejection means *you moved on* rather than *the answer failed*.
|
|
299
|
-
*
|
|
300
|
-
* `error.name` and not `instanceof`: the same `AbortError` reaches here as a `DOMException` from
|
|
301
|
-
* `AbortController`, as one of ours from `abortError`, and — for a source written over `fetch` in
|
|
302
|
-
* another realm, an iframe or a worker — as an object no `instanceof` in this realm matches. The
|
|
303
|
-
* name is what WHATWG specifies and what every producer agrees on.
|
|
304
|
-
*
|
|
305
|
-
* Not on the barrel either, for the same reason as `abortError`: a caller of this package awaits
|
|
306
|
-
* `slice()` behind an `AbortController` it owns, so `controller.signal.aborted` already answers the
|
|
307
|
-
* question for it. This is the branch `useQueryLoop` needs for the *other* half — a source that
|
|
308
|
-
* cancelled a question the loop had not aborted.
|
|
309
|
-
*/
|
|
310
|
-
export declare function isAbort(error: unknown): boolean;
|
|
311
|
-
/**
|
|
312
|
-
* Whether this graph should be sliced at all.
|
|
313
|
-
*
|
|
314
|
-
* `undefined` when the source cannot say cheaply — in which case slice, because an unknown corpus is
|
|
315
|
-
* more likely to be the large kind than not.
|
|
316
|
-
*/
|
|
317
|
-
export declare function shouldSlice(total: number | undefined, limit: number): boolean;
|
|
318
|
-
/**
|
|
319
|
-
* What a question means when the host says nothing — **resolved before a source ever sees it.**
|
|
320
|
-
*
|
|
321
|
-
* These were `BOUNDED_DEFAULTS`, an exported table, and a source read it to find out what it was
|
|
322
|
-
* being asked. That is the defect, stated plainly: a request that arrives unresolved is an
|
|
323
|
-
* *incomplete* request, and every source outside this package had to know a constant of ours to
|
|
324
|
-
* finish it. Two of them did — `duck-source.ts` here, and fossil's tile reader — and each finished
|
|
325
|
-
* it in its own words, which is two chances to disagree about one number.
|
|
326
|
-
*
|
|
327
|
-
* `useQueryLoop` fills both in on every call now, so `limit` and `minLinkPixels` are **required**
|
|
328
|
-
* fields of `SliceRequest`: a source reads `request.limit` and is done. The numbers stay module
|
|
329
|
-
* scoped and off the barrel, because nobody outside needs to look them up any more.
|
|
330
|
-
*
|
|
331
|
-
* The camera→rectangle conversion that used to live beside them is gone for the sibling reason:
|
|
332
|
-
* cosmos.gl owns the screen↔space transform and answers it through `screenToSpacePosition`, so
|
|
333
|
-
* deriving the rectangle from the camera and the space size was a second implementation of the
|
|
334
|
-
* renderer's own maths, free to drift from it. `useQueryLoop` asks the renderer instead.
|
|
335
|
-
*/
|
|
336
|
-
/**
|
|
337
|
-
* Twenty thousand marks.
|
|
338
|
-
*
|
|
339
|
-
* Above about 50,000 points a live layout stops being comfortable and the edge layer is already
|
|
340
|
-
* fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is
|
|
341
|
-
* the legibility ceiling, which arrives first and is the one a reader actually meets.
|
|
342
|
-
*/
|
|
343
|
-
export declare const DEFAULT_LIMIT = 20000;
|
|
344
|
-
/**
|
|
345
|
-
* The shortest edge worth a row — three screen pixels.
|
|
346
|
-
*
|
|
347
|
-
* Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px
|
|
348
|
-
* at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge
|
|
349
|
-
* that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px
|
|
350
|
-
* sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is
|
|
351
|
-
* in `.planning/FAR-VIEW-AND-EDGES.md`.
|
|
352
|
-
*
|
|
353
|
-
* Three rather than two because both were measured against the same five windows of
|
|
354
|
-
* `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of
|
|
355
|
-
* image.
|
|
356
|
-
*
|
|
357
|
-
* **It is on `SliceRequest` now, and the argument against that has been overtaken.** It used to say:
|
|
358
|
-
* one call site, and a knob with one call site is a knob nobody has an opinion about. There were
|
|
359
|
-
* two call sites by then and one of them was in another repository, both reading the constant off
|
|
360
|
-
* the barrel to reconstruct the same product. Whether it is a *knob* is still open — no caller
|
|
361
|
-
* overrides it — but it is a **fact about the question**, and a question carries its own facts.
|
|
362
|
-
*/
|
|
363
|
-
export declare const DEFAULT_MIN_LINK_PIXELS = 3;
|
|
364
|
-
//# sourceMappingURL=bounded.d.ts.map
|
package/dist/bounded.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bounded.d.ts","sourceRoot":"","sources":["../src/bounded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,8FAA8F;IAC9F,SAAS,EAAE,YAAY,CAAC;IACxB,mDAAmD;IACnD,KAAK,EAAE,YAAY,CAAC;IACpB,8CAA8C;IAC9C,UAAU,EAAE,WAAW,CAAC;IACxB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,IAAI,EAAE,QAAQ,CAAC;IACf;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;OAeG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAExD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC"}
|
package/dist/bounded.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
function r(n) {
|
|
2
|
-
return new DOMException(n, "AbortError");
|
|
3
|
-
}
|
|
4
|
-
function t(n) {
|
|
5
|
-
return typeof n == "object" && n !== null && n.name === "AbortError";
|
|
6
|
-
}
|
|
7
|
-
function e(n, o) {
|
|
8
|
-
return n === void 0 || n > o;
|
|
9
|
-
}
|
|
10
|
-
const c = 2e4, u = 3;
|
|
11
|
-
export {
|
|
12
|
-
c as DEFAULT_LIMIT,
|
|
13
|
-
u as DEFAULT_MIN_LINK_PIXELS,
|
|
14
|
-
r as abortError,
|
|
15
|
-
t as isAbort,
|
|
16
|
-
e as shouldSlice
|
|
17
|
-
};
|
|
18
|
-
//# sourceMappingURL=bounded.js.map
|
package/dist/bounded.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bounded.js","sources":["../src/bounded.ts"],"sourcesContent":["/**\n * A graph you never hold all of.\n *\n * `load()` is the other kind: it reads a relation whole, turns it into typed arrays, and hands the\n * lot to the renderer. Measured, that costs 389 ms at 200,000 nodes and stops being viable somewhere\n * short of a million — the working set is N, so the ceiling is whatever N the machine can hold.\n *\n * This is the shape that has no such ceiling: something asks a bounded question — a rectangle, or a\n * neighbourhood a few hops wide — and the source answers with **at most `limit` points**. The\n * answer's size follows the question rather than the corpus. Moving re-asks. A window holding more\n * than `limit` is *sampled* rather than truncated, so a view of everything is still a few thousand\n * marks and they are still spread over everything — see `sampled` in `duck-source.ts`.\n *\n * **Positions are authority, not suggestion.** The corpus is written once and read many — OLAP, which\n * is what GraphAr is for — so the coordinates the batch emits are the index every spatial question\n * is asked against. That settles a tension an earlier draft of this file waved at rather than\n * resolved: re-laying-out a slice live would move points out from under the very coordinates the\n * next query is expressed in, and the camera would drift away from the index within one frame of\n * the first force. **Do not re-lay-out a slice.** If a layout is wrong, it is wrong upstream, and it\n * is fixed by recompiling — the graph is a compiler's output and so is its geometry.\n *\n * **Dragging is the exception, and it is a local overlay.** A reader can move a node; that changes\n * where it is *drawn*, never where it is *indexed*. The consequence is small and real: drag a node\n * far away, pan to where you dropped it, and the spatial query does not know it is there. Which is\n * why `pinned` exists below — the few points a reader has taken hold of ride along with every\n * slice, regardless of the rectangle.\n *\n * **Deliberately not a format.** A source is anything that can answer that question: Parquet\n * fetched by address, a plain relation with `x`/`y` columns and a spatial predicate, or an\n * in-memory index. This package renders; it does not learn a storage layout. The\n * wiring between a particular source and this contract belongs at the call site.\n *\n * **Dense indices, not database ids.** `links` refers to positions in `positions`, so a consumer\n * never pays for an id→index map — the 148 ms that mapping costs at 200,000 nodes is not optimised\n * here, it is designed away. Sources that already number their vertices densely (GraphAr's\n * `dense_id` does) hand this over for free.\n */\n\nimport type { VertexId } from \"./resident\";\n\n/**\n * What the camera is looking at, in the graph's own coordinate space.\n *\n * A rectangle and nothing else. It carried a `zoom` for one reader — the level-of-detail threshold\n * a source compared it against to decide whether to answer with super-nodes — and that branch is\n * gone, so the field went with it rather than staying as a number every caller has to invent and\n * nothing reads.\n */\nexport interface Viewport {\n xMin: number;\n yMin: number;\n xMax: number;\n yMax: number;\n}\n\n/**\n * One answer. Every array is parallel and indexed densely from zero.\n *\n * `n` is what *matched*, before `limit` cut it — the difference between the two is how a view says\n * \"there is more here than I am showing you\", which is the one honest thing a bounded renderer owes\n * its reader. When a window holds more than `limit`, what comes back is a **sample** of it rather\n * than its first `limit` rows; `n` reports the window either way.\n *\n * **A struct, and it used to be a tagged union.** `mode` picked between points and super-nodes and\n * only the second branch carried `weights`. Both are gone — see `/docs/design/graph` — and with one\n * branch left a discriminant is a\n * field with one legal value.\n */\nexport interface Slice {\n n: number;\n /**\n * How many of `positions` are **marks** — points a reader can see. Everything past it is an\n * *anchor*.\n *\n * An anchor is a real vertex at its real coordinates that the window did not return: it is in the\n * buffers so that an edge leaving the window has somewhere to end. It is never drawn — `buffers`\n * gives it radius zero and alpha zero, and `residentOf` stops here, so nothing hovers, selects or\n * frames one.\n *\n * **Why the far end is the vertex rather than a point on the border.** A stub clipped to the\n * viewport carries the right direction and *lies about the distance*, and a reader cannot tell a\n * stub that ends 1.1 window-widths out from one that ends 47 — measured over all the far ends a\n * window loses, 20–22% are past four semi-widths and the worst is 47.1\n * (`.planning/FAR-VIEW-AND-EDGES.md`). Drawing the vertex where it is cannot lie, and it is also\n * the cheaper of the two: a clipped stub is one point *per edge*, an anchor is one point per far\n * *vertex*.\n *\n * Equal to `positions.length / 2` for a source that answers with marks only, which is why it is\n * required rather than optional — a caller that reads it always gets the count it meant.\n */\n marks: number;\n /**\n * Who each returned point *is*, parallel to `positions` — the `(type_idx, dense_id)` pair packed\n * by `vertexId`.\n *\n * Present because a buffer index is **not stable across answers**: index 7 is a different vertex\n * after a pan. Anything that outlives one answer — a selection, a focused node, a pinned set — has\n * to be held as an identity and re-resolved through `residentOf` each time. Leaving this out was\n * how the first draft would have shipped a selection that silently pointed at the wrong nodes.\n *\n * `BigUint64Array` because the pair is 64 bits exactly — a `Uint32Array` cannot hold it at all,\n * and a `Float64Array` holds it only while the type index stays under 2²¹, which is a ceiling\n * nobody would find until they crossed it. Still a typed array, so it is still one allocation and\n * still transferable; only what it carries changed. A dense id on its own is not an identity: it\n * numbers within one vertex type, so a union of two types repeats every value.\n */\n vertices: BigUint64Array;\n /**\n * The subject IRI of each returned point, parallel to `vertices` — **opt-in, and absent by\n * default.**\n *\n * `vertices` says where a point *is*; this says which vertex it *is*. They are not the same thing\n * and the corpus is explicit about it: redoing a layout renumbers every vertex, so a `dense_id`\n * held outside the corpus names a different vertex after the next write. Anything that has to\n * survive a recompile — a bookmark, a link out, a row in somebody else's database — keys on the\n * IRI. A selection held as `VertexId` survives a pan and does not survive a rebuild.\n *\n * **Absent by default because it costs 1.87× the tile, measured on the corpus side.** Compressed\n * bytes per row at five million: `subject` 8.016 against `dense_id` 4.000, `x` 2.717, `y` 2.501.\n * The four drawing columns are 9.23 B/row and become 17.25 with it. So the drawing path carries\n * addresses, and a host asks for names when something has to be *named* rather than painted.\n *\n * A `string[]` rather than a typed array, because that is what an IRI is. It is the one thing in a\n * `Slice` that does not go to the GPU, which is exactly why it is optional: a host that never\n * names a vertex should not pay to move it.\n */\n subjects?: string[];\n /** `[x0, y0, x1, y1, …]`, one pair per returned point — `marks` of them, then the anchors. */\n positions: Float32Array;\n /** `[src, dst, …]` as indices into `positions`. */\n links: Float32Array;\n /** Per-point category ordinal, for colour. */\n categories: Uint16Array;\n /**\n * What the size ramp is spent on, per point — a degree, a count, whatever the corpus ranks by.\n *\n * Optional because a source may not have one, and a graph drawn at one radius is a legitimate\n * picture. But without it a look's `form.size` range has only one end, so a source that can afford\n * the column should send it: it is the difference between seeing a hub and counting one.\n */\n sizes?: Float32Array;\n}\n\n/**\n * A **region** is a map question: what is inside this rectangle. It suits an overview, a minimap, a\n * reader panning across a laid-out corpus, and it is what a corpus can answer by address.\n *\n * **It is the only question this contract asks, and that is a narrowing rather than the natural\n * shape.** A network has no spatial \"near\"; it has topological near, and a rectangle cannot express\n * \"two hops from this node\" no matter how it is positioned — so a contract that only speaks\n * rectangles imposes the map metaphor on a network. `ExploringSource` said the second question here\n * and is deleted with the source that answered it; `index.test.ts` carries the tombstone and the\n * seam it named. What it comes back as is fossil's `expand`, addressed rather than joined — not a\n * second variant of this call.\n */\nexport interface SliceRequest {\n /** The rectangle. */\n view: Viewport;\n /**\n * Which column colours a point — Plot's channel name, and Plot's meaning.\n *\n * **On the request rather than on the source, and that is the whole shape.** A source says *where\n * the bytes are*; a request says *what I want to draw*, and which column colours is the second.\n * Baked into a source at construction — which is where it used to live — changing what a graph is\n * coloured by meant building a new source, and the two are not the same question.\n *\n * It is the reference's own arrangement: in Plot the **mark** carries the channels and the mark is\n * what produces the query, while the data source only says where rows come from.\n *\n * Defaults to `community`, which every corpus has because the layout pass writes it.\n */\n fill?: string;\n /**\n * Which column the size ramp is spent on — Plot's `r`.\n *\n * Omitted, every point is drawn at one radius, which is a legitimate picture: without a ramp a\n * look's `form.size` range has only one end.\n */\n r?: string;\n /**\n * Vertices that must come back whatever the query says.\n *\n * The set a reader has taken hold of — dragged, pinned, selected, focused. Their drawn positions\n * are a view-local overlay on coordinates that never move, so the index cannot find them where\n * they now appear. Carrying them explicitly is cheaper and more honest than making the index\n * mutable: it is a handful of identities, and the alternative is a spatial structure that has to be\n * rewritten every time somebody drags something.\n */\n pinned?: VertexId[];\n /**\n * The most points the source may return.\n *\n * **Above it a source samples the window; it does not take the front of it.** Which is the second\n * half of `n`'s honesty: `n` says how many matched, and this says the answer is a *sample* of\n * those rather than whichever ones an `ORDER BY` happened to put first.\n *\n * Required, and always filled: a host's `limit` is optional all the way down to `useQueryLoop`,\n * which resolves it before the question leaves. A source never has to know what this package\n * would have chosen.\n */\n limit: number;\n /**\n * The shortest edge worth a row, **in screen pixels** — multiply by `perPixel` for a length in the\n * graph's own space.\n *\n * **What it buys is on `perPixel` below**: two of every three edges a five-million-node window\n * draws are under one pixel long, and discarding everything under three sends a third of the rows\n * for an identical picture.\n *\n * Required for the reason `limit` is, and it is the field this whole shape exists for. It used to\n * live on an exported `BOUNDED_DEFAULTS` that a source read to find out what it was being asked —\n * so a request arrived incomplete and every source finished it in its own words. There were two,\n * one of them in another repository. Now the loop resolves it and a source reads it here.\n *\n * Meaningless without `perPixel`, and a source with no resolution discards nothing: a threshold in\n * pixels with no pixels is not a threshold.\n */\n minLinkPixels: number;\n /**\n * How much of the graph's own space one screen pixel covers — the resolution the answer is going\n * to be looked at.\n *\n * **What it buys: an edge shorter than three pixels is not sent.** Two of every three edges a\n * five-million-node window draws are under one pixel long — they are a dot on top of their own\n * endpoints, which the point layer has already drawn. Discarding everything under 3 px sends\n * 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical\n * (`.planning/FAR-VIEW-AND-EDGES.md`, measured 2026-08-17).\n *\n * **A row discard, not a fade.** cosmos.gl's `linkVisibilityDistanceRange` already dims a short\n * link, and dimming happens after the row has been joined, returned, uploaded and rasterised. This\n * is the same picture without the work.\n *\n * The rectangle alone cannot say it: the same rectangle over a 400-pixel canvas and a 4,000-pixel\n * one are different questions, and only the caller knows which. Omitted — a source is asked for\n * everything, or by something with no canvas — nothing is discarded, because a threshold in pixels\n * with no pixels is not a threshold.\n */\n perPixel?: number;\n /**\n * Stop: the caller does not want this answer any more.\n *\n * **A cancelled question rejects with `signal.reason`, and that is the whole contract.** The\n * camera moves faster than a database answers, so a source that can only hold one question at a\n * time is routinely asked a second before the first has landed. The first caller is still holding\n * a promise; dropping it leaves that caller's `finally` unrun, which in `useQueryLoop` reads as a\n * query permanently in flight for the rest of the session. So the stale promise is **settled**,\n * and this says with what.\n *\n * `AbortController.abort()` puts a `DOMException` named `AbortError` in `reason`, and `throw\n * signal.reason` is the whole implementation. A source that cancels for its own reasons — one\n * standing question re-aimed by a newer caller, a connection let go — throws an `AbortError` it\n * builds itself, which is what `abortError()` below is for.\n *\n * **This replaced a sentinel of ours, and the argument for the sentinel was real.** `SUPERSEDED`\n * was an exported `Symbol` with `isSuperseded` beside it, on the reasoning that *you moved on* and\n * *the database said no* are the two things a query loop must tell apart, and a string comparison\n * against a thrown value goes stale with nothing failing. All of that is true and none of it is an\n * argument for a *private* sentinel: `AbortError` is the name the platform already gives that\n * distinction, `fetch` rejects with it, and every third-party async primitive a source is written\n * over — `fetch`, `AbortSignal.timeout`, a WHATWG stream — produces one without being told to.\n * Ours meant a source had to import a symbol from us to be cancellable at all, and a source that\n * simply passed the signal to `fetch` did the standard thing and was reported to the host as a\n * failure. The comparison is against `error.name`, which is the platform's contract rather than a\n * message.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Anything that can answer \"what is in this rectangle, at this zoom, in at most this many marks\".\n *\n * One method, one question. A source that also wants search, aggregation or paths is describing a\n * query surface rather than a render path, and there is already one of those — fossil's verbs. The\n * line: this answers *what should I draw*, and nothing about *what does it mean*.\n */\nexport interface BoundedSource {\n slice(request: SliceRequest): Promise<Slice>;\n /**\n * How many vertices there are in total, if the source knows cheaply.\n *\n * The reason this exists is the one real cost of bounding: **a graph that fits pays for nothing.**\n * Unbounded loads once and then pans on the GPU for free; bounded issues a query per camera move.\n * At 200,000 nodes that trade is overwhelmingly worth it — 1,017 ms of first paint against roughly\n * 130 ms — but at 2,000 it is pure loss, tens of milliseconds of latency per pan buying a ceiling\n * nobody was near.\n *\n * So a consumer asks first. Under the limit, take one slice covering everything and never ask\n * again: same code path, and panning is free exactly when it can be.\n */\n total?(): Promise<number>;\n /**\n * The rectangle the corpus occupies, when the source can say cheaply.\n *\n * **Framing the opening view is not the host's job, and treating it as one is measured.** The\n * archive occupies `x ∈ [1843, 2253]` of a 4,096-wide space — 1% of its area — and a camera that\n * opens on the space rather than on the data draws 1,543 points into a tenth of the viewport, at\n * two to nine pixels each, under a fog of links. Every point is uploaded and none is legible, which\n * a reader reports as *the nodes are not rendering*.\n *\n * It matters more for a bounded source than for a whole one: the first question a sliced graph\n * asks is *what is the camera over*, so a camera pointing at empty space is a first paint of\n * nothing. Framing before asking is the difference between one query and none.\n *\n * Optional, because a source over an unlaid-out relation has no answer — and cheap where it\n * exists: a corpus reads it off tile footers it was going to read anyway, and a relation with\n * `x`/`y` gets it from four aggregates.\n */\n extent?(): Promise<Viewport>;\n /**\n * Say when the answer changes for a reason the camera cannot see, and hold the source's resources\n * for as long as anybody is listening.\n *\n * **A whole slice rather than a nudge to ask again, and that is what makes it worth having.** The\n * reason a bounded answer changes on its own is that the page filtered something — somebody\n * brushed a histogram, a panel picked a value — and a source that lives inside a crossfilter is\n * *told* by the coordinator, which has already re-run the reads with the new predicate by the time\n * this fires. Asking again would issue the same two queries a second time to learn what is in hand.\n *\n * The returned function is also the release: it is where a source lets go of whatever it holds —\n * a client registration, a connection, a cache — so a loop that calls this is a loop that cannot\n * leak one. Optional, because a source over arrays holds nothing and changes for nothing.\n */\n watch?(answered: (slice: Slice) => void): () => void;\n}\n\n/**\n * A cancellation this source is raising itself, in the platform's own shape.\n *\n * For the case a `signal` cannot express: a source holding **one** standing question, re-aimed by a\n * newer caller. There is no signal for the caller that lost — its request was never aborted, it was\n * simply overtaken — so the source builds the rejection, and it builds the same kind the platform\n * would have. `message` says what overtook it; `name` is what anybody tests.\n *\n * Not on the barrel. A source outside this package cancels by passing the `signal` it was handed to\n * whatever it is waiting on, or by `throw signal.reason` — both of which produce an `AbortError`\n * with nothing imported from us. This exists for the one case that has no signal to reach for, and\n * `new DOMException(message, \"AbortError\")` is the whole of it if a third source ever needs it too.\n */\nexport function abortError(message: string): DOMException {\n return new DOMException(message, \"AbortError\");\n}\n\n/**\n * Whether a rejection means *you moved on* rather than *the answer failed*.\n *\n * `error.name` and not `instanceof`: the same `AbortError` reaches here as a `DOMException` from\n * `AbortController`, as one of ours from `abortError`, and — for a source written over `fetch` in\n * another realm, an iframe or a worker — as an object no `instanceof` in this realm matches. The\n * name is what WHATWG specifies and what every producer agrees on.\n *\n * Not on the barrel either, for the same reason as `abortError`: a caller of this package awaits\n * `slice()` behind an `AbortController` it owns, so `controller.signal.aborted` already answers the\n * question for it. This is the branch `useQueryLoop` needs for the *other* half — a source that\n * cancelled a question the loop had not aborted.\n */\nexport function isAbort(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as { name?: unknown }).name === \"AbortError\";\n}\n\n/**\n * Whether this graph should be sliced at all.\n *\n * `undefined` when the source cannot say cheaply — in which case slice, because an unknown corpus is\n * more likely to be the large kind than not.\n */\nexport function shouldSlice(total: number | undefined, limit: number): boolean {\n return total === undefined || total > limit;\n}\n\n/**\n * What a question means when the host says nothing — **resolved before a source ever sees it.**\n *\n * These were `BOUNDED_DEFAULTS`, an exported table, and a source read it to find out what it was\n * being asked. That is the defect, stated plainly: a request that arrives unresolved is an\n * *incomplete* request, and every source outside this package had to know a constant of ours to\n * finish it. Two of them did — `duck-source.ts` here, and fossil's tile reader — and each finished\n * it in its own words, which is two chances to disagree about one number.\n *\n * `useQueryLoop` fills both in on every call now, so `limit` and `minLinkPixels` are **required**\n * fields of `SliceRequest`: a source reads `request.limit` and is done. The numbers stay module\n * scoped and off the barrel, because nobody outside needs to look them up any more.\n *\n * The camera→rectangle conversion that used to live beside them is gone for the sibling reason:\n * cosmos.gl owns the screen↔space transform and answers it through `screenToSpacePosition`, so\n * deriving the rectangle from the camera and the space size was a second implementation of the\n * renderer's own maths, free to drift from it. `useQueryLoop` asks the renderer instead.\n */\n\n/**\n * Twenty thousand marks.\n *\n * Above about 50,000 points a live layout stops being comfortable and the edge layer is already\n * fog well before that, so a limit far below the renderer's ceiling is not a compromise — it is\n * the legibility ceiling, which arrives first and is the one a reader actually meets.\n */\nexport const DEFAULT_LIMIT = 20_000;\n\n/**\n * The shortest edge worth a row — three screen pixels.\n *\n * Measured over the drawn edges of five windows per corpus, 2026-08-17: the median edge is 1.60 px\n * at 200k, 1.47 at a million and **0.52 at five million**, where 64.6% are under one pixel. An edge\n * that short is a dot on top of two dots the point layer has already drawn. Discarding under 3 px\n * sends 27.5–35.3% of the rows and leaves 99.9–100% of the inked pixels identical — the working is\n * in `.planning/FAR-VIEW-AND-EDGES.md`.\n *\n * Three rather than two because both were measured against the same five windows of\n * `docs/public/bench/1000000`: 2 px sends 5.6–25.8% more rows, mean 19%, for the same 0.1% of\n * image.\n *\n * **It is on `SliceRequest` now, and the argument against that has been overtaken.** It used to say:\n * one call site, and a knob with one call site is a knob nobody has an opinion about. There were\n * two call sites by then and one of them was in another repository, both reading the constant off\n * the barrel to reconstruct the same product. Whether it is a *knob* is still open — no caller\n * overrides it — but it is a **fact about the question**, and a question carries its own facts.\n */\nexport const DEFAULT_MIN_LINK_PIXELS = 3;\n"],"names":["abortError","message","isAbort","error","shouldSlice","total","limit","DEFAULT_LIMIT","DEFAULT_MIN_LINK_PIXELS"],"mappings":"AAkVO,SAASA,EAAWC,GAA+B;AACxD,SAAO,IAAI,aAAaA,GAAS,YAAY;AAC/C;AAeO,SAASC,EAAQC,GAAyB;AAC/C,SAAO,OAAOA,KAAU,YAAYA,MAAU,QAASA,EAA6B,SAAS;AAC/F;AAQO,SAASC,EAAYC,GAA2BC,GAAwB;AAC7E,SAAOD,MAAU,UAAaA,IAAQC;AACxC;AA4BO,MAAMC,IAAgB,KAqBhBC,IAA0B;"}
|
package/dist/cluster-ring.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where each cluster sits, as a ring around the middle of the simulation box.
|
|
3
|
-
*
|
|
4
|
-
* `setPointClusters` on its own does not separate anything. cosmos.gl's cluster force reads a
|
|
5
|
-
* cluster's position from `clusterPositionsTexture` and falls back to the group's **centre of
|
|
6
|
-
* mass** when that position is negative — so without this the force pulls every node toward its own
|
|
7
|
-
* group's centroid, which is cohesion toward a target that moves with the thing it is pulling.
|
|
8
|
-
* Groups that start overlapped have overlapping centroids, and nothing in a purely attractive force
|
|
9
|
-
* asks them to move apart. Given explicit positions the same force becomes a positional constraint,
|
|
10
|
-
* and the separation is a fixed geometry the simulation converges onto rather than an emergent
|
|
11
|
-
* property it might not find.
|
|
12
|
-
*
|
|
13
|
-
* That separation is the one thing the CPU seed in `lib/force-layout` cannot supply here. A `hall`
|
|
14
|
-
* is an attribute, not a community: members, tags, beasts and regions are shared across the halls by
|
|
15
|
-
* construction — a party borrows, a beast ranges, a tag is the board's and not one hall's — so the
|
|
16
|
-
* link structure genuinely crosses them and no link-driven layout can pull them apart. Measured on
|
|
17
|
-
* the archive corpus (1,543 nodes, 4,280 edges, five halls), the seed's own angular hint decays from
|
|
18
|
-
* a 2.53 between/within centroid ratio at tick 0 to 2.73 at tick 5 and **0.71 by tick 50**, where it
|
|
19
|
-
* stays: 0.67 at the shipped 200 ticks and 0.68 at 400. On the layout as shipped, 8-nearest-
|
|
20
|
-
* neighbour purity is 27.0% against a 20.1% chance floor for these five group sizes. The seed buys
|
|
21
|
-
* short edges — mean edge length 0.111 of the layout's width, against 0.516 for an unseeded start —
|
|
22
|
-
* and only this buys communities.
|
|
23
|
-
*/
|
|
24
|
-
export declare function clusterRing(clusters: readonly (number | undefined)[], space: number): number[];
|
|
25
|
-
//# sourceMappingURL=cluster-ring.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cluster-ring.d.ts","sourceRoot":"","sources":["../src/cluster-ring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9F"}
|
package/dist/cluster-ring.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
function f(r, n) {
|
|
2
|
-
let o = 0;
|
|
3
|
-
for (const t of r)
|
|
4
|
-
t !== void 0 && t + 1 > o && (o = t + 1);
|
|
5
|
-
const s = [];
|
|
6
|
-
for (let t = 0; t < o; t++) {
|
|
7
|
-
const i = t / o * 2 * Math.PI, c = n * l;
|
|
8
|
-
s.push(n / 2 + Math.cos(i) * c, n / 2 + Math.sin(i) * c);
|
|
9
|
-
}
|
|
10
|
-
return s;
|
|
11
|
-
}
|
|
12
|
-
const l = 0.12;
|
|
13
|
-
export {
|
|
14
|
-
f as clusterRing
|
|
15
|
-
};
|
|
16
|
-
//# sourceMappingURL=cluster-ring.js.map
|
package/dist/cluster-ring.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cluster-ring.js","sources":["../src/cluster-ring.ts"],"sourcesContent":["/**\n * Where each cluster sits, as a ring around the middle of the simulation box.\n *\n * `setPointClusters` on its own does not separate anything. cosmos.gl's cluster force reads a\n * cluster's position from `clusterPositionsTexture` and falls back to the group's **centre of\n * mass** when that position is negative — so without this the force pulls every node toward its own\n * group's centroid, which is cohesion toward a target that moves with the thing it is pulling.\n * Groups that start overlapped have overlapping centroids, and nothing in a purely attractive force\n * asks them to move apart. Given explicit positions the same force becomes a positional constraint,\n * and the separation is a fixed geometry the simulation converges onto rather than an emergent\n * property it might not find.\n *\n * That separation is the one thing the CPU seed in `lib/force-layout` cannot supply here. A `hall`\n * is an attribute, not a community: members, tags, beasts and regions are shared across the halls by\n * construction — a party borrows, a beast ranges, a tag is the board's and not one hall's — so the\n * link structure genuinely crosses them and no link-driven layout can pull them apart. Measured on\n * the archive corpus (1,543 nodes, 4,280 edges, five halls), the seed's own angular hint decays from\n * a 2.53 between/within centroid ratio at tick 0 to 2.73 at tick 5 and **0.71 by tick 50**, where it\n * stays: 0.67 at the shipped 200 ticks and 0.68 at 400. On the layout as shipped, 8-nearest-\n * neighbour purity is 27.0% against a 20.1% chance floor for these five group sizes. The seed buys\n * short edges — mean edge length 0.111 of the layout's width, against 0.516 for an unseeded start —\n * and only this buys communities.\n */\nexport function clusterRing(clusters: readonly (number | undefined)[], space: number): number[] {\n let count = 0;\n for (const slot of clusters) {\n if (slot !== undefined && slot + 1 > count) count = slot + 1;\n }\n const positions: number[] = [];\n for (let slot = 0; slot < count; slot++) {\n const angle = (slot / count) * 2 * Math.PI;\n const ring = space * RING;\n positions.push(space / 2 + Math.cos(angle) * ring, space / 2 + Math.sin(angle) * ring);\n }\n return positions;\n}\n\n/**\n * How far out the ring sits, as a **fraction of the box the renderer is working in** — which is why\n * `clusterRing` is handed that box rather than reading a constant. There is no `SPACE` any more: the\n * coordinate space belongs to whatever wrote the positions, and the one place that can still answer\n * it is the live renderer.\n *\n * Matched to where the seeded layout already lives rather than chosen for the picture, and\n * re-measured for the archive: the shipped seed puts nodes at a median radius of 351 from the centre\n * of the 4,096 box, quartiles 211 and 655. At 492 — 0.12 of that box — the ring lands between the\n * median and the upper quartile, so the cluster force redistributes points around a circle the\n * layout already occupies instead of inflating it. It was `0.16` — 655 — for the old corpus, whose\n * median sat at 587; this one packs tighter, because 938 of its 1,543 vertices are leaves hanging\n * off a contract. **The figures are against a 4,096 box**; the fraction is what survives a different\n * one, and it is a fraction for that reason.\n *\n * Slot order is the order the group values were first seen in the relation, which on a ring means\n * neighbouring slots are neighbouring arcs. Nothing reads meaning into that adjacency, and nothing\n * should — the order is the relation's, not the domain's.\n */\nconst RING = 0.12;\n"],"names":["clusterRing","clusters","space","count","slot","positions","angle","ring","RING"],"mappings":"AAuBO,SAASA,EAAYC,GAA2CC,GAAyB;AAC9F,MAAIC,IAAQ;AACZ,aAAWC,KAAQH;AACjB,IAAIG,MAAS,UAAaA,IAAO,IAAID,UAAeC,IAAO;AAE7D,QAAMC,IAAsB,CAAA;AAC5B,WAASD,IAAO,GAAGA,IAAOD,GAAOC,KAAQ;AACvC,UAAME,IAASF,IAAOD,IAAS,IAAI,KAAK,IAClCI,IAAOL,IAAQM;AACrB,IAAAH,EAAU,KAAKH,IAAQ,IAAI,KAAK,IAAII,CAAK,IAAIC,GAAML,IAAQ,IAAI,KAAK,IAAII,CAAK,IAAIC,CAAI;AAAA,EACvF;AACA,SAAOF;AACT;AAqBA,MAAMG,IAAO;"}
|
package/dist/css-color.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"css-color.d.ts","sourceRoot":"","sources":["../src/css-color.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AA0BpD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,IAAe,GAAG,IAAI,CAK1F;AAOD,gFAAgF;AAChF,wBAAgB,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAEhD"}
|
package/dist/css-color.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"css-color.js","sources":["../src/css-color.ts"],"sourcesContent":["// Theme colours as numbers a WebGL renderer can take.\n//\n// Resolution is the library's — `resolveTokenColor` from `@kanzo-tech/ui`. This file used\n// to carry its own: a 1×1 canvas painted with the colour and read back through `getImageData`. Two\n// implementations of one job, in two packages, sharing nothing and failing differently, for the\n// same underlying reason — the theme is written in `oklch()` and `color-mix()`, cosmos.gl resolves\n// through d3-color and Observable Plot through its own `isColor`, and neither knows either syntax.\n// One browser probe, one place.\n//\n// What stays here is the part that is genuinely the graph's: the library answers `rgb(r, g, b)`\n// because that is what Plot eats, and `setPointColors` wants four floats in 0..1.\n\nimport { resolveTokenColor } from \"@kanzo-tech/ui\";\n\nexport type Rgba = [number, number, number, number];\n\n/**\n * What a point wears when there is no token to resolve — not a colour this package chose.\n *\n * It cannot be `var(--muted-foreground)`: this is the path taken precisely when the token layer is\n * unreachable — `document` is undefined (a server render), or the browser handed back something\n * `parseRgb` could not read. Pointing it at another token would be pointing at the thing that just\n * failed to answer.\n *\n * So it is a mid grey, and mid is the whole of the reasoning: 0.7 sits between the light page and\n * the dark one, so a point wearing it is visible against either rather than invisible against one.\n * It is a *nothing-resolved* signal, and if it ever reaches a real screen that is a bug in the\n * caller's `host`, not a colour to tune.\n */\nconst FALLBACK: Rgba = [0.7, 0.7, 0.7, 1];\nconst CHANNELS = /(-?[\\d.]+)/g;\n\n/** `rgb(37, 99, 235)` / `rgba(37, 99, 235, 0.5)` → four floats. */\nfunction parseRgb(css: string, fallback: Rgba): Rgba {\n const parts = css.match(CHANNELS)?.map(Number);\n if (!parts || parts.length < 3 || parts.some(Number.isNaN)) return fallback;\n const [r = 0, g = 0, b = 0, a = 1] = parts;\n return [r / 255, g / 255, b / 255, a];\n}\n\n/**\n * Any theme colour — a `var(--token)` or a literal — as GPU floats, resolved against `host`.\n *\n * `host` matters and is not ceremony: resolving against the element the canvas actually sits in is\n * what lets a scoped theme override win, which resolving against `<html>` would quietly lose.\n */\nexport function resolveToken(host: Element, value: string, fallback: Rgba = FALLBACK): Rgba {\n if (typeof document === \"undefined\" || !value) return fallback;\n const token = value.trim().replace(/^var\\(\\s*|\\s*\\)$/g, \"\");\n const resolved = resolveTokenColor(host, token.startsWith(\"--\") ? token : value);\n return parseRgb(resolved, fallback);\n}\n\nconst byte = (n: number) =>\n Math.round(Math.min(1, Math.max(0, n)) * 255)\n .toString(16)\n .padStart(2, \"0\");\n\n/** cosmos.gl's *config* colours go through d3-color, so they have to be hex. */\nexport function toHex([r, g, b, a]: Rgba): string {\n return `#${byte(r)}${byte(g)}${byte(b)}${a >= 1 ? \"\" : byte(a)}`;\n}\n"],"names":["FALLBACK","CHANNELS","parseRgb","css","fallback","parts","_a","r","g","b","a","resolveToken","host","value","token","resolved","resolveTokenColor","byte","n","toHex"],"mappings":";AA6BA,MAAMA,IAAiB,CAAC,KAAK,KAAK,KAAK,CAAC,GAClCC,IAAW;AAGjB,SAASC,EAASC,GAAaC,GAAsB;;AACnD,QAAMC,KAAQC,IAAAH,EAAI,MAAMF,CAAQ,MAAlB,gBAAAK,EAAqB,IAAI;AACvC,MAAI,CAACD,KAASA,EAAM,SAAS,KAAKA,EAAM,KAAK,OAAO,KAAK,EAAG,QAAOD;AACnE,QAAM,CAACG,IAAI,GAAGC,IAAI,GAAGC,IAAI,GAAGC,IAAI,CAAC,IAAIL;AACrC,SAAO,CAACE,IAAI,KAAKC,IAAI,KAAKC,IAAI,KAAKC,CAAC;AACtC;AAQO,SAASC,EAAaC,GAAeC,GAAeT,IAAiBJ,GAAgB;AAC1F,MAAI,OAAO,WAAa,OAAe,CAACa,EAAO,QAAOT;AACtD,QAAMU,IAAQD,EAAM,KAAA,EAAO,QAAQ,qBAAqB,EAAE,GACpDE,IAAWC,EAAkBJ,GAAME,EAAM,WAAW,IAAI,IAAIA,IAAQD,CAAK;AAC/E,SAAOX,EAASa,GAAUX,CAAQ;AACpC;AAEA,MAAMa,IAAO,CAACC,MACZ,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAGA,CAAC,CAAC,IAAI,GAAG,EACzC,SAAS,EAAE,EACX,SAAS,GAAG,GAAG;AAGb,SAASC,EAAM,CAAC,GAAGX,GAAGC,GAAGC,CAAC,GAAiB;AAChD,SAAO,IAAIO,EAAK,CAAC,CAAC,GAAGA,EAAKT,CAAC,CAAC,GAAGS,EAAKR,CAAC,CAAC,GAAGC,KAAK,IAAI,KAAKO,EAAKP,CAAC,CAAC;AAChE;"}
|