@kanzo-tech/graph 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bounded.d.ts +115 -40
- package/dist/bounded.d.ts.map +1 -1
- package/dist/bounded.js +13 -34
- package/dist/bounded.js.map +1 -1
- package/dist/duck-source.d.ts.map +1 -1
- package/dist/duck-source.js +165 -166
- package/dist/duck-source.js.map +1 -1
- package/dist/graph-canvas.d.ts +1 -1
- package/dist/graph-canvas.js.map +1 -1
- package/dist/graph-looks.d.ts +63 -18
- package/dist/graph-looks.d.ts.map +1 -1
- package/dist/graph-looks.js +37 -26
- package/dist/graph-looks.js.map +1 -1
- package/dist/graph-model.d.ts +2 -2
- package/dist/graph-model.d.ts.map +1 -1
- package/dist/graph-model.js +19 -14
- package/dist/graph-model.js.map +1 -1
- package/dist/graph-sim.d.ts +15 -1
- package/dist/graph-sim.d.ts.map +1 -1
- package/dist/graph-sim.js +17 -13
- package/dist/graph-sim.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -49
- package/dist/index.js.map +1 -1
- package/dist/memory-source.d.ts.map +1 -1
- package/dist/memory-source.js +99 -92
- package/dist/memory-source.js.map +1 -1
- package/dist/obligations.d.ts +1 -3
- package/dist/obligations.d.ts.map +1 -1
- package/dist/shape-glyph.d.ts +30 -0
- package/dist/shape-glyph.d.ts.map +1 -0
- package/dist/shape-glyph.js +15 -0
- package/dist/shape-glyph.js.map +1 -0
- package/dist/slice-client.d.ts.map +1 -1
- package/dist/slice-client.js +36 -36
- package/dist/slice-client.js.map +1 -1
- package/dist/use-graph-overlays.d.ts +21 -30
- package/dist/use-graph-overlays.d.ts.map +1 -1
- package/dist/use-graph-overlays.js +39 -37
- package/dist/use-graph-overlays.js.map +1 -1
- package/dist/use-graph.d.ts +38 -10
- package/dist/use-graph.d.ts.map +1 -1
- package/dist/use-graph.js +66 -65
- package/dist/use-graph.js.map +1 -1
- package/dist/use-query-loop.d.ts +6 -0
- package/dist/use-query-loop.d.ts.map +1 -1
- package/dist/use-query-loop.js +88 -70
- package/dist/use-query-loop.js.map +1 -1
- package/dist/use-renderer.d.ts.map +1 -1
- package/dist/use-renderer.js +69 -66
- package/dist/use-renderer.js.map +1 -1
- package/package.json +4 -4
package/dist/memory-source.js
CHANGED
|
@@ -1,134 +1,141 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
for (let i = 0; i < u; i++) x.set(t.vertices[i], i);
|
|
1
|
+
function U(t) {
|
|
2
|
+
const l = t.positions.length / 2;
|
|
3
|
+
let u = null, x = null;
|
|
4
|
+
const I = (o) => {
|
|
5
|
+
if (!u) {
|
|
6
|
+
u = /* @__PURE__ */ new Map();
|
|
7
|
+
for (let s = 0; s < l; s++) u.set(t.vertices[s], s);
|
|
9
8
|
}
|
|
10
|
-
return
|
|
11
|
-
},
|
|
12
|
-
var
|
|
13
|
-
if (!
|
|
14
|
-
|
|
15
|
-
for (let
|
|
16
|
-
const f = t.links[
|
|
17
|
-
(
|
|
9
|
+
return u.get(o);
|
|
10
|
+
}, m = (o) => {
|
|
11
|
+
var s, c;
|
|
12
|
+
if (!x) {
|
|
13
|
+
x = Array.from({ length: l }, () => []);
|
|
14
|
+
for (let i = 0; i < t.links.length; i += 2) {
|
|
15
|
+
const f = t.links[i], e = t.links[i + 1];
|
|
16
|
+
(s = x[f]) == null || s.push(e), (c = x[e]) == null || c.push(f);
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
return
|
|
19
|
+
return x[o] ?? [];
|
|
21
20
|
};
|
|
22
21
|
return {
|
|
23
|
-
total: () => Promise.resolve(
|
|
22
|
+
total: () => Promise.resolve(l),
|
|
24
23
|
// One pass over the array it is already holding, so the canvas frames the arrays rather than the
|
|
25
24
|
// renderer's default box.
|
|
26
|
-
extent: () => Promise.resolve(
|
|
27
|
-
slice(
|
|
28
|
-
const { limit:
|
|
29
|
-
return Promise.resolve(
|
|
25
|
+
extent: () => Promise.resolve(z(t.positions)),
|
|
26
|
+
slice(o) {
|
|
27
|
+
const { limit: s, minLinkPixels: c, perPixel: i, pinned: f, view: e } = o;
|
|
28
|
+
return Promise.resolve(
|
|
29
|
+
V(t, y(t, e, f), s, i, c)
|
|
30
|
+
);
|
|
30
31
|
},
|
|
31
32
|
// The only source we ship that has this at all: a rectangle needs a spatial predicate, which
|
|
32
33
|
// every source has, and a neighbourhood needs adjacency, which only a host holding its own links
|
|
33
34
|
// does. So the bounded canvas is an explorer here while a SQL source leaves it a map.
|
|
34
|
-
explore(
|
|
35
|
+
explore(o) {
|
|
35
36
|
return Promise.resolve(
|
|
36
|
-
|
|
37
|
+
V(
|
|
38
|
+
t,
|
|
39
|
+
w(o.seeds, o.depth),
|
|
40
|
+
o.limit,
|
|
41
|
+
o.perPixel,
|
|
42
|
+
o.minLinkPixels
|
|
43
|
+
)
|
|
37
44
|
);
|
|
38
45
|
}
|
|
39
46
|
};
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
let
|
|
43
|
-
for (const f of
|
|
44
|
-
const
|
|
45
|
-
|
|
47
|
+
function w(o, s) {
|
|
48
|
+
const c = /* @__PURE__ */ new Set();
|
|
49
|
+
let i = [];
|
|
50
|
+
for (const f of o) {
|
|
51
|
+
const e = I(f);
|
|
52
|
+
e !== void 0 && !c.has(e) && (c.add(e), i.push(e));
|
|
46
53
|
}
|
|
47
|
-
for (let f = 0; f <
|
|
48
|
-
const
|
|
49
|
-
for (const
|
|
50
|
-
for (const
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
for (let f = 0; f < s && i.length > 0; f++) {
|
|
55
|
+
const e = [];
|
|
56
|
+
for (const d of i)
|
|
57
|
+
for (const a of m(d))
|
|
58
|
+
c.has(a) || (c.add(a), e.push(a));
|
|
59
|
+
i = e;
|
|
53
60
|
}
|
|
54
|
-
return [...
|
|
61
|
+
return [...c];
|
|
55
62
|
}
|
|
56
|
-
function
|
|
57
|
-
const
|
|
58
|
-
for (let
|
|
59
|
-
const
|
|
60
|
-
|
|
63
|
+
function y(o, s, c) {
|
|
64
|
+
const i = [];
|
|
65
|
+
for (let e = 0; e < l; e++) {
|
|
66
|
+
const d = o.positions[e * 2], a = o.positions[e * 2 + 1];
|
|
67
|
+
d >= s.xMin && d <= s.xMax && a >= s.yMin && a <= s.yMax && i.push(e);
|
|
61
68
|
}
|
|
62
|
-
if (!(
|
|
63
|
-
const f = new Set(
|
|
64
|
-
for (const
|
|
65
|
-
const
|
|
66
|
-
|
|
69
|
+
if (!(c != null && c.length)) return i;
|
|
70
|
+
const f = new Set(i);
|
|
71
|
+
for (const e of c) {
|
|
72
|
+
const d = I(e);
|
|
73
|
+
d !== void 0 && f.add(d);
|
|
67
74
|
}
|
|
68
75
|
return [...f];
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
|
-
function
|
|
72
|
-
var
|
|
73
|
-
const
|
|
74
|
-
for (let n = 0; n <
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
for (let n = 0; n <
|
|
78
|
-
const
|
|
79
|
-
|
|
78
|
+
function V(t, l, u, x, I) {
|
|
79
|
+
var T, P, E;
|
|
80
|
+
const m = l.length, w = Math.max(1, Math.ceil(m / u)), y = [];
|
|
81
|
+
for (let n = 0; n < l.length && y.length < u; n += w)
|
|
82
|
+
y.push(l[n]);
|
|
83
|
+
const o = y.length, s = new Int32Array(t.positions.length / 2).fill(-1), c = new BigUint64Array(o), i = new Float32Array(o * 2), f = new Uint16Array(o), e = t.sizes ? new Float32Array(o) : void 0, d = t.subjects ? new Array(o) : void 0;
|
|
84
|
+
for (let n = 0; n < o; n++) {
|
|
85
|
+
const r = y[n];
|
|
86
|
+
s[r] = n, c[n] = t.vertices[r], d && (d[n] = ((T = t.subjects) == null ? void 0 : T[r]) ?? ""), i[n * 2] = t.positions[r * 2], i[n * 2 + 1] = t.positions[r * 2 + 1], f[n] = ((P = t.categories) == null ? void 0 : P[r]) ?? 0, e && (e[n] = ((E = t.sizes) == null ? void 0 : E[r]) ?? 0);
|
|
80
87
|
}
|
|
81
|
-
const a =
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
const
|
|
85
|
-
return
|
|
88
|
+
const a = x !== void 0 && x > 0 ? I * x : 0, N = [], k = (n) => {
|
|
89
|
+
const r = s[n];
|
|
90
|
+
if (r >= 0) return r;
|
|
91
|
+
const M = o + N.length;
|
|
92
|
+
return s[n] = M, N.push(n), M;
|
|
86
93
|
}, h = [];
|
|
87
94
|
for (let n = 0; n < t.links.length; n += 2) {
|
|
88
|
-
const
|
|
89
|
-
if (!(
|
|
95
|
+
const r = t.links[n], M = t.links[n + 1];
|
|
96
|
+
if (!(s[r] < 0 && s[M] < 0)) {
|
|
90
97
|
if (a > 0) {
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
98
|
+
const O = t.positions[r * 2] - t.positions[M * 2], S = t.positions[r * 2 + 1] - t.positions[M * 2 + 1];
|
|
99
|
+
if (O * O + S * S < a * a) continue;
|
|
93
100
|
}
|
|
94
|
-
h.push(
|
|
101
|
+
h.push(k(r), k(M));
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
|
-
const A =
|
|
98
|
-
M.set(r);
|
|
99
|
-
const v = new BigUint64Array(A);
|
|
104
|
+
const A = o + N.length, v = new Float32Array(A * 2);
|
|
100
105
|
v.set(i);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
const b = new BigUint64Array(A);
|
|
107
|
+
b.set(c);
|
|
108
|
+
const F = new Uint16Array(A);
|
|
109
|
+
F.set(f);
|
|
110
|
+
for (let n = 0; n < N.length; n++) {
|
|
111
|
+
const r = N[n];
|
|
112
|
+
v[(o + n) * 2] = t.positions[r * 2], v[(o + n) * 2 + 1] = t.positions[r * 2 + 1], b[o + n] = t.vertices[r], d && (d[o + n] = "");
|
|
106
113
|
}
|
|
107
114
|
return {
|
|
108
|
-
n:
|
|
109
|
-
marks:
|
|
110
|
-
vertices:
|
|
111
|
-
subjects:
|
|
112
|
-
positions:
|
|
115
|
+
n: m,
|
|
116
|
+
marks: o,
|
|
117
|
+
vertices: b,
|
|
118
|
+
subjects: d,
|
|
119
|
+
positions: v,
|
|
113
120
|
links: Float32Array.from(h),
|
|
114
|
-
categories:
|
|
115
|
-
sizes:
|
|
121
|
+
categories: F,
|
|
122
|
+
sizes: e ? j(e, A) : void 0
|
|
116
123
|
};
|
|
117
124
|
}
|
|
118
|
-
function
|
|
119
|
-
if (t.length ===
|
|
120
|
-
const
|
|
121
|
-
return
|
|
125
|
+
function j(t, l) {
|
|
126
|
+
if (t.length === l) return t;
|
|
127
|
+
const u = new Float32Array(l);
|
|
128
|
+
return u.set(t), u;
|
|
122
129
|
}
|
|
123
|
-
function
|
|
124
|
-
let
|
|
125
|
-
for (let
|
|
126
|
-
const
|
|
127
|
-
|
|
130
|
+
function z(t) {
|
|
131
|
+
let l = Number.POSITIVE_INFINITY, u = Number.POSITIVE_INFINITY, x = Number.NEGATIVE_INFINITY, I = Number.NEGATIVE_INFINITY;
|
|
132
|
+
for (let m = 0; m < t.length; m += 2) {
|
|
133
|
+
const w = t[m], y = t[m + 1];
|
|
134
|
+
w < l && (l = w), w > x && (x = w), y < u && (u = y), y > I && (I = y);
|
|
128
135
|
}
|
|
129
|
-
return Number.isFinite(
|
|
136
|
+
return Number.isFinite(l) ? { xMin: l, yMin: u, xMax: x, yMax: I } : { xMin: 0, yMin: 0, xMax: 0, yMax: 0 };
|
|
130
137
|
}
|
|
131
138
|
export {
|
|
132
|
-
|
|
139
|
+
U as memorySource
|
|
133
140
|
};
|
|
134
141
|
//# sourceMappingURL=memory-source.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-source.js","sources":["../src/memory-source.ts"],"sourcesContent":["import {\n BOUNDED_DEFAULTS,\n type ExploreRequest,\n type ExploringSource,\n type Slice,\n type SliceRequest,\n type Viewport,\n} from \"./bounded\";\nimport type { VertexId } from \"./resident\";\n\n/**\n * The trivial source: a host that already holds its arrays.\n *\n * ADR-0001 deletes `load()`, which means every consumer needs a source — including the ones for\n * which bounding buys nothing, because their graph fits. This is that source, and it exists so that\n * \"wrap your arrays\" is one import rather than a hundred lines each call site writes differently.\n *\n * It is also the only source we ship that is an [`ExploringSource`]. A rectangle is a map question\n * and any relation with a spatial predicate can answer it; a neighbourhood is the graph question,\n * and answering it needs adjacency. Having the links in hand, this one does.\n *\n * Everything derived is built on first use and kept: a graph small enough to hold is small enough\n * that an adjacency list is cheap, but a host that only ever pans should not pay to build one.\n */\n\nexport interface MemoryGraph {\n /** Who each point is, parallel to the position pairs — `vertexId(type, dense)` per point. */\n vertices: BigUint64Array;\n /** `[x0, y0, x1, y1, …]`. */\n positions: Float32Array;\n /** `[src, dst, …]` as indices into `positions`. */\n links: Float32Array;\n /**\n * The subject IRI of each vertex, parallel to `vertices` — optional, and the same opt-in the SQL\n * source makes for the same reason: a host that never names a vertex should not carry the names.\n */\n subjects?: string[];\n categories?: Uint16Array;\n sizes?: Float32Array;\n}\n\nexport function memorySource(graph: MemoryGraph): ExploringSource {\n const count = graph.positions.length / 2;\n\n /** Built on demand — a host that only pans never asks for either of these. */\n let byVertex: Map<bigint, number> | null = null;\n let adjacency: number[][] | null = null;\n\n const indexOf = (vertex: VertexId): number | undefined => {\n if (!byVertex) {\n byVertex = new Map();\n for (let i = 0; i < count; i++) byVertex.set(graph.vertices[i] as bigint, i);\n }\n return byVertex.get(vertex);\n };\n\n const neighbours = (index: number): number[] => {\n if (!adjacency) {\n adjacency = Array.from({ length: count }, () => [] as number[]);\n for (let e = 0; e < graph.links.length; e += 2) {\n const src = graph.links[e] as number;\n const dst = graph.links[e + 1] as number;\n adjacency[src]?.push(dst);\n adjacency[dst]?.push(src);\n }\n }\n return adjacency[index] ?? [];\n };\n\n return {\n total: () => Promise.resolve(count),\n // One pass over the array it is already holding, so the canvas frames the arrays rather than the\n // renderer's default box.\n extent: () => Promise.resolve(boundsOf(graph.positions)),\n slice(request: SliceRequest): Promise<Slice> {\n const { limit, perPixel, pinned, view } = request;\n return Promise.resolve(gather(graph, inside(graph, view, pinned), limit, perPixel));\n },\n // The only source we ship that has this at all: a rectangle needs a spatial predicate, which\n // every source has, and a neighbourhood needs adjacency, which only a host holding its own links\n // does. So the bounded canvas is an explorer here while a SQL source leaves it a map.\n explore(request: ExploreRequest): Promise<Slice> {\n return Promise.resolve(\n gather(graph, expand(request.seeds, request.depth), request.limit, request.perPixel),\n );\n },\n };\n\n /**\n * Breadth-first from the seeds, `depth` hops out. Seeds are identities; everything after is a\n * corpus index, which is this source's own numbering and not a slice's.\n */\n function expand(seeds: VertexId[], depth: number): number[] {\n const seen = new Set<number>();\n let frontier: number[] = [];\n for (const vertex of seeds) {\n const index = indexOf(vertex);\n if (index !== undefined && !seen.has(index)) {\n seen.add(index);\n frontier.push(index);\n }\n }\n for (let hop = 0; hop < depth && frontier.length > 0; hop++) {\n const next: number[] = [];\n for (const index of frontier) {\n for (const other of neighbours(index)) {\n if (seen.has(other)) continue;\n seen.add(other);\n next.push(other);\n }\n }\n frontier = next;\n }\n return [...seen];\n }\n\n /** Everything the rectangle holds, plus whatever the reader is holding on to. */\n function inside(data: MemoryGraph, view: Viewport, pinned: VertexId[] | undefined): number[] {\n const hit: number[] = [];\n for (let i = 0; i < count; i++) {\n const x = data.positions[i * 2] as number;\n const y = data.positions[i * 2 + 1] as number;\n if (x >= view.xMin && x <= view.xMax && y >= view.yMin && y <= view.yMax) hit.push(i);\n }\n if (!pinned?.length) return hit;\n const held = new Set(hit);\n for (const vertex of pinned) {\n const index = indexOf(vertex);\n if (index !== undefined) held.add(index);\n }\n return [...held];\n }\n}\n\n/**\n * Chosen indices to a slice: renumber, copy the geometry, keep the links both of whose ends survived.\n *\n * `n` is what matched before `limit` cut it, which is the one honest thing a bounded view owes its\n * reader — a truncated answer must not look like a complete one.\n *\n * **Over the limit it strides rather than taking the front**, which is the same rule the SQL sources\n * apply and for the same reason: the front of an ordering is a *region* of whatever that ordering\n * follows, so `chosen.slice(0, limit)` drew one corner of a window and called it the window.\n *\n * What this source cannot promise, and the SQL ones can: that the stride is **spatially**\n * stratified. A corpus numbers along the Morton curve so every `s`-th id is spread over the space;\n * here the order is whichever order the host built its arrays in, and nothing knows what that is. A\n * stride over an unknown order is at worst an arbitrary sample, where a prefix of an unknown order\n * is at worst an arbitrary *contiguous* sample — so this is never the worse of the two and is\n * sometimes much better.\n */\nfunction gather(\n graph: MemoryGraph,\n chosen: number[],\n limit: number,\n perPixel: number | undefined,\n): Slice {\n const matched = chosen.length;\n const stride = Math.max(1, Math.ceil(matched / limit));\n const kept: number[] = [];\n for (let i = 0; i < chosen.length && kept.length < limit; i += stride) {\n kept.push(chosen[i] as number);\n }\n const n = kept.length;\n\n // Global index → position in this slice, `-1` for everything not in it. One pass over the corpus\n // rather than a Map: the arrays are already dense and an Int32Array of N is cheaper than N boxed\n // entries, which is the whole argument of this branch in miniature.\n const local = new Int32Array(graph.positions.length / 2).fill(-1);\n const vertices = new BigUint64Array(n);\n const positions = new Float32Array(n * 2);\n const categories = new Uint16Array(n);\n const sizes = graph.sizes ? new Float32Array(n) : undefined;\n const subjects = graph.subjects ? new Array<string>(n) : undefined;\n for (let i = 0; i < n; i++) {\n const from = kept[i] as number;\n local[from] = i;\n vertices[i] = graph.vertices[from] as bigint;\n if (subjects) subjects[i] = graph.subjects?.[from] ?? \"\";\n positions[i * 2] = graph.positions[from * 2] as number;\n positions[i * 2 + 1] = graph.positions[from * 2 + 1] as number;\n categories[i] = graph.categories?.[from] ?? 0;\n if (sizes) sizes[i] = graph.sizes?.[from] ?? 0;\n }\n\n /**\n * The edges, and the far ends they need — **which this source has, because it holds everything.**\n *\n * A window that fits loses nothing here; a window over the limit is a sample, and an edge from a\n * sampled vertex to one the stride passed over used to be dropped. It is not a missing fact: the\n * arrays are in hand. So the far end is appended as an **anchor** — a point at its real\n * coordinates, past `marks`, never drawn. The SQL source reaches the same answer out of the tiles\n * it fetched; this one reaches it out of the arrays it was handed.\n *\n * `perPixel` given, an edge shorter than `minLinkPixels` on screen is not sent at all — it is a\n * dot on top of two dots the point layer has already drawn.\n */\n const floor = perPixel !== undefined && perPixel > 0 ? BOUNDED_DEFAULTS.minLinkPixels * perPixel : 0;\n const anchors: number[] = [];\n const anchorOf = (from: number): number => {\n const seen = local[from] as number;\n if (seen >= 0) return seen;\n const at = n + anchors.length;\n local[from] = at;\n anchors.push(from);\n return at;\n };\n\n const links: number[] = [];\n for (let e = 0; e < graph.links.length; e += 2) {\n const a = graph.links[e] as number;\n const b = graph.links[e + 1] as number;\n // At least one end drawn. Neither drawn is an edge somewhere else entirely, and drawing it would\n // put ink outside the window the caller asked about.\n if ((local[a] as number) < 0 && (local[b] as number) < 0) continue;\n if (floor > 0) {\n const dx = (graph.positions[a * 2] as number) - (graph.positions[b * 2] as number);\n const dy = (graph.positions[a * 2 + 1] as number) - (graph.positions[b * 2 + 1] as number);\n if (dx * dx + dy * dy < floor * floor) continue;\n }\n links.push(anchorOf(a), anchorOf(b));\n }\n\n const all = n + anchors.length;\n const whole = new Float32Array(all * 2);\n whole.set(positions);\n const who = new BigUint64Array(all);\n who.set(vertices);\n const ordinals = new Uint16Array(all);\n ordinals.set(categories);\n for (let i = 0; i < anchors.length; i++) {\n const from = anchors[i] as number;\n whole[(n + i) * 2] = graph.positions[from * 2] as number;\n whole[(n + i) * 2 + 1] = graph.positions[from * 2 + 1] as number;\n who[n + i] = graph.vertices[from] as bigint;\n // No category and no size: an anchor is not drawn, and giving it either would put it in the\n // colour scale's domain and the size ramp's range for a point nobody can see.\n if (subjects) subjects[n + i] = \"\";\n }\n\n return {\n n: matched,\n marks: n,\n vertices: who,\n subjects,\n positions: whole,\n links: Float32Array.from(links),\n categories: ordinals,\n sizes: sizes ? growTo(sizes, all) : undefined,\n };\n}\n\n/** A ramp column widened to cover the anchors, which contribute nothing to it. */\nfunction growTo(values: Float32Array, length: number): Float32Array {\n if (values.length === length) return values;\n const wider = new Float32Array(length);\n wider.set(values);\n return wider;\n}\n\n/**\n * The rectangle a set of interleaved positions occupies.\n *\n * Empty arrays answer a degenerate rectangle at the origin rather than `±Infinity`: a view framed on\n * nothing should sit somewhere, and the infinities would make the fit arithmetic produce `NaN`.\n */\nfunction boundsOf(positions: Float32Array): Viewport {\n let xMin = Number.POSITIVE_INFINITY;\n let yMin = Number.POSITIVE_INFINITY;\n let xMax = Number.NEGATIVE_INFINITY;\n let yMax = Number.NEGATIVE_INFINITY;\n for (let i = 0; i < positions.length; i += 2) {\n const x = positions[i] as number;\n const y = positions[i + 1] as number;\n if (x < xMin) xMin = x;\n if (x > xMax) xMax = x;\n if (y < yMin) yMin = y;\n if (y > yMax) yMax = y;\n }\n if (!Number.isFinite(xMin)) return { xMin: 0, yMin: 0, xMax: 0, yMax: 0 };\n return { xMin, yMin, xMax, yMax };\n}\n"],"names":["memorySource","graph","count","byVertex","adjacency","indexOf","vertex","neighbours","index","e","src","dst","_a","_b","boundsOf","request","limit","perPixel","pinned","view","gather","inside","expand","seeds","depth","seen","frontier","hop","next","other","data","hit","i","x","held","chosen","matched","stride","kept","n","local","vertices","positions","categories","sizes","subjects","from","_c","floor","BOUNDED_DEFAULTS","anchors","anchorOf","at","links","a","b","dx","dy","all","whole","who","ordinals","growTo","values","length","wider","xMin","yMin","xMax","yMax","y"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"memory-source.js","sources":["../src/memory-source.ts"],"sourcesContent":["import {\n type ExploreRequest,\n type ExploringSource,\n type Slice,\n type SliceRequest,\n type Viewport,\n} from \"./bounded\";\nimport type { VertexId } from \"./resident\";\n\n/**\n * The trivial source: a host that already holds its arrays.\n *\n * ADR-0001 deletes `load()`, which means every consumer needs a source — including the ones for\n * which bounding buys nothing, because their graph fits. This is that source, and it exists so that\n * \"wrap your arrays\" is one import rather than a hundred lines each call site writes differently.\n *\n * It is also the only source we ship that is an [`ExploringSource`]. A rectangle is a map question\n * and any relation with a spatial predicate can answer it; a neighbourhood is the graph question,\n * and answering it needs adjacency. Having the links in hand, this one does.\n *\n * Everything derived is built on first use and kept: a graph small enough to hold is small enough\n * that an adjacency list is cheap, but a host that only ever pans should not pay to build one.\n */\n\nexport interface MemoryGraph {\n /** Who each point is, parallel to the position pairs — `vertexId(type, dense)` per point. */\n vertices: BigUint64Array;\n /** `[x0, y0, x1, y1, …]`. */\n positions: Float32Array;\n /** `[src, dst, …]` as indices into `positions`. */\n links: Float32Array;\n /**\n * The subject IRI of each vertex, parallel to `vertices` — optional, and the same opt-in the SQL\n * source makes for the same reason: a host that never names a vertex should not carry the names.\n */\n subjects?: string[];\n categories?: Uint16Array;\n sizes?: Float32Array;\n}\n\nexport function memorySource(graph: MemoryGraph): ExploringSource {\n const count = graph.positions.length / 2;\n\n /** Built on demand — a host that only pans never asks for either of these. */\n let byVertex: Map<bigint, number> | null = null;\n let adjacency: number[][] | null = null;\n\n const indexOf = (vertex: VertexId): number | undefined => {\n if (!byVertex) {\n byVertex = new Map();\n for (let i = 0; i < count; i++) byVertex.set(graph.vertices[i] as bigint, i);\n }\n return byVertex.get(vertex);\n };\n\n const neighbours = (index: number): number[] => {\n if (!adjacency) {\n adjacency = Array.from({ length: count }, () => [] as number[]);\n for (let e = 0; e < graph.links.length; e += 2) {\n const src = graph.links[e] as number;\n const dst = graph.links[e + 1] as number;\n adjacency[src]?.push(dst);\n adjacency[dst]?.push(src);\n }\n }\n return adjacency[index] ?? [];\n };\n\n return {\n total: () => Promise.resolve(count),\n // One pass over the array it is already holding, so the canvas frames the arrays rather than the\n // renderer's default box.\n extent: () => Promise.resolve(boundsOf(graph.positions)),\n slice(request: SliceRequest): Promise<Slice> {\n const { limit, minLinkPixels, perPixel, pinned, view } = request;\n return Promise.resolve(\n gather(graph, inside(graph, view, pinned), limit, perPixel, minLinkPixels),\n );\n },\n // The only source we ship that has this at all: a rectangle needs a spatial predicate, which\n // every source has, and a neighbourhood needs adjacency, which only a host holding its own links\n // does. So the bounded canvas is an explorer here while a SQL source leaves it a map.\n explore(request: ExploreRequest): Promise<Slice> {\n return Promise.resolve(\n gather(\n graph,\n expand(request.seeds, request.depth),\n request.limit,\n request.perPixel,\n request.minLinkPixels,\n ),\n );\n },\n };\n\n /**\n * Breadth-first from the seeds, `depth` hops out. Seeds are identities; everything after is a\n * corpus index, which is this source's own numbering and not a slice's.\n */\n function expand(seeds: VertexId[], depth: number): number[] {\n const seen = new Set<number>();\n let frontier: number[] = [];\n for (const vertex of seeds) {\n const index = indexOf(vertex);\n if (index !== undefined && !seen.has(index)) {\n seen.add(index);\n frontier.push(index);\n }\n }\n for (let hop = 0; hop < depth && frontier.length > 0; hop++) {\n const next: number[] = [];\n for (const index of frontier) {\n for (const other of neighbours(index)) {\n if (seen.has(other)) continue;\n seen.add(other);\n next.push(other);\n }\n }\n frontier = next;\n }\n return [...seen];\n }\n\n /** Everything the rectangle holds, plus whatever the reader is holding on to. */\n function inside(data: MemoryGraph, view: Viewport, pinned: VertexId[] | undefined): number[] {\n const hit: number[] = [];\n for (let i = 0; i < count; i++) {\n const x = data.positions[i * 2] as number;\n const y = data.positions[i * 2 + 1] as number;\n if (x >= view.xMin && x <= view.xMax && y >= view.yMin && y <= view.yMax) hit.push(i);\n }\n if (!pinned?.length) return hit;\n const held = new Set(hit);\n for (const vertex of pinned) {\n const index = indexOf(vertex);\n if (index !== undefined) held.add(index);\n }\n return [...held];\n }\n}\n\n/**\n * Chosen indices to a slice: renumber, copy the geometry, keep the links both of whose ends survived.\n *\n * `n` is what matched before `limit` cut it, which is the one honest thing a bounded view owes its\n * reader — a truncated answer must not look like a complete one.\n *\n * **Over the limit it strides rather than taking the front**, which is the same rule the SQL sources\n * apply and for the same reason: the front of an ordering is a *region* of whatever that ordering\n * follows, so `chosen.slice(0, limit)` drew one corner of a window and called it the window.\n *\n * What this source cannot promise, and the SQL ones can: that the stride is **spatially**\n * stratified. A corpus numbers along the Morton curve so every `s`-th id is spread over the space;\n * here the order is whichever order the host built its arrays in, and nothing knows what that is. A\n * stride over an unknown order is at worst an arbitrary sample, where a prefix of an unknown order\n * is at worst an arbitrary *contiguous* sample — so this is never the worse of the two and is\n * sometimes much better.\n */\nfunction gather(\n graph: MemoryGraph,\n chosen: number[],\n limit: number,\n perPixel: number | undefined,\n minLinkPixels: number,\n): Slice {\n const matched = chosen.length;\n const stride = Math.max(1, Math.ceil(matched / limit));\n const kept: number[] = [];\n for (let i = 0; i < chosen.length && kept.length < limit; i += stride) {\n kept.push(chosen[i] as number);\n }\n const n = kept.length;\n\n // Global index → position in this slice, `-1` for everything not in it. One pass over the corpus\n // rather than a Map: the arrays are already dense and an Int32Array of N is cheaper than N boxed\n // entries, which is the whole argument of this branch in miniature.\n const local = new Int32Array(graph.positions.length / 2).fill(-1);\n const vertices = new BigUint64Array(n);\n const positions = new Float32Array(n * 2);\n const categories = new Uint16Array(n);\n const sizes = graph.sizes ? new Float32Array(n) : undefined;\n const subjects = graph.subjects ? new Array<string>(n) : undefined;\n for (let i = 0; i < n; i++) {\n const from = kept[i] as number;\n local[from] = i;\n vertices[i] = graph.vertices[from] as bigint;\n if (subjects) subjects[i] = graph.subjects?.[from] ?? \"\";\n positions[i * 2] = graph.positions[from * 2] as number;\n positions[i * 2 + 1] = graph.positions[from * 2 + 1] as number;\n categories[i] = graph.categories?.[from] ?? 0;\n if (sizes) sizes[i] = graph.sizes?.[from] ?? 0;\n }\n\n /**\n * The edges, and the far ends they need — **which this source has, because it holds everything.**\n *\n * A window that fits loses nothing here; a window over the limit is a sample, and an edge from a\n * sampled vertex to one the stride passed over used to be dropped. It is not a missing fact: the\n * arrays are in hand. So the far end is appended as an **anchor** — a point at its real\n * coordinates, past `marks`, never drawn. The SQL source reaches the same answer out of the tiles\n * it fetched; this one reaches it out of the arrays it was handed.\n *\n * `perPixel` given, an edge shorter than `minLinkPixels` on screen is not sent at all — it is a\n * dot on top of two dots the point layer has already drawn. Both numbers come off the request:\n * this source used to read the threshold out of an exported table, which is the thing that made a\n * request something a source had to complete rather than answer.\n */\n const floor = perPixel !== undefined && perPixel > 0 ? minLinkPixels * perPixel : 0;\n const anchors: number[] = [];\n const anchorOf = (from: number): number => {\n const seen = local[from] as number;\n if (seen >= 0) return seen;\n const at = n + anchors.length;\n local[from] = at;\n anchors.push(from);\n return at;\n };\n\n const links: number[] = [];\n for (let e = 0; e < graph.links.length; e += 2) {\n const a = graph.links[e] as number;\n const b = graph.links[e + 1] as number;\n // At least one end drawn. Neither drawn is an edge somewhere else entirely, and drawing it would\n // put ink outside the window the caller asked about.\n if ((local[a] as number) < 0 && (local[b] as number) < 0) continue;\n if (floor > 0) {\n const dx = (graph.positions[a * 2] as number) - (graph.positions[b * 2] as number);\n const dy = (graph.positions[a * 2 + 1] as number) - (graph.positions[b * 2 + 1] as number);\n if (dx * dx + dy * dy < floor * floor) continue;\n }\n links.push(anchorOf(a), anchorOf(b));\n }\n\n const all = n + anchors.length;\n const whole = new Float32Array(all * 2);\n whole.set(positions);\n const who = new BigUint64Array(all);\n who.set(vertices);\n const ordinals = new Uint16Array(all);\n ordinals.set(categories);\n for (let i = 0; i < anchors.length; i++) {\n const from = anchors[i] as number;\n whole[(n + i) * 2] = graph.positions[from * 2] as number;\n whole[(n + i) * 2 + 1] = graph.positions[from * 2 + 1] as number;\n who[n + i] = graph.vertices[from] as bigint;\n // No category and no size: an anchor is not drawn, and giving it either would put it in the\n // colour scale's domain and the size ramp's range for a point nobody can see.\n if (subjects) subjects[n + i] = \"\";\n }\n\n return {\n n: matched,\n marks: n,\n vertices: who,\n subjects,\n positions: whole,\n links: Float32Array.from(links),\n categories: ordinals,\n sizes: sizes ? growTo(sizes, all) : undefined,\n };\n}\n\n/** A ramp column widened to cover the anchors, which contribute nothing to it. */\nfunction growTo(values: Float32Array, length: number): Float32Array {\n if (values.length === length) return values;\n const wider = new Float32Array(length);\n wider.set(values);\n return wider;\n}\n\n/**\n * The rectangle a set of interleaved positions occupies.\n *\n * Empty arrays answer a degenerate rectangle at the origin rather than `±Infinity`: a view framed on\n * nothing should sit somewhere, and the infinities would make the fit arithmetic produce `NaN`.\n */\nfunction boundsOf(positions: Float32Array): Viewport {\n let xMin = Number.POSITIVE_INFINITY;\n let yMin = Number.POSITIVE_INFINITY;\n let xMax = Number.NEGATIVE_INFINITY;\n let yMax = Number.NEGATIVE_INFINITY;\n for (let i = 0; i < positions.length; i += 2) {\n const x = positions[i] as number;\n const y = positions[i + 1] as number;\n if (x < xMin) xMin = x;\n if (x > xMax) xMax = x;\n if (y < yMin) yMin = y;\n if (y > yMax) yMax = y;\n }\n if (!Number.isFinite(xMin)) return { xMin: 0, yMin: 0, xMax: 0, yMax: 0 };\n return { xMin, yMin, xMax, yMax };\n}\n"],"names":["memorySource","graph","count","byVertex","adjacency","indexOf","vertex","i","neighbours","index","_a","_b","e","src","dst","boundsOf","request","limit","minLinkPixels","perPixel","pinned","view","gather","inside","expand","seeds","depth","seen","frontier","hop","next","other","data","hit","x","y","held","chosen","_c","matched","stride","kept","n","local","vertices","positions","categories","sizes","subjects","from","floor","anchors","anchorOf","at","links","a","b","dx","dy","all","whole","who","ordinals","growTo","values","length","wider","xMin","yMin","xMax","yMax"],"mappings":"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"}
|
package/dist/obligations.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Channels } from './graph-model';
|
|
2
|
-
import {
|
|
2
|
+
import { Look } from './graph-looks';
|
|
3
3
|
/**
|
|
4
4
|
* What the graph's geometry owes, as a report rather than as prose.
|
|
5
5
|
*
|
|
@@ -57,6 +57,4 @@ export interface Check extends Obligation {
|
|
|
57
57
|
* `OBLIGATIONS.length - check().length` is the number of open questions this section has.
|
|
58
58
|
*/
|
|
59
59
|
export declare function check(): Check[];
|
|
60
|
-
/** `SHAPE.cross` is the past-capacity glyph and `SHAPE` is re-exported so a caller can name it. */
|
|
61
|
-
export { SHAPE };
|
|
62
60
|
//# sourceMappingURL=obligations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"obligations.d.ts","sourceRoot":"","sources":["../src/obligations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,
|
|
1
|
+
{"version":3,"file":"obligations.d.ts","sourceRoot":"","sources":["../src/obligations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAsC,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,UAAU;IACzB,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,gGAAgG;IAChG,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gDAAgD;IAChD,KAAK,EAAE,IAAI,GAAG,IAAI,GAAG,GAAG,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;CAChB;AAgDD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,IAAI,CAK7E;AAED,eAAO,MAAM,WAAW,EAAE,SAAS,UAAU,EAiE5C,CAAC;AAEF,8DAA8D;AAC9D,MAAM,WAAW,KAAM,SAAQ,UAAU;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,IAAI,KAAK,EAAE,CAa/B"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Shape } from './graph-looks';
|
|
2
|
+
/**
|
|
3
|
+
* The glyph the canvas draws for a category, in the DOM — a legend key, a hover card, a preview.
|
|
4
|
+
*
|
|
5
|
+
* **This replaced `SHAPE_PATH` on the barrel, and the difference is what a host is made responsible
|
|
6
|
+
* for.** The path table was exported for one reason: a host drawing a legend wanted the same glyph
|
|
7
|
+
* the point shader fills, because a legend drawn from a second set of shapes is a legend that
|
|
8
|
+
* explains a picture nobody is looking at. What the host got was path *data*, and with it three
|
|
9
|
+
* facts it had to keep in step with `graph-looks.ts` by reading a comment — that the box is twelve
|
|
10
|
+
* units, that the path wants a `fill` rather than a `stroke`, and which enum index each glyph is
|
|
11
|
+
* keyed by. Every one of those was silent when it drifted.
|
|
12
|
+
*
|
|
13
|
+
* A component carries all three and cannot disagree with itself. What is left at the call site is
|
|
14
|
+
* the only thing the host actually decides: how big, and what colour.
|
|
15
|
+
*
|
|
16
|
+
* **No `"use client"`, deliberately.** It is a function of its props with no state, no effect and no
|
|
17
|
+
* event handler, so it renders on a server the way any other markup does. The rest of this package
|
|
18
|
+
* is client-only because a WebGL context is; a `<path>` is not.
|
|
19
|
+
*/
|
|
20
|
+
export interface ShapeGlyphProps extends React.SVGProps<SVGSVGElement> {
|
|
21
|
+
/** Which glyph — the name `scaleOf(...).shape(ordinal)` answers with. */
|
|
22
|
+
shape: Shape;
|
|
23
|
+
/**
|
|
24
|
+
* What to fill it with. Defaults to `currentColor`, so a glyph inside a legend row inherits the
|
|
25
|
+
* row's colour and a host that is colouring by category passes `scaleOf(...).color(ordinal)`.
|
|
26
|
+
*/
|
|
27
|
+
color?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function ShapeGlyph({ color, shape, ...rest }: ShapeGlyphProps): import("react").JSX.Element;
|
|
30
|
+
//# sourceMappingURL=shape-glyph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape-glyph.d.ts","sourceRoot":"","sources":["../src/shape-glyph.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAgB,SAAQ,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC;IACpE,yEAAyE;IACzE,KAAK,EAAE,KAAK,CAAC;IACb;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,UAAU,CAAC,EAAE,KAAsB,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,eAAe,+BAUrF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as r } from "react/jsx-runtime";
|
|
2
|
+
import { SHAPE_PATH as i } from "./graph-looks.js";
|
|
3
|
+
function n({ color: o = "currentColor", shape: t, ...e }) {
|
|
4
|
+
return (
|
|
5
|
+
// The `viewBox` is the glyph's own box and is not a prop: the paths are authored inside a 12×12
|
|
6
|
+
// square, which is the fact the host used to have to know. Size it from outside — a `className`,
|
|
7
|
+
// a `width`/`height`, or `x`/`y`/`width`/`height` when this is nested inside another `<svg>`,
|
|
8
|
+
// which is how a preview places a glyph at a vertex.
|
|
9
|
+
/* @__PURE__ */ r("svg", { viewBox: "0 0 12 12", ...e, children: /* @__PURE__ */ r("path", { d: i[t], fill: o }) })
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
n as ShapeGlyph
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=shape-glyph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape-glyph.js","sources":["../src/shape-glyph.tsx"],"sourcesContent":["import { SHAPE_PATH, type Shape } from \"./graph-looks\";\n\n/**\n * The glyph the canvas draws for a category, in the DOM — a legend key, a hover card, a preview.\n *\n * **This replaced `SHAPE_PATH` on the barrel, and the difference is what a host is made responsible\n * for.** The path table was exported for one reason: a host drawing a legend wanted the same glyph\n * the point shader fills, because a legend drawn from a second set of shapes is a legend that\n * explains a picture nobody is looking at. What the host got was path *data*, and with it three\n * facts it had to keep in step with `graph-looks.ts` by reading a comment — that the box is twelve\n * units, that the path wants a `fill` rather than a `stroke`, and which enum index each glyph is\n * keyed by. Every one of those was silent when it drifted.\n *\n * A component carries all three and cannot disagree with itself. What is left at the call site is\n * the only thing the host actually decides: how big, and what colour.\n *\n * **No `\"use client\"`, deliberately.** It is a function of its props with no state, no effect and no\n * event handler, so it renders on a server the way any other markup does. The rest of this package\n * is client-only because a WebGL context is; a `<path>` is not.\n */\nexport interface ShapeGlyphProps extends React.SVGProps<SVGSVGElement> {\n /** Which glyph — the name `scaleOf(...).shape(ordinal)` answers with. */\n shape: Shape;\n /**\n * What to fill it with. Defaults to `currentColor`, so a glyph inside a legend row inherits the\n * row's colour and a host that is colouring by category passes `scaleOf(...).color(ordinal)`.\n */\n color?: string;\n}\n\nexport function ShapeGlyph({ color = \"currentColor\", shape, ...rest }: ShapeGlyphProps) {\n return (\n // The `viewBox` is the glyph's own box and is not a prop: the paths are authored inside a 12×12\n // square, which is the fact the host used to have to know. Size it from outside — a `className`,\n // a `width`/`height`, or `x`/`y`/`width`/`height` when this is nested inside another `<svg>`,\n // which is how a preview places a glyph at a vertex.\n <svg viewBox=\"0 0 12 12\" {...rest}>\n <path d={SHAPE_PATH[shape]} fill={color} />\n </svg>\n );\n}\n"],"names":["ShapeGlyph","color","shape","rest","jsx","SHAPE_PATH"],"mappings":";;AA8BO,SAASA,EAAW,EAAE,OAAAC,IAAQ,gBAAgB,OAAAC,GAAO,GAAGC,KAAyB;AACtF;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE,gBAAAC,EAAC,OAAA,EAAI,SAAQ,aAAa,GAAGD,GAC3B,UAAA,gBAAAC,EAAC,QAAA,EAAK,GAAGC,EAAWH,CAAK,GAAG,MAAMD,GAAO,EAAA,CAC3C;AAAA;AAEJ;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slice-client.d.ts","sourceRoot":"","sources":["../src/slice-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG7E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,SAAU,SAAQ,YAAY;;IAOzC;;;;;OAKG;gBACS,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,SAAS;IAK1D;;;;;;;;OAQG;IACH,IAAa,YAAY,IAAI,OAAO,CAEnC;IAED;;;;;;;OAOG;IACH,IAAI,SAAS,IAAI,UAAU,CAE1B;IAED;;;;;;OAMG;IACH,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"slice-client.d.ts","sourceRoot":"","sources":["../src/slice-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG7E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,SAAU,SAAQ,YAAY;;IAOzC;;;;;OAKG;gBACS,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,SAAS;IAK1D;;;;;;;;OAQG;IACH,IAAa,YAAY,IAAI,OAAO,CAEnC;IAED;;;;;;;OAOG;IACH,IAAI,SAAS,IAAI,UAAU,CAE1B;IAED;;;;;;OAMG;IACH,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B5D;;;;;;OAMG;IACH,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,EAEpD;IAED,+FAA+F;IAC/F,OAAO,IAAI,IAAI;IAWN,KAAK,CAAC,MAAM,GAAE,UAAe,GAAG,MAAM,GAAG,IAAI;IAI7C,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAQhC,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;CAMxC"}
|
package/dist/slice-client.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
var p = (
|
|
3
|
-
throw TypeError(
|
|
2
|
+
var p = (t) => {
|
|
3
|
+
throw TypeError(t);
|
|
4
4
|
};
|
|
5
|
-
var m = (
|
|
6
|
-
var l = (
|
|
7
|
-
import { MosaicClient as
|
|
8
|
-
import {
|
|
9
|
-
var
|
|
10
|
-
class
|
|
5
|
+
var m = (t, r, e) => r.has(t) || p("Cannot " + e);
|
|
6
|
+
var l = (t, r, e) => (m(t, r, "read from private field"), e ? e.call(t) : r.get(t)), o = (t, r, e) => r.has(t) ? p("Cannot add the same private member more than once") : r instanceof WeakSet ? r.add(t) : r.set(t, e), i = (t, r, e, s) => (m(t, r, "write to private field"), s ? s.call(t, e) : r.set(t, e), e);
|
|
7
|
+
import { MosaicClient as y } from "@kanzo-tech/mosaic";
|
|
8
|
+
import { abortError as w } from "./bounded.js";
|
|
9
|
+
var u, c, n, a, h;
|
|
10
|
+
class x extends y {
|
|
11
11
|
/**
|
|
12
12
|
* @param filterBy The crossfilter this read lives inside, if any. The unfiltered reads — how big
|
|
13
13
|
* the corpus is, where it is, what its tile footers say — take none: those are facts about the
|
|
14
14
|
* corpus rather than about what the page is looking at, and filtering them would make
|
|
15
15
|
* "20,000 of 1,000,000" a fraction of itself.
|
|
16
16
|
*/
|
|
17
|
-
constructor(
|
|
17
|
+
constructor(e, s) {
|
|
18
18
|
super(s);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
o(this, u);
|
|
20
|
+
o(this, c, null);
|
|
21
|
+
o(this, n, null);
|
|
22
|
+
o(this, a, null);
|
|
23
|
+
o(this, h, !1);
|
|
24
|
+
i(this, u, e);
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Pre-aggregation cannot help here, and saying so costs a getter instead of a query.
|
|
@@ -44,8 +44,8 @@ class R extends w {
|
|
|
44
44
|
* the client exemption by hand.
|
|
45
45
|
*/
|
|
46
46
|
get predicate() {
|
|
47
|
-
var
|
|
48
|
-
return ((
|
|
47
|
+
var e;
|
|
48
|
+
return ((e = this.filterBy) == null ? void 0 : e.predicate(this)) ?? [];
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Ask, and *keep* the question. The coordinator issues, consolidates, caches and re-runs it.
|
|
@@ -54,13 +54,13 @@ class R extends w {
|
|
|
54
54
|
* filters something the camera is still over the same rectangle, so the coordinator re-running this
|
|
55
55
|
* with a new predicate is precisely the query anybody would have written by hand.
|
|
56
56
|
*/
|
|
57
|
-
ask(
|
|
58
|
-
|
|
59
|
-
const s = new Promise((
|
|
60
|
-
var
|
|
61
|
-
(
|
|
57
|
+
ask(e) {
|
|
58
|
+
i(this, c, e);
|
|
59
|
+
const s = new Promise((d, q) => {
|
|
60
|
+
var f;
|
|
61
|
+
(f = l(this, n)) == null || f.reject(w("superseded: this read was re-aimed at a newer question")), i(this, n, { resolve: d, reject: q });
|
|
62
62
|
});
|
|
63
|
-
return l(this, h) ? this.requestQuery() : (
|
|
63
|
+
return l(this, h) ? this.requestQuery() : (i(this, h, !0), l(this, u).connect(this)), s;
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Where an answer goes when nobody asked for it — which is the page's filters moving.
|
|
@@ -69,30 +69,30 @@ class R extends w {
|
|
|
69
69
|
* cannot disagree: an answer settles the promise when one is waiting, and is reported here when
|
|
70
70
|
* none is.
|
|
71
71
|
*/
|
|
72
|
-
set onAnswer(
|
|
73
|
-
|
|
72
|
+
set onAnswer(e) {
|
|
73
|
+
i(this, a, e);
|
|
74
74
|
}
|
|
75
75
|
/** Let go: no more queries, and the coordinator stops walking us on every selection change. */
|
|
76
76
|
release() {
|
|
77
|
-
var
|
|
78
|
-
(
|
|
77
|
+
var e;
|
|
78
|
+
(e = l(this, n)) == null || e.reject(w("released: this read let go of the coordinator")), i(this, n, null), i(this, a, null), i(this, c, null), l(this, h) && (i(this, h, !1), l(this, u).disconnect(this));
|
|
79
79
|
}
|
|
80
|
-
query(
|
|
80
|
+
query(e = []) {
|
|
81
81
|
var s;
|
|
82
|
-
return ((s = l(this, c)) == null ? void 0 : s.call(this,
|
|
82
|
+
return ((s = l(this, c)) == null ? void 0 : s.call(this, e)) ?? null;
|
|
83
83
|
}
|
|
84
|
-
queryResult(
|
|
85
|
-
var
|
|
84
|
+
queryResult(e) {
|
|
85
|
+
var d;
|
|
86
86
|
const s = l(this, n);
|
|
87
|
-
return
|
|
87
|
+
return i(this, n, null), s ? s.resolve(e) : (d = l(this, a)) == null || d.call(this, e), this;
|
|
88
88
|
}
|
|
89
|
-
queryError(
|
|
89
|
+
queryError(e) {
|
|
90
90
|
const s = l(this, n);
|
|
91
|
-
return
|
|
91
|
+
return i(this, n, null), s && s.reject(e), this;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
u = new WeakMap(), c = new WeakMap(), n = new WeakMap(), a = new WeakMap(), h = new WeakMap();
|
|
95
95
|
export {
|
|
96
|
-
|
|
96
|
+
x as SliceRead
|
|
97
97
|
};
|
|
98
98
|
//# sourceMappingURL=slice-client.js.map
|
package/dist/slice-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slice-client.js","sources":["../src/slice-client.ts"],"sourcesContent":["\"use client\";\n\nimport { MosaicClient } from \"@kanzo-tech/mosaic\";\nimport type { Coordinator, FilterExpr, Selection } from \"@kanzo-tech/mosaic\";\nimport {
|
|
1
|
+
{"version":3,"file":"slice-client.js","sources":["../src/slice-client.ts"],"sourcesContent":["\"use client\";\n\nimport { MosaicClient } from \"@kanzo-tech/mosaic\";\nimport type { Coordinator, FilterExpr, Selection } from \"@kanzo-tech/mosaic\";\nimport { abortError } from \"./bounded\";\n\n/**\n * One read of a corpus, as a client of the page's coordinator.\n *\n * **A source used to query outside the protocol, and that was the whole defect.** `onceQuery`\n * connected a throwaway `MosaicClient` per query, took the answer and disconnected — a second query\n * path beside the one every chart on the page uses, and one that no selection could reach. So a\n * graph inside a crossfilter had to be joined to it from outside: something else asked which ids\n * survived, and the canvas painted grey over the ones that had not. Two round trips, a full scan and\n * a texture upload, to express a `WHERE` clause.\n *\n * A read is the same protocol a histogram uses, in the two directions it runs:\n *\n * `query(filter)` — the coordinator hands us the page's predicate and we build the SQL around it,\n * so what comes back is *what survives* rather than everything with a mask beside it.\n * `queryResult(data)` — the answer, whether we asked for it or the page's filters moved.\n *\n * **The second direction is the one that pays.** A selection change re-runs this read with the new\n * predicate and no camera involvement at all: the coordinator already walks its clients on every\n * selection update, so a graph that is one of them is re-queried the way a plot is. Nothing\n * subscribes to anything, and there is no window where the picture and the page's filters disagree.\n *\n * **Not `makeClient`.** That helper takes a `query` and a `queryResult` as options and is right for a\n * client with one standing question. A slice is two reads that have to arrive together, over a\n * question the camera rewrites — so what is needed is a handle the source can re-aim, which is a\n * class with a method rather than a closure fixed at construction.\n */\nexport class SliceRead extends MosaicClient {\n #coordinator: Coordinator;\n #build: ((filter: FilterExpr) => string) | null = null;\n #settle: { resolve: (data: unknown) => void; reject: (error: unknown) => void } | null = null;\n #onAnswer: ((data: unknown) => void) | null = null;\n #connected = false;\n\n /**\n * @param filterBy The crossfilter this read lives inside, if any. The unfiltered reads — how big\n * the corpus is, where it is, what its tile footers say — take none: those are facts about the\n * corpus rather than about what the page is looking at, and filtering them would make\n * \"20,000 of 1,000,000\" a fraction of itself.\n */\n constructor(coordinator: Coordinator, filterBy?: Selection) {\n super(filterBy);\n this.#coordinator = coordinator;\n }\n\n /**\n * Pre-aggregation cannot help here, and saying so costs a getter instead of a query.\n *\n * `preaggColumns` reaches the same \"no\" by calling `query()` and finding a string where it wanted a\n * `SelectQuery` — a slice is a CTE over window functions and is not expressible in the builder —\n * but it calls `query()` to find out, on every selection change. The claim is true rather than\n * defensive: the filter decides which rows are numbered, so it moves the groupby domain outright,\n * which is exactly what this flag is asked about.\n */\n override get filterStable(): boolean {\n return false;\n }\n\n /**\n * What the rest of the page is filtering by, right now.\n *\n * The same value the coordinator would hand `query()`. A source that needs the predicate *before*\n * it can build SQL — because it has to know which relation to point at, or because it is assembling\n * one answer out of two reads — asks here rather than reaching into the selection and re-deriving\n * the client exemption by hand.\n */\n get predicate(): FilterExpr {\n return this.filterBy?.predicate(this) ?? [];\n }\n\n /**\n * Ask, and *keep* the question. The coordinator issues, consolidates, caches and re-runs it.\n *\n * `build` is retained rather than consumed, because it is the standing question: after the page\n * filters something the camera is still over the same rectangle, so the coordinator re-running this\n * with a new predicate is precisely the query anybody would have written by hand.\n */\n ask(build: (filter: FilterExpr) => string): Promise<unknown> {\n this.#build = build;\n const answer = new Promise<unknown>((resolve, reject) => {\n // A superseded question is settled rather than dropped. The camera moves faster than DuckDB\n // answers, so the previous promise has a caller awaiting it; leaving it unsettled leaves that\n // caller's `finally` unrun and the loop reporting a query in flight for the rest of the session.\n //\n // An `AbortError`, because that is what it is: the older caller does not want this answer any\n // more, and there is no signal to reach for — nobody aborted it, a newer question overtook it.\n // It used to be a `SUPERSEDED` symbol of ours, which meant a caller had to import a name from\n // this package to tell a cancellation from a failure. `bounded.ts` carries that argument.\n this.#settle?.reject(abortError(\"superseded: this read was re-aimed at a newer question\"));\n this.#settle = { resolve, reject };\n });\n if (this.#connected) {\n this.requestQuery();\n } else {\n // Connected on first use rather than at construction, and that is what keeps a null question\n // out of the protocol: a connected client is one the coordinator may re-query on any selection\n // change, and before the first camera question there is nothing to re-query it *with*.\n // `connect` initializes, and initializing requests a query — so the opening read is issued by\n // the coordinator's own lifecycle rather than beside it.\n this.#connected = true;\n this.#coordinator.connect(this);\n }\n return answer;\n }\n\n /**\n * Where an answer goes when nobody asked for it — which is the page's filters moving.\n *\n * Told apart from a pull by which of the two is outstanding rather than by a flag, so the two\n * cannot disagree: an answer settles the promise when one is waiting, and is reported here when\n * none is.\n */\n set onAnswer(handle: ((data: unknown) => void) | null) {\n this.#onAnswer = handle;\n }\n\n /** Let go: no more queries, and the coordinator stops walking us on every selection change. */\n release(): void {\n this.#settle?.reject(abortError(\"released: this read let go of the coordinator\"));\n this.#settle = null;\n this.#onAnswer = null;\n this.#build = null;\n if (this.#connected) {\n this.#connected = false;\n this.#coordinator.disconnect(this);\n }\n }\n\n override query(filter: FilterExpr = []): string | null {\n return this.#build?.(filter) ?? null;\n }\n\n override queryResult(data: unknown): this {\n const settle = this.#settle;\n this.#settle = null;\n if (settle) settle.resolve(data);\n else this.#onAnswer?.(data);\n return this;\n }\n\n override queryError(error: Error): this {\n const settle = this.#settle;\n this.#settle = null;\n if (settle) settle.reject(error);\n return this;\n }\n}\n"],"names":["_coordinator"],"mappings":";;;;;;;;;AAgCO;AAAqC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcxC;AAbF;AACA;AACA;AACA;AACA;AAUE;AAAoB;AACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYE;AAAO;AACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAWE;AAAyC;AAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUE;AACA;;AASE;AAC0B;AAE5B;AAWO;AACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUE;AAAiB;AACnB;AAAA;;AAIE;AAMmC;AAErC;;AAGE;AAAgC;AAClC;;AAGE;AACA;AAGO;AACT;AAGE;AACA;AAEO;AAEX;AAtHEA;;;;"}
|