@kanzo-tech/graph 0.9.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} +8 -8
- 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} +2 -2
- 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/duck-source.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"duck-source.js","sources":["../src/duck-source.ts"],"sourcesContent":["\"use client\";\n\nimport { PAYLOAD_ADDRESS, PAYLOAD_COORDINATES, PAYLOAD_IDENTITY } from \"@fossil-lang/corpus\";\nimport type {\n Corpus,\n CorpusRelation,\n EdgeAddress,\n GapReason,\n ProjectionAddress,\n} from \"@fossil-lang/corpus\";\nimport { clausePoints, column, fillColumn, numbers } from \"@kanzo-tech/mosaic\";\nimport type { Coordinator, Engine, FilterExpr, Selection } from \"@kanzo-tech/mosaic\";\nimport type { BoundedSource, Slice, SliceRequest, Viewport } from \"./bounded\";\nimport { SliceRead } from \"./slice-client\";\nimport { denseOf, typeOf, vertexId, type VertexId } from \"./resident\";\n\n/**\n * The source: a corpus fossil wrote, read through DuckDB.\n *\n * On a subpath because Mosaic and fossil's reader are optional peers and this is the half that needs\n * them. The root barrel ships no source at all, which is the whole of what that split now means: a\n * host that installs neither gets the rendering surface and draws nothing.\n *\n * **There used to be two here and the second was neutral about storage.** `duckBoundedSource` took\n * two ordinary relations and the column names that made sense of them, and that neutrality earned\n * its keep once — it let bounded be measured against unbounded before anything committed to a layout\n * on disk. What it cost afterwards is the reason it is gone: a corpus already declares those names\n * in its manifest, so the second source was this side writing down what the other side owns, and the\n * two answered the same question in two dialects of the same SQL.\n *\n * **Every query in this file goes through a `SliceRead`, and there is no other path.** `onceQuery`\n * was the other one — a throwaway client per query, on this subpath, re-exported for the two\n * showcases that also read a relation directly. It is gone: a read the page's filters cannot reach\n * is a picture that disagrees with the page, and `slice-client.ts` carries what that cost.\n */\n\n/**\n * A DuckDB-backed source, and the one thing it can do that the render contract knows nothing about.\n *\n * `BoundedSource` says what a renderer needs: answer a bounded question. Publishing a selection is\n * the other direction of the same seam and it is Mosaic's, not the renderer's — so it lives on the\n * concrete type rather than on the contract, beside `watch`, which is on the contract because the\n * query loop is what has to act on it.\n */\nexport interface DuckSource extends BoundedSource {\n /**\n * The reader's own selection, as a clause the rest of the page filters by. `null` retracts it.\n *\n * **The graph is exempt from its own clause, and that is the whole difference from the greyout it\n * replaces.** While the canvas *faded* excluded rows it could take its own clause too — the row was\n * still drawn and still selectable, and the fade was the brush. A canvas that now draws what\n * survives would answer a lasso by deleting everything the reader did not lasso, which is not a\n * selection, it is a filter nobody asked for.\n */\n publish(vertices: readonly VertexId[] | null): void;\n}\n\n/**\n * The type index every vertex this source returns wears — **zero, because there is one of them.**\n *\n * A `dense_id` numbers within one vertex type, so an identity is the pair and the second half has to\n * come from somewhere. It used to be an option: the general source took a `typeIndex` because only\n * the caller knew which relation it had handed over, and a defaulted `0` would have let a second\n * relation ship the first one's identities with nothing raised. `openCorpus` draws **one** vertex\n * type — `vertexType` picks which, and it is numbered zero either way — so the option had one legal\n * argument and it is written here instead, once, where the reason fits beside it.\n *\n * **What would reverse it:** a canvas drawing two vertex types at once, which is what a multi-type\n * corpus asks for. Then the number comes back — off the manifest's own type ordering rather than off\n * a caller, because by then the corpus is what knows.\n */\nconst VERTEX_TYPE = 0;\n\ninterface Columns {\n id: string;\n /** `undefined` when the host did not ask to be able to name a vertex. */\n subject: string | undefined;\n x: string;\n y: string;\n /**\n * The categorical column, when a channel named one — **and `undefined` is not a missing value.**\n *\n * It used to default to `community`, in both sources, which is this side writing what the corpus\n * owns: a relation that has no such column answered `Referenced column \"community\" not found`, and\n * one that has a differently named cluster column was silently coloured by the wrong thing. Neither\n * failure is the caller's, and both were invented here.\n *\n * Unbound, every point is one colour — which is Plot's own answer to a mark with no `fill` channel,\n * and an honest picture rather than a guess.\n */\n category: string | undefined;\n size: string | undefined;\n source: string;\n target: string;\n}\n\n/**\n * The three reads a source makes, and which of them the page can filter.\n *\n * `points` and `links` carry the crossfilter; `meta` deliberately does not. How big the corpus is,\n * where it sits and what its tile footers say are facts about the corpus rather than about the\n * page's current question — and a `total()` that shrank with the filters would make the view's own\n * \"20,000 of 1,000,000\" a fraction of itself, which is the one number a bounded renderer owes its\n * reader honestly.\n */\ninterface Reads {\n points: SliceRead;\n links: SliceRead;\n meta: SliceRead;\n}\n\nfunction openReads(coordinator: Coordinator, filterBy?: Selection): Reads {\n return {\n points: new SliceRead(coordinator, filterBy),\n links: new SliceRead(coordinator, filterBy),\n meta: new SliceRead(coordinator),\n };\n}\n\n/**\n * The metadata reads, queued behind each other.\n *\n * One client answers one question at a time — a second `ask` supersedes the first — and `total()`,\n * `extent()` and the tile probing are issued by different effects with no ordering between them. A\n * queue rather than a client each, because they *already* run one at a time: DuckDB-WASM answers\n * over one connection, measured, so three clients would buy three registrations and no concurrency.\n */\nfunction metaAsker(read: SliceRead): (sql: string) => Promise<unknown> {\n let queue: Promise<unknown> = Promise.resolve();\n return (sql) => {\n const next = queue.then(() => read.ask(() => sql));\n queue = next.catch(() => undefined);\n return next;\n };\n}\n\n/**\n * The page's predicate, as SQL text.\n *\n * The reads here are CTEs over window functions rather than builder queries — `row_number()` over the\n * visible set is what makes a slice's links speak in buffer positions — so the predicate has to be\n * interpolated rather than handed to `Query.where`. Mosaic's expression nodes stringify to the same\n * SQL the builder would emit, which is what makes that safe rather than a re-implementation.\n */\nfunction predicateSql(filter: FilterExpr | undefined): string {\n if (filter == null) return \"\";\n const list = Array.isArray(filter) ? filter : [filter];\n const clauses = list.filter((node) => node != null).map((node) => String(node));\n return clauses.length > 0 ? clauses.map((c) => `(${c})`).join(\" AND \") : \"\";\n}\n\n/** Two predicates, conjoined, where an absent one contributes nothing rather than `AND TRUE`. */\nfunction both(left: string, right: string): string {\n if (!left) return right || \"TRUE\";\n if (!right) return left;\n return `(${left}) AND (${right})`;\n}\n\n/**\n * The re-indexing happens in SQL, and that is the whole trick.\n *\n * `row_number() - 1` over the visible set gives every returned point a position in the arrays about\n * to be built, so the edge query can join to it twice and hand back links that already speak in\n * those positions. No id→index map is constructed in JavaScript — which is the 148 ms `load()` spent\n * at 200,000 nodes, gone by construction rather than by optimisation.\n *\n * The `LIMIT` sits inside the CTE, so the numbering is over what survives it. Numbering first and\n * limiting after would hand out indices into an array that was never built.\n */\n/**\n * A rectangle as a SQL predicate, and an **unbounded** rectangle as no predicate at all.\n *\n * `shouldSlice` answers `false` for a graph that fits, and the loop then asks for everything — a\n * viewport whose edges are `±Infinity`. Interpolated, that reads `x BETWEEN -Infinity AND Infinity`,\n * and SQL has no infinity literal: DuckDB parses `Infinity` as a **column name** and fails with\n * `Referenced column \"Infinity\" not found`. So an open edge contributes no clause, and a rectangle\n * open on every side is `TRUE` — which is also the right plan, because a query that wants every row\n * has nothing to prune.\n */\nfunction bboxSql(c: Columns, view: Viewport): string {\n const bounds: [string, number, string][] = [\n [c.x, view.xMin, \">=\"],\n [c.x, view.xMax, \"<=\"],\n [c.y, view.yMin, \">=\"],\n [c.y, view.yMax, \"<=\"],\n ];\n const clauses = bounds\n .filter(([, value]) => Number.isFinite(value))\n .map(([column, value, op]) => `${column} ${op} ${value}`);\n return clauses.length > 0 ? clauses.join(\" AND \") : \"TRUE\";\n}\n\n/**\n * How far apart the sampled ids are — one every `ceil(matched / limit)`.\n *\n * **In SQL rather than in JavaScript because the number it divides is only known inside the query.**\n * `matched` is a window aggregate over the rows the `WHERE` kept, so a caller wanting to compute\n * this outside would have to count first and slice second — two round trips down a connection that\n * answers one at a time, which is the shape `BENCHMARKS.md` records as a hung tab rather than a slow\n * one. As a column reference it costs the pass that was being made anyway.\n *\n * `greatest(1, …)` because an empty window makes the divisor zero, and a modulo by zero is an error\n * rather than an empty answer. At `matched <= limit` it is exactly 1 and `id % 1 = 0` keeps every\n * row: a window that fits is not sampled, it is returned.\n */\nconst strideSql = (limit: number) => `greatest(1, CAST(ceil(matched / ${limit}.0) AS BIGINT))`;\n\n/**\n * The visible set: what the rectangle matched, and the sample of it that gets drawn.\n *\n * **Two CTEs, and the second one is the whole of the far view.** `pool` is every row the predicate\n * kept, carrying `count(*) OVER ()` — a window function is evaluated over everything the `WHERE`\n * kept and `LIMIT` applies after it, so that column is the number that *matched* rather than the\n * number returned. It is what deleted the third query: `SELECT count(*) FROM … WHERE <the same\n * predicate>` was a second scan to learn a number the first scan already had to compute.\n *\n * `vis` then keeps one row in `stride`, **striding over the id rather than taking the front of the\n * ordering**, and that is the difference between a picture of the window and a picture of one corner\n * of it. A corpus numbers `dense_id` along the Morton curve, so every `s`-th id is a spatially\n * stratified sample; the same `LIMIT` with no stride returns a contiguous run of the curve, which is\n * a sub-region. Measured against the truth at screen resolution, L1@8px over blocks of eight pixels:\n * a stride sample of 20,000 scores 0.167 / 0.240 / 0.269 at 200k / 1M / 5M against a uniform null of\n * 1.044 / 0.829 / 0.731 — see `/docs/design/graph`.\n *\n * @param matched Whether to project the pre-sample count out to the caller.\n *\n * It rides on the points read only. The links read builds the same CTEs to join against and never\n * looks at the column — but it does compute it, because the stride is a function of it and both\n * reads have to select the *same* rows or a slice would draw edges to vertices it did not return.\n */\nfunction visibleCte(\n nodes: string,\n c: Columns,\n where: string,\n limit: number,\n matched = true,\n): string {\n const size = c.size ? `, ${c.size} AS size` : \"\";\n // Selected in the CTE rather than joined back afterwards: the numbering is over what survives the\n // LIMIT, and a second pass keyed on `local` would be a second scan to fetch a column the first one\n // was already standing on.\n const subject = c.subject ? `, ${c.subject} AS subject` : \"\";\n // No categorical binding, no ranking: a literal zero is the ordinal every point wears, and the\n // scale hands that one colour. Ranking a column nobody named is how a default column gets invented.\n // Ranked over the sample rather than over the window, so the ordinals are contiguous across what\n // is actually drawn — which is what the colour scale is handed.\n const category = c.category\n ? `(dense_rank() OVER (ORDER BY cat) - 1)::INTEGER AS category`\n : \"0::INTEGER AS category\";\n return `WITH pool AS (\n SELECT ${c.id} AS id, ${c.x} AS x, ${c.y} AS y${size}${subject}${\n c.category ? `, ${c.category} AS cat` : \"\"\n },\n count(*) OVER () AS matched\n FROM ${nodes}\n WHERE ${where}\n ), vis AS (\n SELECT id, x, y${c.size ? \", size\" : \"\"}${c.subject ? \", subject\" : \"\"}${\n matched ? \", matched\" : \"\"\n },\n ${category},\n (row_number() OVER (ORDER BY id) - 1)::INTEGER AS local\n FROM pool\n WHERE id % ${strideSql(limit)} = 0\n LIMIT ${limit}\n )`;\n}\n\n/**\n * The shortest edge worth a row, as a predicate over the two endpoints — **squared, and on purpose.**\n *\n * A distance is compared against a threshold, and squaring both sides removes a `sqrt` per row from\n * a predicate evaluated once per candidate edge. It changes no answer: both sides are non-negative.\n *\n * `undefined` when the caller said nothing about resolution, and then there is no predicate at all\n * rather than a permissive one — a request with no canvas behind it (`EVERYTHING`) has no pixels to\n * measure three of.\n */\nfunction longEnough(\n a: string,\n b: string,\n perPixel: number | undefined,\n minLinkPixels: number,\n): string {\n if (perPixel === undefined || !Number.isFinite(perPixel) || perPixel <= 0) return \"\";\n const floor = minLinkPixels * perPixel;\n return `(${a}.x - ${b}.x) * (${a}.x - ${b}.x) + (${a}.y - ${b}.y) * (${a}.y - ${b}.y) >= ${floor * floor}`;\n}\n\n/**\n * The far ends, and the edges that reach them — **out of bytes the reader already fetched.**\n *\n * An edge with one end outside the rectangle is dropped today, and that loses 19.31% / 31.94% /\n * 28.92% of the edges incident to a window at 200k / 1M / 5M; 7,930 of 20,000 vertices carry at least\n * one at five million. What was missing was never the edge row — a window reads the `by_source` tiles\n * of every vertex it draws, so the row is in hand — it was **a position to draw the far end at**.\n *\n * **And a tile answers that for free.** A tile is 4,096 rows of a Morton-ordered relation and its\n * bounding box is far wider than the rows the rectangle keeps, so the vertices just outside the\n * window are usually in a tile the window already fetched. Measured over five windows of\n * `docs/public/bench/1000000`, 2026-08-19: relaxing the join from *inside the rectangle* to *inside\n * the tiles that were read* takes the drawn edges of five windows from 616,885 to 781,562 of 906,337\n * incident — **56.9% of everything the reader was losing, at no request, no query and no byte.**\n *\n * **The tile boundary is also a distance filter, and that is what settles the drawing.** Over every\n * far end a window loses, the median sits 1.64 semi-widths out and the worst 47.1 — which is why a\n * stub clipped to the viewport is a lie: nothing distinguishes 1.1 from 47. The far ends a held tile\n * can answer are the near ones: median 1.11–1.85 semi-widths, 90th percentile 1.29–2.60, worst\n * **6.74**, against 7.09–16.91 for the full reachable set. So the honest picture and the free one are\n * the same picture, and there is no trade to make: the anchors are drawn where the vertices are, and\n * the long tail stays undrawn because its bytes are not here — not because we decided.\n *\n * @param held The relation whose rows the reader is holding — the tiles a corpus fetched for this\n * window, which is the same relation the marks were read from. **Only an addressed source can name\n * one**, and that is why there is no longer a branch here for a source that cannot: over an\n * ordinary relation `held` would be the whole node table, and the join would scan the corpus twice\n * per camera move — the unbounded pattern wearing a bounded interface.\n */\nfunction anchorCte(\n held: string,\n edges: string,\n c: Columns,\n spatial: string,\n filter: string,\n perPixel: number | undefined,\n minLinkPixels: number,\n): string {\n const outside = both(`NOT (${spatial})`, filter);\n const long = longEnough(\"a\", \"b\", perPixel, minLinkPixels);\n return `, out AS (\n SELECT ${c.id} AS id, ${c.x} AS x, ${c.y} AS y FROM ${held} WHERE ${outside}\n ), reach AS (\n SELECT id, x, y FROM vis UNION ALL SELECT id, x, y FROM out\n ), span AS (\n SELECT sv.local AS src, tv.local AS dst, a.id AS src_id, b.id AS dst_id\n FROM ${edges} e\n JOIN reach a ON e.${c.source} = a.id\n JOIN reach b ON e.${c.target} = b.id\n LEFT JOIN vis sv ON sv.id = a.id\n LEFT JOIN vis tv ON tv.id = b.id\n WHERE (sv.id IS NOT NULL OR tv.id IS NOT NULL)${long ? ` AND ${long}` : \"\"}\n ), anchor AS (\n SELECT o.id, o.x, o.y,\n ((SELECT count(*) FROM vis) + row_number() OVER (ORDER BY o.id) - 1)::INTEGER AS local\n FROM out o\n WHERE o.id IN (SELECT src_id FROM span WHERE src IS NULL\n UNION SELECT dst_id FROM span WHERE dst IS NULL)\n )`;\n}\n\n/**\n * A question, in the two halves it is asked in and the one place they are put back together.\n *\n * The reads run through the coordinator, so the *same* pair of SQL builders serves both directions:\n * the camera pulling an answer, and the page's filters pushing one. `assemble` is therefore a\n * function of the two results and of nothing else — no closure over which of the two paths asked,\n * because a slice that came back because somebody brushed a histogram is the same slice.\n */\ninterface Plan {\n points: (filter: FilterExpr) => string;\n links: (filter: FilterExpr) => string;\n assemble: (points: unknown, links: unknown) => Slice;\n}\n\n/**\n * The one plan there is: a rectangle, its sample, and the edges both of whose ends survived it.\n *\n * It was called `detail` because it was one of two, and the other one — a `GROUP BY` over a\n * categorical column, one super-node per group — is gone. There is no mode to be in.\n */\nfunction region(\n nodes: string,\n edges: string,\n c: Columns,\n view: Viewport,\n limit: number,\n pinned: VertexId[] | undefined,\n perPixel: number | undefined,\n minLinkPixels: number,\n): Plan {\n const bbox = bboxSql(c, view);\n // A dragged node is drawn where the reader dropped it and indexed where it always was, so the\n // rectangle cannot find it. Riding along in the predicate is what keeps it on screen — and it\n // stays a predicate rather than a second query so the numbering still covers everything returned.\n //\n // Only this relation's own vertices: a pinned set spans the whole canvas, and asking one node\n // table for another type's dense ids returns the wrong rows rather than none. `VERTEX_TYPE` is the\n // whole of what \"this relation\" means here — see the constant.\n const mine = (pinned ?? []).filter((v) => typeOf(v) === VERTEX_TYPE).map(denseOf);\n const pins = mine.length > 0 ? ` OR ${c.id} IN (${mine.join(\",\")})` : \"\";\n const spatial = `(${bbox})${pins}`;\n /**\n * The page's predicate outside the pin, not inside it.\n *\n * A pin says *where to look*; the filters say *what exists*. Written the other way round —\n * `bbox AND filter OR pinned` — a pinned node would survive a filter that excludes it, and the\n * canvas would draw a vertex the rest of the page has agreed is not there.\n */\n const where = (filter: FilterExpr) => both(spatial, predicateSql(filter));\n const size = c.size ? \", size\" : \"\";\n const subject = c.subject ? \", subject\" : \"\";\n // The relation the marks were read from *is* what the reader is holding, so the anchor CTE is\n // unconditional: there is no source left that fetches a window without also fetching the tiles\n // around it.\n const anchors = (filter: FilterExpr) =>\n anchorCte(nodes, edges, c, spatial, predicateSql(filter), perPixel, minLinkPixels);\n\n return {\n /**\n * The marks, then the anchors, in one answer — because they are one buffer.\n *\n * `ORDER BY local` is load-bearing rather than tidy: `local` runs `0..marks-1` over the sample\n * and continues past it over the anchors, so ordering by it puts every mark before every anchor\n * and makes `marks` a prefix length. `matched` is then still read off row zero.\n */\n points: (filter) =>\n `${visibleCte(nodes, c, where(filter), limit)}${anchors(filter)}\n SELECT local, id, x, y, category, matched, TRUE AS mark${size}${subject} FROM vis\n UNION ALL\n SELECT local, id, x, y, 0::INTEGER, NULL::BIGINT, FALSE AS mark${\n c.size ? \", CAST(NULL AS DOUBLE)\" : \"\"\n }${c.subject ? \", CAST(NULL AS VARCHAR)\" : \"\"} FROM anchor\n ORDER BY local`,\n /**\n * One end drawn and both ends positioned.\n *\n * There was a second form here — both ends drawn, edges leaving the window dropped — for a source\n * that fetched no bytes beyond the rectangle and therefore had no position to put a far end at.\n * It went with that source, and `longEnough` with it — the length predicate lives in `anchorCte`\n * now, on the same span, which is where it has to sit once the join runs over the held rows\n * rather than over the visible ones.\n */\n links: (filter) =>\n `${visibleCte(nodes, c, where(filter), limit, false)}${anchors(filter)}\n SELECT coalesce(sp.src, sa.local) AS src, coalesce(sp.dst, da.local) AS dst\n FROM span sp\n LEFT JOIN anchor sa ON sa.id = sp.src_id\n LEFT JOIN anchor da ON da.id = sp.dst_id`,\n assemble: (points, links) => ({\n /**\n * How many matched, separately from how many came back.\n *\n * Without it the view cannot tell a reader \"there is more here than I am showing you\", and a\n * truncated slice looks exactly like a complete one — which is the failure this whole branch\n * has been about. Read off the first row rather than asked for: `matched` is constant down the\n * column, and an empty answer has no row and no matches, which agree.\n */\n n: Number(numbers(points, \"matched\")[0] ?? 0),\n ...arrays(points, links, c.size ? \"size\" : undefined, c.subject !== undefined),\n }),\n };\n}\n\n/**\n * The half of a source that runs a [`Plan`], and the half that answers when nobody asked.\n *\n * Both sources need exactly this and neither should own a second copy of it — which is why it is a\n * function over the reads rather than two blocks of the same bookkeeping. What it holds is the\n * *standing* plan: the last question the camera put, kept so an answer arriving because the page\n * filtered something can be put back together the same way.\n */\nfunction watcher(reads: Reads) {\n let standing: Plan | null = null;\n let listener: ((slice: Slice) => void) | null = null;\n /** The half-answers of a push, waiting for their sibling. */\n const landed = new Map<\"points\" | \"links\", unknown>();\n\n const arrived = (half: \"points\" | \"links\") => (data: unknown) => {\n if (!standing || !listener) return;\n landed.set(half, data);\n if (landed.size < 2) return;\n const points = landed.get(\"points\");\n const links = landed.get(\"links\");\n landed.clear();\n listener(standing.assemble(points, links));\n };\n\n return {\n api: {\n /**\n * Say when the answer changes for a reason the camera cannot see.\n *\n * The reason it hands over a whole `Slice` rather than a nudge to ask again: the coordinator\n * has *already* re-run both reads with the new predicate by the time we hear about it. Asking\n * again would run the same two queries a second time to learn what is in hand.\n */\n watch(answered: (slice: Slice) => void): () => void {\n listener = answered;\n reads.points.onAnswer = arrived(\"points\");\n reads.links.onAnswer = arrived(\"links\");\n return () => {\n listener = null;\n landed.clear();\n standing = null;\n reads.points.release();\n reads.links.release();\n reads.meta.release();\n };\n },\n },\n async run(plan: Plan): Promise<Slice> {\n standing = plan;\n // Cleared because these two are halves of the *previous* question: keeping one would pair a\n // stale rectangle's points with the new rectangle's links the next time the page filters.\n landed.clear();\n const [points, links] = await Promise.all([\n reads.points.ask(plan.points),\n reads.links.ask(plan.links),\n ]);\n return plan.assemble(points, links);\n },\n };\n}\n\n/**\n * The reader's own gesture, as a clause — and the graph exempted from it.\n *\n * `clausePoints` defaults `clients` to the clause's source when that source is itself a client, which\n * is the exemption a crossfilter is built on. Here the source is a plain object and there are two\n * clients to exempt, so the set is written out: a lasso must filter the page's charts and leave the\n * canvas showing what the reader lassoed *in context*, rather than deleting everything else.\n */\nfunction publishSelection(\n reads: Reads,\n filterBy: Selection | undefined,\n idField: string,\n vertices: readonly VertexId[] | null,\n): void {\n if (!filterBy) return;\n filterBy.update(\n clausePoints([idField], vertices?.map((vertex) => [denseOf(vertex)]), {\n source: reads.points,\n clients: new Set([reads.points, reads.links]),\n }),\n );\n}\n\n/**\n * Arrow columns to the parallel typed arrays the renderer takes — sized once, filled in place.\n *\n * `fillColumn` rather than `numbers`: Arrow already hands back a typed buffer, and the obvious route\n * through `Array.from(...).map(Number)` allocates two full-length boxed arrays on the way to a third\n * that was the actual destination. Three copies to move nothing. Here the point buffers are sized\n * against the query's own `LIMIT` and each column is written straight into its stride, so `x` and\n * `y` interleave with no seam between them and no intermediate at all.\n */\nfunction arrays(\n points: unknown,\n links: unknown,\n sizeField?: string,\n withSubjects = false,\n): Omit<Slice, \"n\"> {\n // Sized from the answer rather than from the request's `limit`, which it used to be: an answer\n // carries the window's marks *and* the anchors the edges leaving it end at, so `limit` is no longer\n // an upper bound on the rows. Under-sizing here would drop the anchors silently and leave every\n // link that pointed at one indexing past the buffer.\n const rows = countOf(points, \"x\");\n const positions = new Float32Array(rows * 2);\n const n = fillColumn(points, \"x\", positions, 0, 2);\n fillColumn(points, \"y\", positions, 1, 2);\n\n // Where the marks stop. `mark` is `TRUE` down the sample and `FALSE` down the anchors, and the\n // query orders by `local`, so this is a prefix length rather than a count — which is what lets\n // `residentOf` and `buffers` treat \"is this a mark\" as an index comparison.\n const marked = new Int32Array(rows);\n fillColumn(points, \"mark\", marked);\n let marks = 0;\n while (marks < n && marked[marks] !== 0) marks++;\n\n // The dense ids land in a scratch and are widened into identities as they are copied across.\n //\n // In place, into the destination, would be better and is not available: `fillColumn` writes\n // `Number(…)`, and a `BigUint64Array` element takes a `bigint` only — assigning a `number` to one\n // throws rather than coercing. That refusal is the same guarantee this whole change is for, so the\n // extra `n`-long buffer is the price of the boundary being enforced by the runtime and not by us.\n const dense = new Float64Array(n);\n fillColumn(points, \"id\", dense);\n const vertices = new BigUint64Array(n);\n for (let i = 0; i < n; i++) vertices[i] = vertexId(VERTEX_TYPE, dense[i] as number);\n const categories = new Uint16Array(n);\n fillColumn(points, \"category\", categories);\n\n // `column` rather than `fillColumn`: an IRI is a string, so there is no typed buffer to write\n // into and no interleaving to express. It is the one thing a slice carries that never reaches the\n // GPU, which is why asking for it is a decision rather than a default.\n const subjects = withSubjects ? (column(points, \"subject\") as string[]) : undefined;\n\n let sizes: Float32Array | undefined;\n if (sizeField) {\n sizes = new Float32Array(n);\n fillColumn(points, sizeField, sizes);\n }\n\n // The edge count is not bounded by the point limit, so it is asked for rather than assumed.\n const edgeCount = countOf(links, \"src\");\n const edges = new Float32Array(edgeCount * 2);\n const wrote = fillColumn(links, \"src\", edges, 0, 2);\n fillColumn(links, \"dst\", edges, 1, 2);\n\n return {\n marks,\n vertices,\n subjects,\n positions: positions.subarray(0, n * 2),\n links: edges.subarray(0, wrote * 2),\n categories,\n sizes,\n };\n}\n\n/** A row count for a result that does not advertise one, without materialising the rows. */\nfunction countOf(rows: unknown, field: string): number {\n const advertised = (rows as { numRows?: number } | null)?.numRows;\n if (typeof advertised === \"number\") return advertised;\n const child = (rows as { getChild?: (f: string) => { length: number } | null })?.getChild?.(field);\n if (child) return child.length;\n return Array.from(rows as Iterable<unknown>).length;\n}\n\n/**\n * A corpus that fossil wrote, read by address — **and the addressing is fossil's.**\n *\n * **The five things a call site used to know, and now does not.** Drawing a corpus meant deriving\n * the chunk URLs from a `chunk_size` copied by hand, knowing how a tile is named, knowing what the\n * edge directory is called, knowing GraphAr's column names, and knowing that a glob cannot work over\n * a plain HTTP origin because there is no listing. Five conventions and about forty lines, none of\n * it the business of something that wants to draw a graph. `/docs/design/graph` carries the\n * argument; the copied `chunk_size` carries the evidence, because it went stale and read\n * a fraction of a corpus in silence for as long as it did.\n *\n * The consumer holds two things: **the corpus fossil opened, and the page's engine.**\n *\n * ```ts\n * const e = await engine();\n * const corpus = await open(\"/bench/1000000\", { query: e.query });\n * const { source } = await openCorpus({ corpus, engine: e });\n * ```\n *\n * **And this side no longer knows the conventions either, which is the change.** It used to remove\n * that defect for its callers by committing it one level down — a YAML line-scanner, a\n * `chunk{k}.parquet` spelling, a `by_source/tile{k}.parquet` spelling and a `HEAD`-probing search\n * for a tile count — and by the time those were deleted all four were **wrong**: fossil writes\n * `container: rowgroups`, one `tiles.parquet` whose row groups are the tiles, and `vertex_count`\n * had been in the manifest the whole time the search was probing for it. Fossil's `open` is\n * `fossil_graph::plan` compiled to wasm32: the arithmetic the native reader runs, not a second\n * implementation of it that agrees until it does not.\n *\n * **Addressed, not queried.** The manifests and the per-tile boxes are read once and kept; after\n * that a camera move is arithmetic over boxes and a list of URLs. There is deliberately no request\n * on the path between the camera moving and a URL being computable — the moment there is one, this\n * has become the `viewport` verb fossil deleted.\n *\n * **What it is not.** It takes no column names and no type index. Those come from the manifest or\n * they do not come. There used to be a second source here for the case this is not — an arbitrary\n * relation with `x`/`y` that nobody wrote as a corpus — and taking `idField` here would have been\n * that one with extra steps. It is gone, so the rule is simpler than the guard against it: a column\n * name reaching this door is a name the manifest should have carried.\n */\n\nexport interface OpenCorpusOptions {\n /**\n * The corpus, as fossil's `open` answered it — **opened by the host, and once.**\n *\n * This door used to open one itself from a `dest`, a `readText` and a `wasm`, which made it the\n * third open of the same corpus on a keasy discover visit: the host had already opened it to\n * query it, and opened it again only so that this could. Opening is the host's, because what it\n * takes is the host's — where the corpus is, and what signs it. Drawing takes the result.\n */\n corpus: Corpus;\n /**\n * The page's engine — `engine()` from `@kanzo-tech/mosaic`. The canvas reads its tiles and\n * publishes its clauses through the coordinator; the files are the ones the corpus already made\n * readable, by the names its addressing gives them.\n */\n engine: Pick<Engine, \"coordinator\">;\n /**\n * The crossfilter this graph draws inside — the same `Selection` the page's charts filter by.\n *\n * Given, the predicate rides in the slice query and the canvas draws what survives.\n */\n filterBy?: Selection;\n /**\n * Which vertex type to draw, when a corpus carries more than one.\n *\n * Defaults to the first the manifest names. A corpus of one type never passes it; a corpus of\n * several has to, because *which graph do you mean* is not a question a reader can answer.\n */\n vertexType?: string;\n /**\n * Read the identity column as well. **Off by default, and the same trade as everywhere else:**\n * `subject` costs about twice the drawing tile, so the drawing path carries addresses and a host\n * asks for names when something has to be *named* rather than painted.\n */\n subjects?: boolean;\n}\n\n/** A relation this source draws: its address, and the adjacency it reads it from. */\ninterface DrawnRelation {\n readonly address: EdgeAddress;\n /** The source-ordered orientation — the CSR one, which is the drawing read. */\n readonly adjacency: ProjectionAddress;\n}\n\n/** One tile's bounding box, from the footer. A tile with no `x`/`y` statistics is not in the list. */\ninterface TileBox {\n tile: number;\n x0: number;\n x1: number;\n y0: number;\n y1: number;\n}\n\n/**\n * An opened corpus: the half that **draws** and the half that **answers**.\n *\n * A host needs both over the same bytes and they are not the same access. The canvas reads tiles by\n * address — a handful of files per camera move, chosen from the footer's boxes, with no query. A\n * chart, a crossfilter clause or a verb reads the *relation*: every row, by column name, in SQL.\n * Hiding the URLs behind `source` is right for the first and leaves the second with nothing to\n * query, so this hands on the names fossil's verbs already query the corpus by.\n */\nexport interface OpenedCorpus {\n /**\n * For the canvas: `<GraphCanvas source={…}>`. Reads tiles, never the whole relation.\n *\n * A `DuckSource`, and `CorpusSource` is gone with the reason it existed: `extent()` was declared\n * there because a source over an unlaid-out relation has no answer to it, and *optional on the\n * base contract* says that better than a second interface — a relation with `x`/`y` has an extent\n * too, and it was the one host that could not frame its opening view.\n */\n source: DuckSource;\n /**\n * The vertex relation, as fossil registered it — qualified by the corpus's catalog.\n *\n * Every column the manifest declares, including the corpus' own properties — so a clause a chart\n * publishes over `kind` or `region` lands here with no translation, which is what makes one\n * crossfilter serve the canvas and the charts.\n */\n nodes: string;\n /**\n * The source-ordered edge relations this canvas draws, one fossil relation each.\n *\n * **A list, because a corpus declares a list.** This was one name, picked with `.find` over the\n * relations whose source is this type — so a corpus declaring two edge labels registered the\n * first and dropped the second with nothing raised, and a chart over `corpus_Person_edges` was a\n * chart over half the graph. `frame` on the other side takes every one of them\n * (`packages/corpus/src/corpus.ts`, `edges.filter((e) => e.srcType === address.type)`), and\n * fossil's own `verbs()` registers a view per relation under `{src}_{edge}_{dst}` rather than\n * unioning them — which is also what keeps two relations' differing property columns from having\n * to agree on one schema.\n *\n * Empty when the corpus declares no relation this source can draw; {@link OpenedCorpus.undrawn}\n * is then where the ones it declared went.\n */\n edges: readonly EdgeRelation[];\n /**\n * The relations incident to this type that the canvas does **not** read, and why.\n *\n * A single-type canvas can draw a relation only where *both* endpoints are numbered in its own\n * `dense_id` space. The rest are dropped from the read rather than mixed into it, and reported\n * here rather than dropped in silence.\n */\n undrawn: readonly UndrawnRelation[];\n}\n\n/** One edge relation of the drawn type, and the relation fossil registered its adjacency as. */\nexport interface EdgeRelation {\n readonly edgeType: string;\n readonly srcType: string;\n readonly dstType: string;\n /** Fossil's relation, qualified by the corpus's catalog — its `CorpusRelation.sql`. */\n readonly view: string;\n}\n\n/**\n * One relation incident to the drawn type that this source cannot read, and why.\n *\n * **The reason is fossil's {@link GapReason} and not a second spelling of it** — `other-space` when\n * an endpoint is another vertex type, so the `dense_id` on that side numbers a different set of\n * vertices and this source holds no coordinates for any of them; `not-declared` when both endpoints\n * are this type and the corpus publishes no source-ordered adjacency to read. The two the drawing\n * read can produce; the third, `not-requested`, is `tilesFor`'s and never reaches here.\n *\n * What this adds to a `Gap` is the endpoint pair, which a `Gap` does not carry: it names a relation\n * by label and orientation, and a corpus may declare two relations under one label.\n */\nexport interface UndrawnRelation {\n readonly edgeType: string;\n readonly srcType: string;\n readonly dstType: string;\n readonly reason: GapReason;\n}\n\nexport async function openCorpus(options: OpenCorpusOptions): Promise<OpenedCorpus> {\n const { corpus, engine, filterBy, subjects = false, vertexType } = options;\n const { addressing } = corpus;\n\n const reads = openReads(engine.coordinator, filterBy);\n const meta = metaAsker(reads.meta);\n const watching = watcher(reads);\n\n const type = addressing.vertexType(vertexType);\n /**\n * Which relations this canvas may draw, and which incident ones it may not — **asked, not\n * derived.**\n *\n * A picture is one type's `dense_id` space, so a relation that LEAVES the type has its far ends\n * numbered in another type's, both spaces are dense from zero, and `BIGINT` compares against\n * `BIGINT` without complaining: the join matches and draws a line between two vertices with\n * nothing between them. The rule that refuses it used to be written here too — a filter over\n * `incident` on `srcType`/`dstType` plus a null check on the source-ordered adjacency — which was\n * the second copy of a rule that belongs to the addressing. It is `ReadPlan::drawing`\n * (`crates/fossil-graph/src/plan.rs`), in Rust, said once, and this is the call.\n *\n * **Not `tilesFor`, which answers the other question.** Its `edgeUrls` is every relation whose\n * *source* is this type, cross-type ones included — right for incidence, unusable for a picture.\n * Confusing the two is the bug this call closes.\n *\n * `by_target` is not asked for and its absence is reported rather than hidden: a window's\n * drawable edges all have their source on screen, so the source-aligned tiles are complete for\n * drawing and incomplete for incidence. `tilesFor` says which below, in `gaps`.\n */\n const drawing = addressing.drawing(type.type);\n const drawn: DrawnRelation[] = drawing.relations.map((address) => ({\n address,\n // Never null on a drawn relation: a declared source-ordered adjacency is what `drawing` admits\n // one for, and `not-declared` is why the others are in `undrawn` instead.\n adjacency: address.adjacency(\"src\") as ProjectionAddress,\n }));\n const adjacencies = drawn.map((relation) => relation.adjacency);\n /**\n * The rejected ones, with their endpoints — **the reasons are fossil's, the endpoints are the\n * corpus's, and neither is a judgement of ours.**\n *\n * The two lists are one partition of `incident` in declaration order: fossil walks the plan's\n * edges once and puts each incident one in `relations` or in `undrawn`, so the incident relations\n * missing from the first are the gaps of the second, in order. That join is here because a `Gap`\n * names a relation by label and orientation, and a label does not identify one — two relations\n * may share it — so the endpoint types cannot be read back off the gap alone.\n */\n const undrawn: UndrawnRelation[] = addressing\n .incident(type.type)\n .filter((edge) => !drawing.relations.includes(edge))\n .map((edge, index) => ({\n edgeType: edge.edgeType,\n srcType: edge.srcType,\n dstType: edge.dstType,\n reason: drawing.undrawn[index]!.reason,\n }));\n\n /**\n * The boxes, and the one query this source makes that is not a slice.\n *\n * Read on first use rather than in the factory: a host that constructs a source and never draws\n * should not pay for it, and the cost is a footer read over the payload. Kept forever after —\n * tiles are precomputed and their boxes cannot move without the corpus being rewritten.\n *\n * **Held as the promise rather than as the answer**, which the move to one shared metadata client\n * forced and which was a latent defect before it: `total()` and `extent()` are called by different\n * effects with nothing ordering them, so two loads used to run concurrently and probe the whole\n * tile range twice.\n */\n let loading: Promise<TileBox[]> | null = null;\n\n /**\n * Where the payload is, as the list of files that hold it.\n *\n * One file per tile under `container: files`, one file in total under `rowgroups` — and this side\n * does not know or care which, because the address is asked for rather than composed. Throws when\n * the manifest declares no `vertex_count`, which is the one absence that makes a corpus\n * un-enumerable.\n */\n const payloadFiles = type.files();\n /** A URL list as a SQL list literal. Every read below composes one and none of them composes a URL. */\n const quoted = (urls: readonly string[]) =>\n urls.map((url) => `'${url.replace(/'/g, \"''\")}'`).join(\", \");\n\n /**\n * The boxes, from the footer — **which tile a row group is, asked of the addressing.**\n *\n * `parquet_metadata` reports a `file_name` and a `row_group_id`, and which of the two names the\n * tile is the container's business: under `rowgroups` row group `k` IS tile `k`; under `files`\n * the file is, and its row groups are the writer's business, so their boxes are merged. This used\n * to read the tile out of the URL with `regexp_extract(file_name, 'chunk(\\d+)')` — one\n * container's spelling hard-coded into a query, and `NULL` for every row of a corpus fossil\n * writes today.\n *\n * `min_value`/`max_value`, never `min`/`max`. Parquet's original statistics fields are defined by\n * *signed* byte comparison, which is meaningless for an unsigned column — a writer that gets this\n * right leaves them empty. A reader that only knows the deprecated pair concludes the footer\n * carries no box for the column the whole address is built on. `coalesce` keeps the float columns\n * working either way.\n */\n function load(): Promise<TileBox[]> {\n loading ??= (async () => {\n const files = quoted(payloadFiles);\n const [xCol, yCol] = PAYLOAD_COORDINATES;\n // Which of `file_name` and `row_group_id` names the tile, as an expression rather than as a\n // branch in JavaScript: the grouping has to happen where the rows are either way.\n const tile =\n addressing.container === \"rowgroups\"\n ? \"row_group_id\"\n : `list_position([${files}], file_name) - 1`;\n const stats = await meta(\n `SELECT ${tile} AS tile,\n min(CASE WHEN path_in_schema = '${xCol}' THEN coalesce(stats_min_value, stats_min)::DOUBLE END) AS x0,\n max(CASE WHEN path_in_schema = '${xCol}' THEN coalesce(stats_max_value, stats_max)::DOUBLE END) AS x1,\n min(CASE WHEN path_in_schema = '${yCol}' THEN coalesce(stats_min_value, stats_min)::DOUBLE END) AS y0,\n max(CASE WHEN path_in_schema = '${yCol}' THEN coalesce(stats_max_value, stats_max)::DOUBLE END) AS y1\n FROM parquet_metadata([${files}])\n WHERE path_in_schema IN ('${xCol}', '${yCol}') GROUP BY 1 ORDER BY 1`,\n );\n const tiles = numbers(stats, \"tile\");\n const [x0, x1, y0, y1] = [\"x0\", \"x1\", \"y0\", \"y1\"].map((f) => numbers(stats, f));\n return tiles.map((t, i) => ({\n tile: t as number,\n x0: x0?.[i] as number,\n x1: x1?.[i] as number,\n y0: y0?.[i] as number,\n y1: y1?.[i] as number,\n }));\n })();\n return loading;\n }\n\n /** The tiles a rectangle touches. Pure — this is the whole of the selection, and it makes no call. */\n function intersecting(all: TileBox[], view: Viewport): number[] {\n return all\n .filter((b) => b.x1 >= view.xMin && b.x0 <= view.xMax && b.y1 >= view.yMin && b.y0 <= view.yMax)\n .map((b) => b.tile);\n }\n\n /**\n * Everything the corpus fixes, and nothing it does not — **by ROLE, not by name.**\n *\n * The address, the identity and the coordinates are facts of the format, and the names they are\n * written under come off `@fossil-lang/corpus`'s generated column table rather than four string\n * literals here. The endpoint columns come off the adjacency's own address. What colours and what\n * sizes are channels, so they arrive with the request and are filled in per slice.\n */\n const fixed = {\n id: PAYLOAD_ADDRESS[0] as string,\n subject: subjects ? (PAYLOAD_IDENTITY[0] as string) : undefined,\n x: PAYLOAD_COORDINATES[0] as string,\n y: PAYLOAD_COORDINATES[1] as string,\n source: adjacencies[0]?.column ?? \"src_dense\",\n target: drawn[0]?.address.adjacency(\"dst\")?.column ?? \"dst_dense\",\n } as const;\n\n /**\n * The relation half — **fossil's views, not a second set of ours.**\n *\n * A chart, a count or a crossfilter clause reads every row by name, so the canvas has to hand a\n * host relation names as well as tiles. This used to register its own: persistent `corpus_*`\n * views over the same files fossil's verbs already viewed as `TEMP`, with a different rule for\n * which edge files a relation is — two view sets over one corpus, and the persistent one outlived\n * it. `relations()` answers with the names the verbs query, qualified by the corpus's own catalog,\n * so closing the corpus takes them with it.\n */\n const relations = await corpus.relations();\n const relationOf = (match: (r: CorpusRelation) => boolean, what: string): string => {\n const found = relations.find(match);\n if (!found) throw new Error(`openCorpus: the corpus registers no relation for ${what}`);\n return found.sql;\n };\n const nodesView = relationOf((r) => r.kind === \"vertex\" && r.name === type.type, type.type);\n\n const source: DuckSource = {\n ...watching.api,\n\n publish(vertices) {\n publishSelection(reads, filterBy, fixed.id, vertices);\n },\n\n /**\n * How many vertices there are — **read, not probed.**\n *\n * The manifest declares `vertex_count`. What stood here was a note saying no manifest carries\n * it, and a doubling-then-bisecting `HEAD` search for the last chunk plus a\n * `parquet_file_metadata` read of its row count — about a dozen requests and a query, per\n * corpus, for a number already in hand.\n */\n async total() {\n if (type.count !== null) return Number(type.count);\n const rows = await meta(`SELECT count(*) AS n FROM ${nodesView}`);\n return Number(numbers(rows, \"n\")[0] ?? 0);\n },\n\n async extent() {\n const all = await load();\n return {\n xMin: Math.min(...all.map((b) => b.x0)),\n yMin: Math.min(...all.map((b) => b.y0)),\n xMax: Math.max(...all.map((b) => b.x1)),\n yMax: Math.max(...all.map((b) => b.y1)),\n };\n },\n\n // Regions only, and it says so by being the only method there is. A neighbourhood needs\n // adjacency this source does not index; `ExploringSource` declared the second question here and\n // was deleted with it — `index.test.ts` carries why, and fossil's `expand` is what answers it.\n async slice(request: SliceRequest): Promise<Slice> {\n const { fill, limit, minLinkPixels, perPixel, pinned, r, signal, view } = request;\n const columns: Columns = { ...fixed, category: fill, size: r };\n const all = await load();\n /**\n * The tiles the rectangle touches — **and the far view is not a special case of this.**\n *\n * It used to be: past a zoom threshold the selection was replaced by every tile, because the\n * aggregate branch was going to read the whole relation anyway. A window that covers the\n * extent already intersects every box, so the branch was arithmetic restating itself, and it\n * is the reason `Viewport` carried a `zoom` at all.\n */\n const selected = intersecting(all, view);\n // Nothing selected is a legitimate answer — the camera is over empty space — and asking\n // `read_parquet([])` is a syntax error rather than an empty result. Checked before the files\n // are fetched, so an empty window costs no bytes at all.\n if (selected.length === 0) {\n return {\n n: 0,\n marks: 0,\n vertices: new BigUint64Array(0),\n positions: new Float32Array(0),\n links: new Float32Array(0),\n categories: new Uint16Array(0),\n };\n }\n\n /**\n * The URLs those tiles are in — **asked, not composed**, and distinct.\n *\n * The vertex half off `tilesFor`; the edge half off each drawn relation's own adjacency,\n * which is `frame`'s shape for the same reason (`corpus.ts`, `edgeLevels.flatMap((set) =>\n * held.map((tile) => set.tileUrl(tile)))`). `tilesFor`'s `edgeUrls` is every relation whose\n * SOURCE is this type, so a cross-type one is in it — and `drawing` is where that set is\n * narrowed to the relations whose far end this source can position. The source-ordered\n * orientation is the drawing read either way: every edge a window can draw has its source on\n * screen, therefore in one of these files.\n */\n const addressed = addressing.tilesFor({\n type: type.type,\n tiles: selected,\n directions: [\"src\"],\n });\n const edgeUrls = [\n ...new Set(adjacencies.flatMap((a) => selected.map((tile) => a.tileUrl(tile)))),\n ];\n // Both halves at once: the vertex files and the edge files a window touches are independent\n // reads, and the window is not drawable until both have landed.\n /**\n * The camera moved while the tiles were arriving, so this question is already the wrong one.\n *\n * Checked here rather than left to the caller because of what comes next: the reads hold one\n * standing question each, so a request that resumes after the loop moved on would *supersede*\n * the newer one and reject it — the stale question winning the race against the live one.\n *\n * `signal.reason` and not a sentinel of ours: an aborted signal already carries what it was\n * aborted with, which for `AbortController.abort()` is a `DOMException` named `AbortError`.\n * Rethrowing it is the whole of the cancellation contract a source owes — see `SliceRequest`.\n */\n if (signal?.aborted) throw signal.reason;\n const nodes = `read_parquet([${quoted(addressed.vertexUrls)}])`;\n // A corpus that declares no adjacency for this type still has to answer: the links query is\n // built either way, so what it reads is an empty relation of the right shape rather than a\n // `read_parquet([])`, which is a syntax error, or the vertex view, which has neither column.\n const relation =\n edgeUrls.length > 0\n ? `read_parquet([${quoted(edgeUrls)}])`\n : `(SELECT NULL::BIGINT AS ${columns.source}, NULL::BIGINT AS ${columns.target} WHERE FALSE)`;\n\n // `nodes` is both the window's rows and the bytes the reader holds: the tiles that answer\n // \"what is in the rectangle\" are the tiles that answer \"where is the far end of an edge that\n // leaves it\".\n return watching.run(region(nodes, relation, columns, view, limit, pinned, perPixel, minLinkPixels));\n },\n };\n\n return {\n source,\n nodes: nodesView,\n edges: drawn.map(({ address }) => ({\n edgeType: address.edgeType,\n srcType: address.srcType,\n dstType: address.dstType,\n view: relationOf(\n (r) =>\n r.kind === \"edge\" &&\n r.edgeType === address.edgeType &&\n r.srcType === address.srcType &&\n r.dstType === address.dstType,\n `${address.srcType}_${address.edgeType}_${address.dstType}`,\n ),\n })),\n undrawn,\n };\n}\n"],"names":[],"mappings":";;;;;AAuEA;AAwCA;AACE;AAAO;AACsC;AACD;AACX;AAEnC;AAUA;AACE;AACA;AACE;AACA;AAAmB;AACZ;AAEX;AAUA;AACE;AAEA;AACA;AACF;AAGA;AACE;AAGF;AAuBA;AAOE;AAN2C;AACpB;AACA;AACA;AACA;AAKvB;AACF;AAeA;AAyBA;AAOE;AAYA;AAAO;AAGL;AAAA;AAEY;AACC;AAAA;AAIb;AACiB;AAAA;AAAA;AAGY;AAChB;AAEjB;AAYA;AAME;AACA;AACA;AACF;AA+BA;AASE;AAEA;AAAO;AACsE;AAAA;AAAA;AAAA;AAAA;AAK/D;AACgB;AACA;AAAA;AAAA;AAG8C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ9E;AAsBA;AAUE;AA2BA;AAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS4D;AACS;AAAA;AAI1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYwB;AAAA;AAAA;AAAA;AAAA;AAK1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASgB;AACiC;AAAA;AAGnF;AAUA;AACE;AAGA;AAKE;AACA;AAEA;AACyC;AAG3C;AAAO;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASD;AAIE;AAKW;AACb;AACF;AAAA;AAGA;AAIA;AAA0C;AACZ;AACF;AAE5B;AAAkC;AACpC;AAEJ;AAUA;AAME;AACS;AAC+D;AACtD;AAC8B;AAC7C;AAEL;AAWA;AAUE;AAGA;AAKA;AACA;AACA;AACA;AAQA;AACA;AACA;AACA;AACA;AACA;AAKA;AAEA;AACA;AAMA;AAGA;AAEO;AACL;AACA;AACA;AACsC;AACJ;AAClC;AACA;AAEJ;AAGA;;AACE;AACA;AACA;AACA;AAEF;AAgLA;;AACE;AA6BmE;AACjE;AAAA;AAAA;AAGkC;AAgBX;AACN;AACD;AACA;AACkB;AAepC;AAUA;AAqBA;AACE;AACE;AAQoB;AACJ;AAC2B;AACA;AACA;AACA;AACV;AACa;AAI9C;AAA4B;AACpB;AACG;AACA;AACA;AACA;AACT;AAEG;AAIT;AACE;AAEoB;AAWtB;AAAc;AACS;AACiC;AAC9B;AACA;AACU;AACoB;AAetD;AACA;AACA;AAAa;AAkHf;AAAO;AA9GoB;AACb;AAGV;AAAoD;AACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWE;AACA;AACA;AAAwC;AAC1C;AAGE;AACA;AAAO;AACiC;AACA;AACA;AACA;AAAA;AAE1C;AAAA;AAAA;AAAA;AAME;AAeA;AACE;AAAO;AACF;AACI;AACuB;AACD;AACJ;AACI;AAejC;AAAsC;AACzB;AACJ;AACW;AAEH;AAC+D;AAehF;AACA;AAYA;AAAkG;AACpG;AAAA;AAKO;AAC4B;AACf;AACD;AACA;AACX;AAKoB;AACiC;AAAA;AAE3D;AACF;AAEJ;;;;"}
|
package/dist/graph-canvas.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
import { GraphApi, UseGraphProps } from './use-graph';
|
|
3
|
-
/**
|
|
4
|
-
* Reach the graph from its chrome. Strict: outside a provider there is nothing to answer with, and a
|
|
5
|
-
* `null` here would surface as a legend that silently counts zero.
|
|
6
|
-
*/
|
|
7
|
-
export declare function useGraphContext(): GraphApi;
|
|
8
|
-
export interface GraphRootProviderProps {
|
|
9
|
-
/** The api from `useGraph`, built wherever the host needs to reach it. */
|
|
10
|
-
value: GraphApi;
|
|
11
|
-
className?: string;
|
|
12
|
-
/** The chrome — a legend, a toolbar, a zoom control. Positioned over the surface by the host. */
|
|
13
|
-
children?: ReactNode;
|
|
14
|
-
slot?: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* The element, and the context over it.
|
|
18
|
-
*
|
|
19
|
-
* Two divs rather than one, and the split is load-bearing: cosmos.gl takes the inner one and fills
|
|
20
|
-
* it with a canvas of its own, so chrome parented there would be a sibling of that canvas inside an
|
|
21
|
-
* element the renderer resizes. The outer one is the positioning context — `isolate`, so a host's
|
|
22
|
-
* `z-index` on a legend cannot escape into the page — and `children` go there.
|
|
23
|
-
*/
|
|
24
|
-
export declare function GraphRootProvider(props: GraphRootProviderProps): import("react").JSX.Element;
|
|
25
|
-
export interface GraphCanvasProps extends UseGraphProps {
|
|
26
|
-
className?: string;
|
|
27
|
-
children?: ReactNode;
|
|
28
|
-
slot?: string;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* A bounded WebGL graph, wired.
|
|
32
|
-
*
|
|
33
|
-
* **This is `ChartRoot`'s counterpart, and it is deliberately not the whole screen.** It owns the
|
|
34
|
-
* renderer's lifetime, the query loop that follows the camera, and the buffers a look implies — the
|
|
35
|
-
* three that are the same in every product and were being re-wired by hand at each call site.
|
|
36
|
-
* Everything else stays where it differs: a legend, an inspector, a hover card and a rule builder are
|
|
37
|
-
* arrangements, and `children` is where they go.
|
|
38
|
-
*
|
|
39
|
-
* **Overlays and selection are not in here on purpose.** `useGraphOverlays` and `useGraphSelection`
|
|
40
|
-
* need callbacks only the product can write — what a click means, what a lasso commits to. They also
|
|
41
|
-
* need the api from *above* this element, where a context cannot be read, which
|
|
42
|
-
* is the whole reason `useGraph` and `GraphRootProvider` exist beside this shortcut. A host with
|
|
43
|
-
* overlays calls those two; a host with only chrome calls this one and reads `useGraphContext` from
|
|
44
|
-
* a child.
|
|
45
|
-
*
|
|
46
|
-
* This component exists against an earlier decision that there should be no canvas component, and
|
|
47
|
-
* `/docs/design/graph` carries what changed and what would reverse it.
|
|
48
|
-
*/
|
|
49
|
-
export declare function GraphCanvas(props: GraphCanvasProps): import("react").JSX.Element;
|
|
50
|
-
//# sourceMappingURL=graph-canvas.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-canvas.d.ts","sourceRoot":"","sources":["../src/graph-canvas.tsx"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAAY,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAiB1E;;;GAGG;AACH,wBAAgB,eAAe,IAAI,QAAQ,CAI1C;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,KAAK,EAAE,QAAQ,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,+BAW9D;AAED,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BAQlD"}
|
package/dist/graph-canvas.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as s, jsxs as l } from "react/jsx-runtime";
|
|
3
|
-
import { cn as c } from "@kanzo-tech/ui";
|
|
4
|
-
import { useContext as u, createContext as h } from "react";
|
|
5
|
-
import { useGraph as p } from "./use-graph.js";
|
|
6
|
-
const n = h(null);
|
|
7
|
-
function G() {
|
|
8
|
-
const r = u(n);
|
|
9
|
-
if (!r) throw new Error("useGraphContext must be used inside a <GraphCanvas> or <GraphRootProvider>");
|
|
10
|
-
return r;
|
|
11
|
-
}
|
|
12
|
-
function d(r) {
|
|
13
|
-
const { children: t, className: o, slot: a, value: e } = r;
|
|
14
|
-
return /* @__PURE__ */ l(
|
|
15
|
-
"div",
|
|
16
|
-
{
|
|
17
|
-
className: c("relative isolate size-full overflow-hidden", o),
|
|
18
|
-
"data-slot": a ?? "graph-canvas",
|
|
19
|
-
children: [
|
|
20
|
-
/* @__PURE__ */ s("div", { className: "size-full", "data-slot": "graph-canvas-surface", ref: e.hostRef }),
|
|
21
|
-
/* @__PURE__ */ s(n.Provider, { value: e, children: t })
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
function C(r) {
|
|
27
|
-
const { children: t, className: o, slot: a, ...e } = r, i = p(e);
|
|
28
|
-
return /* @__PURE__ */ s(d, { className: o, slot: a, value: i, children: t });
|
|
29
|
-
}
|
|
30
|
-
export {
|
|
31
|
-
C as GraphCanvas,
|
|
32
|
-
d as GraphRootProvider,
|
|
33
|
-
G as useGraphContext
|
|
34
|
-
};
|
|
35
|
-
//# sourceMappingURL=graph-canvas.js.map
|
package/dist/graph-canvas.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-canvas.js","sources":["../src/graph-canvas.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"@kanzo-tech/ui\";\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport { useGraph, type GraphApi, type UseGraphProps } from \"./use-graph\";\n\n/**\n * The three pieces that turn `useGraph` into something you can put on a page — Ark's shape, minus\n * the part of it that only makes sense when a component has parts.\n *\n * `GraphRootProvider` takes an api built above it and renders the surface. `GraphCanvas` is the\n * shortcut that builds one for you, which is what a host wants until it needs to call a hook beside\n * the canvas. `useGraphContext` is how the chrome reads either of them.\n *\n * **`useGraph` creates and `useGraphContext` reads**, which is the convention Ark states and this\n * package used to invert: the reader was called `useGraphCanvas` and there was no creator at all, so\n * anyone arriving from Ark would have read it as the factory and got the opposite.\n */\n\nconst GraphContext = createContext<GraphApi | null>(null);\n\n/**\n * Reach the graph from its chrome. Strict: outside a provider there is nothing to answer with, and a\n * `null` here would surface as a legend that silently counts zero.\n */\nexport function useGraphContext(): GraphApi {\n const value = useContext(GraphContext);\n if (!value) throw new Error(\"useGraphContext must be used inside a <GraphCanvas> or <GraphRootProvider>\");\n return value;\n}\n\nexport interface GraphRootProviderProps {\n /** The api from `useGraph`, built wherever the host needs to reach it. */\n value: GraphApi;\n className?: string;\n /** The chrome — a legend, a toolbar, a zoom control. Positioned over the surface by the host. */\n children?: ReactNode;\n slot?: string;\n}\n\n/**\n * The element, and the context over it.\n *\n * Two divs rather than one, and the split is load-bearing: cosmos.gl takes the inner one and fills\n * it with a canvas of its own, so chrome parented there would be a sibling of that canvas inside an\n * element the renderer resizes. The outer one is the positioning context — `isolate`, so a host's\n * `z-index` on a legend cannot escape into the page — and `children` go there.\n */\nexport function GraphRootProvider(props: GraphRootProviderProps) {\n const { children, className, slot, value } = props;\n return (\n <div\n className={cn(\"relative isolate size-full overflow-hidden\", className)}\n data-slot={slot ?? \"graph-canvas\"}\n >\n <div className=\"size-full\" data-slot=\"graph-canvas-surface\" ref={value.hostRef} />\n <GraphContext.Provider value={value}>{children}</GraphContext.Provider>\n </div>\n );\n}\n\nexport interface GraphCanvasProps extends UseGraphProps {\n className?: string;\n children?: ReactNode;\n slot?: string;\n}\n\n/**\n * A bounded WebGL graph, wired.\n *\n * **This is `ChartRoot`'s counterpart, and it is deliberately not the whole screen.** It owns the\n * renderer's lifetime, the query loop that follows the camera, and the buffers a look implies — the\n * three that are the same in every product and were being re-wired by hand at each call site.\n * Everything else stays where it differs: a legend, an inspector, a hover card and a rule builder are\n * arrangements, and `children` is where they go.\n *\n * **Overlays and selection are not in here on purpose.** `useGraphOverlays` and `useGraphSelection`\n * need callbacks only the product can write — what a click means, what a lasso commits to. They also\n * need the api from *above* this element, where a context cannot be read, which\n * is the whole reason `useGraph` and `GraphRootProvider` exist beside this shortcut. A host with\n * overlays calls those two; a host with only chrome calls this one and reads `useGraphContext` from\n * a child.\n *\n * This component exists against an earlier decision that there should be no canvas component, and\n * `/docs/design/graph` carries what changed and what would reverse it.\n */\nexport function GraphCanvas(props: GraphCanvasProps) {\n const { children, className, slot, ...rest } = props;\n const api = useGraph(rest);\n return (\n <GraphRootProvider className={className} slot={slot} value={api}>\n {children}\n </GraphRootProvider>\n );\n}\n"],"names":[],"mappings":";;;;;AAmBA;AAMO;AACL;AACA;AACA;AACF;AAmBO;AACL;AACA;AACE;AAAC;AAAA;AACsE;AAClD;AAEnB;AAAgF;AACjC;AAAA;AAAA;AAGrD;AA2BO;AACL;AAEA;AAKF;;;;;;"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../src/graph-looks.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,EAAgD,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAe,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvB,IAAI,EAAE;QACJ;;;;;WAKG;QACH,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;;;;;WAUG;QACH,KAAK,EAAE,OAAO,CAAC;QACf;;;;;;WAMG;QACH,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACxB,CAAC;IACF,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;CAuBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,IAAI,CA0BxF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,IAAiB,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAQ5C,CAAC"}
|
package/dist/graph-looks.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-looks.js","sources":["../src/graph-looks.ts"],"sourcesContent":["// Three registers the same graph can be *drawn* in. Not three palettes, and — since a channel is a\n// binding on the request — not three encodings either.\n//\n// **A look is form and nothing else.** No colours: colour belongs to the theme's categorical\n// scheme, the way it belongs to a chart, so every surface showing the same categories gets the same\n// answer. And no encoding: what a channel carries is bound by whoever draws the graph, under Plot's\n// names. A look that decided whether identity reached the GPU as colour or as shape was a theme\n// reaching into an encoding — Vega-Lite states the general rule, that `config` sets defaults for\n// marks, scales, axes and legends and may not touch `encoding` — and its concrete cost was a `fill`\n// binding that painted nothing under the look named Ink.\n//\n// This is the grammar-of-graphics split, and it is the reference model rather than a local idea:\n// in Observable Plot colour is a property of the **scale**, never of the mark, and the mark carries\n// the channels. A look is the mark's geometry.\n//\n// The three names survive as **recommended pairings** — a form plus the bindings that were bundled\n// with it — offered by the host that draws the graph. A host offering three arrangements it authored\n// is not the same act as a preference silently discarding the caller's binding.\n\n/**\n * The glyphs this canvas draws — **by name**, because a name is what the concept is.\n *\n * This was five exports of one idea: a `SHAPE` object mapping names to numbers, a `ShapeId` type\n * that was the union of those numbers, `SHAPE_ORDER`, `SHAPE_OTHER` and `SHAPE_PATH` keyed by them.\n * The numbers were never ours. They are cosmos.gl's `setPointShapes` enum indices, and the jump from\n * `3` (diamond) to `7` (cross) is what gives that away — there is no `4`, `5` or `6` here because\n * Pentagon, Hexagon and Star are members this canvas does not draw. Publishing them made the\n * renderer's internal numbering part of a contract, so a host held `3` where it meant *diamond*, and\n * an upstream enum that renumbered would have moved every legend on every page silently.\n *\n * A string union says the same thing, reads at the call site — `<ShapeGlyph shape=\"cross\" />` — and\n * makes the mapping this file's private business, which is what it always was.\n */\nexport type Shape = \"circle\" | \"square\" | \"triangle\" | \"diamond\" | \"cross\";\n\n/**\n * The name → cosmos.gl enum index, and the one place that translation happens.\n *\n * `None` (`8`) has no name here on purpose: the point fragment shader `discard`s a `NONE` point that\n * carries no image, so \"past capacity\" spelled as `None` would delete the node from the picture, and\n * a category nobody can name is still a node with edges. `obligations.ts` grades that.\n *\n * Module-scoped and off the barrel. `buffers` reads it on the way to the GPU; nothing else needs it,\n * and anything that did would be reaching for the enum this type exists to hide.\n */\nexport const SHAPE_INDEX: Record<Shape, number> = {\n circle: 0,\n square: 1,\n triangle: 2,\n diamond: 3,\n cross: 7,\n};\n\n/**\n * The shape scale, in slot order — the sibling of the colour scale.\n *\n * Plot calls this channel `symbol` and gives it its own legend, which is the tell that it is a peer\n * of colour rather than a decoration. Four slots and no cycling: a fifth category cannot wear circle\n * again without claiming to be the first one.\n *\n * **Four because the shape channel's measured capacity is five, not because small shapes stop being\n * distinguishable.** Giovannangeli et al. (arXiv 2103.06084) put the ceiling at **5 for shape**\n * against **7 for colour**; `SHAPE_ORDER`'s four plus `SHAPE_OTHER` is exactly five distinguishable\n * glyphs, and the two scales differ in cardinality because the channels do. The colour side already\n * says the same thing from the other end — Dracula's document publishes `--chart-capacity: 7`.\n *\n * **This comment used to give the size argument, and the size argument is wrong.** It said a\n * pentagon, a hexagon and a circle are one dot at Ink's floor. Smart & Szafir (CHI 2019,\n * doi:10.1145/3290605.3300899) measured 16 shapes across 6 mark sizes from 6 to 50 px and found\n * shape discrimination *robust* to size: the only significant variation is at 6 px, and it is 4.5\n * accuracy points against 50 px. The conclusion survived the argument that was given for it, which\n * is the most dangerous shape a comment can have — see `OBLIGATIONS` in `./obligations.ts` for what\n * the floor actually protects.\n */\nexport const SHAPE_ORDER: Shape[] = [\"circle\", \"square\", \"triangle\", \"diamond\"];\n\n/**\n * What a category past the scale wears — the shape channel's `--muted-foreground`.\n *\n * No slot wears it, which is the whole point: it says *not one of the four* rather than repeating\n * the first one. This is the half that used to be missing, and the comment above was false without\n * it — `SHAPE_ORDER[4]` is `undefined`, and the fallback was `circle`.\n */\nexport const SHAPE_OTHER: Shape = \"cross\";\n\n/**\n * The geometry a canvas draws — and nothing else.\n *\n * **No `id`, no `label`, no `blurb`.** Those three were a picker's metadata, and the picker was a\n * list of three names that turned out to be two pictures. What a person chooses is declared in\n * `section.ts` as axes and resolved by `lookFrom`; what a renderer consumes is this.\n */\nexport interface Look {\n /** Radius at the lowest degree in the corpus, and at the highest. */\n size: [number, number];\n link: {\n /**\n * Whether the edge layer is drawn at all.\n *\n * It was `Display.links`, a second vocabulary for the picture that sat beside this one with its\n * own default and its own switch. A form that draws no links is a form.\n */\n render: boolean;\n opacity: number;\n width: number;\n /**\n * How far a link bows off the straight line, as a fraction of its length. `0` is straight.\n *\n * Keep it small. Every link curves the same way, so at cosmos.gl's default of `0.5` a few\n * hundred of them read as one pinwheel and the picture looks like it is spinning — motion\n * where there is structure. A hint is enough to tell two parallel edges apart.\n */\n curve: number;\n /**\n * Whether links **add** where they overlap, instead of compositing over one another.\n *\n * cosmos.gl's default is on, which is a choice nobody here made and which the archive made\n * visible: 4,280 grey links at 0.45 summed to a white spray that swallowed 1,543 points of\n * 2–9 px. Every point was uploaded, none was legible, and the picture read as *the nodes are\n * not rendering*.\n *\n * On, it is a real register rather than a bug — additive light is what makes a dense graph read\n * as flow — so it belongs to the form that wants it and not to the renderer's defaults.\n */\n blend: boolean;\n /**\n * Screen lengths between which a link fades out — depth, for free.\n *\n * Keep the far end generous. cosmos.gl measures this in *screen* pixels, so a range that\n * looks reasonable while zoomed in erases the whole edge layer when you zoom out, which\n * reads as \"Show links stopped working\".\n */\n fade: [number, number];\n };\n /** How many of the highest-degree nodes carry a standing label. */\n labels: number;\n /** A darkened rim. Mood rather than a reading aid, which is why it is form and not display. */\n vignette: boolean;\n /**\n * The dot grid behind the graph, which pans and subdivides with the camera.\n *\n * Beside `vignette` because it is the same kind of thing — the backdrop a form sits on — and it\n * arrived from the same place the rim did not: a `Display` interface that spelled the backdrop,\n * the edge layer and two multipliers as a second set of appearance controls.\n */\n grid: boolean;\n // **There is no `pointScale` and no `linkOpacity`**, and they are the two fields `Display` had that\n // this does not. Each was a reader's multiplier over a number this builder already computes from\n // the axis that owns it — `size` from `marks`, `link.opacity` from `marks` — so the panel offered\n // two ways to say one thing and the second could always overrule the measurement. The measured\n // pair (0.28 legible, 0.42 dense) is the whole argument of `marks`; a slider on top of it is the\n // post-process `graph-model.ts` forbids one layer down, wearing a preference's clothes.\n //\n // What is lost with them is the fit-to-corpus case, and it was never a preference: `adaptive`\n // scales a mark by node count, and a computed fit belongs to the tenant's starting point — which\n // is a policy, and which is now expressible.\n\n // There is no `filter`, and its absence is a rule rather than an omission. Nebula carried\n // `saturate(1.1)` on the canvas element — the one thing left in a Look that touched hue, and a\n // chroma multiplier is colour wearing geometry's clothes. Measured over Kanzo's eight slots\n // (2026-08-13, `saturate(1.1)` through the same filter engine the browser applies): it moved\n // every slot, by ΔE 0.85 to **8.05**, which is the size of the separation `deriveScheme`\n // *guarantees* between two different categories. It did not break that separation here —\n // the closest pair went from ΔE 32.36 to 30.38, with room to spare — so the reason it is gone\n // is not a failure it caused, it is that a look must not be able to cause one. The whole claim\n // of the colour layer is that what ships is what was derived and measured; a post-process on\n // the canvas voids it silently, and `graph-model.ts` already forbids the same move one layer\n // down (\"nudging one on the way to the GPU voids all three\").\n}\n\n/**\n * The form a canvas draws, from the axes a person chose.\n *\n * **One function and no table of constants**, which is the shape the axes forced and the reason\n * the three named looks are gone. They were three parallel tables of ten fields; six of those\n * fields separated two of the three by 7–17%, under this file's own threshold for a difference\n * meaning anything — a luminance JND of 6.48–11.30 ΔL*. What is left of them is this: every number\n * appears once, where the axis that owns it is read, and the axis is declared next door in\n * `section.ts` for a panel to draw.\n *\n * **Link opacity and width ride the mark**, and that is what the numbers said rather than a tidy\n * guess: 0.42 and 0.45 on the two dense forms against 0.28 on the legible one. A form that spends\n * more ink on points cannot also spend it on edges.\n *\n * **The legible mark's radius is the whole reason that mark exists.** Its floor protects the other\n * two channels from shape rather than shape from smallness, which is why it is the one to pair\n * `symbol` with: spending shape on identity *and* size on degree at once is what Giovannangeli et\n * al. (arXiv 2103.06084) measure as dropping performance drastically under even minor heterogeneity,\n * and smaller marks worsen it both ways — the luminance JND above, and a square reported larger than\n * any other shape at equal area in 82% of trials (Smart & Szafir, CHI 2019). Not \"a triangle and a\n * square are the same dot below four pixels\", which is what this said for months and is false; see\n * `SHAPE_ORDER` for the measurement that refutes it.\n *\n * **The values are strings because a contributed preference is a string**, in all three kinds, so an\n * unrecognised namespace rides through a write untouched. Parsing what a kind means is the reader's\n * job and it is two lines; `@kanzo-tech/theme` exports the same two, and importing them would add a\n * dependency to this package to carry no code — the same call `GRAPH_SECTION` already makes about the\n * manifest type.\n *\n * A key that is missing, or that carries a value the section never offered, takes the manifest's\n * default. Those defaults are what a graph drew before any of this existed.\n */\nexport function lookFrom(values: Readonly<Record<string, string | undefined>> = {}): Look {\n const on = (key: string, fallback: boolean) => {\n const value = values[key];\n return value === undefined ? fallback : value === \"true\";\n };\n const legible = values.marks === \"legible\";\n const labels = Number.parseFloat(values.labels ?? \"\");\n return {\n size: legible ? [4, 13] : [2, 8],\n link: {\n render: on(\"links\", true),\n opacity: legible ? 0.28 : 0.42,\n width: legible ? 0.5 : 0.6,\n // A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default\n // of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two\n // pictures — straight, and told apart — not a number to tune.\n curve: on(\"bowed-links\", true) ? 0.12 : 0,\n blend: on(\"additive-links\", false),\n // Shared by every form. It was three ranges within ±10% of each other, which is the\n // definition of a field nobody chose.\n fade: [200, 1400],\n },\n labels: Number.isFinite(labels) ? labels : 26,\n vignette: on(\"vignette\", false),\n grid: on(\"grid\", true),\n };\n}\n\n/**\n * What a canvas draws when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be.** It is literally `lookFrom()` — a second public name for\n * a value the package already hands out on request — and the reason it was exported is the one\n * `resolveLook` below removes: a host that wanted one field different had to start from the whole\n * object, because `look` took a whole `Look`. Spreading a default you were given is a copy of it,\n * and a copy is what stops tracking the original the next time a number here moves.\n */\nexport const DEFAULT_LOOK: Look = lookFrom();\n\n/**\n * A look in the pieces a caller wants different — everything else is this package's answer.\n *\n * Two levels, because a `Look` has exactly two: the fields, and `link`. Deep-merging arbitrarily\n * would be a guess about a shape that is right here in this file, and `size` and `fade` are tuples\n * that must be replaced whole rather than merged element-wise.\n */\nexport interface LookPatch extends Partial<Omit<Look, \"link\">> {\n link?: Partial<Look[\"link\"]>;\n}\n\n/**\n * A patch over the package's own default — the merge `DEFAULT_LOOK` existed so a host could do by\n * hand.\n *\n * **Nothing, and it is the shared constant rather than a copy of it.** That identity matters: the\n * buffers are rebuilt whenever the look's reference changes, so a fresh object per render would\n * mean a full colour/size/shape upload on every render of every canvas that never asked for one.\n */\nexport function resolveLook(patch?: LookPatch): Look {\n if (!patch) return DEFAULT_LOOK;\n return { ...DEFAULT_LOOK, ...patch, link: { ...DEFAULT_LOOK.link, ...patch.link } };\n}\n\n/**\n * The SVG path for a shape glyph inside a 12×12 box — the legend draws what the canvas draws.\n *\n * **Off the barrel, and `ShapeGlyph` is what replaced it.** One host imported this, to fill a\n * `<path>` in a legend key and a hover card. What that host actually wanted was *the glyph*, and\n * handing it the path data made it responsible for the viewBox, the fill and the fact that the box\n * is twelve units — three facts it had to keep in step with this file by reading the comment above.\n * A component carries all three and cannot fall out of step with itself.\n */\nexport const SHAPE_PATH: Record<Shape, string> = {\n circle: \"M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z\",\n square: \"M2 2h8v8H2Z\",\n triangle: \"M6 1.6 10.6 10H1.4Z\",\n diamond: \"M6 1 11 6l-5 5-5-5Z\",\n // The proportions are cosmos.gl's own `crossDistance`: a plus with arms at 0.8 of the radius and\n // a bar 0.3 thick, so the legend's glyph is the shape the shader draws.\n cross: \"M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z\",\n};\n"],"names":["SHAPE_INDEX","SHAPE_ORDER","SHAPE_OTHER","lookFrom","values","on","key","fallback","value","legible","labels","DEFAULT_LOOK","resolveLook","patch","SHAPE_PATH"],"mappings":"AA6CO,MAAMA,IAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AACT,GAuBaC,IAAuB,CAAC,UAAU,UAAU,YAAY,SAAS,GASjEC,IAAqB;AAuH3B,SAASC,EAASC,IAAuD,IAAU;AACxF,QAAMC,IAAK,CAACC,GAAaC,MAAsB;AAC7C,UAAMC,IAAQJ,EAAOE,CAAG;AACxB,WAAOE,MAAU,SAAYD,IAAWC,MAAU;AAAA,EACpD,GACMC,IAAUL,EAAO,UAAU,WAC3BM,IAAS,OAAO,WAAWN,EAAO,UAAU,EAAE;AACpD,SAAO;AAAA,IACL,MAAMK,IAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AAAA,IAC/B,MAAM;AAAA,MACJ,QAAQJ,EAAG,SAAS,EAAI;AAAA,MACxB,SAASI,IAAU,OAAO;AAAA,MAC1B,OAAOA,IAAU,MAAM;AAAA;AAAA;AAAA;AAAA,MAIvB,OAAOJ,EAAG,eAAe,EAAI,IAAI,OAAO;AAAA,MACxC,OAAOA,EAAG,kBAAkB,EAAK;AAAA;AAAA;AAAA,MAGjC,MAAM,CAAC,KAAK,IAAI;AAAA,IAAA;AAAA,IAElB,QAAQ,OAAO,SAASK,CAAM,IAAIA,IAAS;AAAA,IAC3C,UAAUL,EAAG,YAAY,EAAK;AAAA,IAC9B,MAAMA,EAAG,QAAQ,EAAI;AAAA,EAAA;AAEzB;AAWO,MAAMM,IAAqBR,EAAA;AAqB3B,SAASS,EAAYC,GAAyB;AACnD,SAAKA,IACE,EAAE,GAAGF,GAAc,GAAGE,GAAO,MAAM,EAAE,GAAGF,EAAa,MAAM,GAAGE,EAAM,OAAK,IAD7DF;AAErB;AAWO,MAAMG,IAAoC;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO;AACT;"}
|
package/dist/graph-model.d.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { Graph, GraphConfig } from '@cosmos.gl/graph';
|
|
2
|
-
import { Slice } from './bounded';
|
|
3
|
-
import { Look, Shape } from './graph-looks';
|
|
4
|
-
import { Sim } from './graph-sim';
|
|
5
|
-
/**
|
|
6
|
-
* From a slice to the GPU, and nothing about where the slice came from.
|
|
7
|
-
*
|
|
8
|
-
* `load()` used to live here — a relation in, every id, every row, an id→index map and a global
|
|
9
|
-
* ranking out. ADR-0001 deleted it: the working set was N, so the ceiling was whatever N the machine
|
|
10
|
-
* could hold, and 1,225 ms of first paint at 200,000 nodes was the measurement that ended the
|
|
11
|
-
* argument. What replaces it is not a faster loader but a different question — `BoundedSource`
|
|
12
|
-
* answers *what should I draw*, `useQueryLoop` asks it, and this turns the answer into buffers.
|
|
13
|
-
*
|
|
14
|
-
* The visible consequence is that **a category is an ordinal here, never a name.** A slice carries
|
|
15
|
-
* `Uint16Array` category codes because carrying twenty thousand label strings to colour twenty
|
|
16
|
-
* thousand dots is paying for a vocabulary the GPU cannot read. What each ordinal is *called* is a
|
|
17
|
-
* question for a legend, and a legend asks the source.
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* What each channel is bound to — Plot's names, and Plot's rule about what a value means.
|
|
21
|
-
*
|
|
22
|
-
* **A CSS colour is a constant; anything else is a column name.** `fill="kind"` spends colour on a
|
|
23
|
-
* category; `fill="var(--foreground)"` paints every point one ink and leaves colour free to mean the
|
|
24
|
-
* selection. That is not our invention: it is how Plot reads the same string, and it is what makes
|
|
25
|
-
* "monochrome" expressible without a look that rebinds an encoding.
|
|
26
|
-
*
|
|
27
|
-
* The rule is spelled narrowly on purpose: a constant is `var(…)`, a hex, or a CSS colour function.
|
|
28
|
-
* **A bare word is always a column**, so a corpus with a column called `red` is not a trap. The cost
|
|
29
|
-
* is that the 148 CSS named colours are not accepted as constants, which is the trade a one-line
|
|
30
|
-
* rule buys over a table nobody would keep current.
|
|
31
|
-
*/
|
|
32
|
-
export interface Channels {
|
|
33
|
-
/** Which column colours a point, or a CSS colour every point wears. */
|
|
34
|
-
fill?: string;
|
|
35
|
-
/**
|
|
36
|
-
* Which column a point's **shape** carries — Plot's `symbol`, and a peer of colour rather than a
|
|
37
|
-
* decoration.
|
|
38
|
-
*
|
|
39
|
-
* Bound to the same column as `fill`, this is redundant encoding and costs nothing. Bound to a
|
|
40
|
-
* *different* column it would need a second categorical array in the slice and in every source,
|
|
41
|
-
* which is not paid for — and it is also the pairing the literature warns about, so the limit and
|
|
42
|
-
* the advice point the same way. A slice carries one categorical column, and this spends shape on
|
|
43
|
-
* it.
|
|
44
|
-
*/
|
|
45
|
-
symbol?: string;
|
|
46
|
-
/**
|
|
47
|
-
* What tints a link. **Absent, each link takes the colour of the vertex it leaves**; a constant
|
|
48
|
-
* makes links plain structure.
|
|
49
|
-
*
|
|
50
|
-
* No column form yet: a per-link datum — weight, confidence, recency — is a second array a slice
|
|
51
|
-
* does not carry.
|
|
52
|
-
*/
|
|
53
|
-
stroke?: string;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Whether a channel's value is a **colour** — and therefore a constant rather than a column.
|
|
57
|
-
*
|
|
58
|
-
* Plot's test, narrowed to what a token-based system actually writes. Exported because the split it
|
|
59
|
-
* decides happens in two places: the buffers paint the constant, and the query must not be asked to
|
|
60
|
-
* fetch a column called `var(--foreground)`.
|
|
61
|
-
*/
|
|
62
|
-
export declare function isColour(value: string | undefined): value is string;
|
|
63
|
-
/**
|
|
64
|
-
* The categorical scale the bindings imply — what colour and what shape a category ordinal wears.
|
|
65
|
-
*
|
|
66
|
-
* One answer for the GPU buffers, the hover card and the legend, because three answers is how a
|
|
67
|
-
* legend ends up disagreeing with the canvas it explains. Values are CSS strings: the DOM resolves
|
|
68
|
-
* them itself, and the buffer path resolves them once on its way to the GPU.
|
|
69
|
-
*
|
|
70
|
-
* `capacity` is where Other begins, and it is the document's number rather than the token
|
|
71
|
-
* vocabulary's: a set derived from a client's brand names 6 to 8 real categories, not always 8. The
|
|
72
|
-
* colour past it would come out muted anyway — `compile()` writes `var(--muted-foreground)` into
|
|
73
|
-
* those slots — but the graph has to *count* the same way, or a legend claims eight kinds it cannot
|
|
74
|
-
* tell apart.
|
|
75
|
-
*/
|
|
76
|
-
export declare function scaleOf(channels: Channels, capacity?: number): {
|
|
77
|
-
color: (ordinal: number) => string;
|
|
78
|
-
shape: (ordinal: number) => Shape;
|
|
79
|
-
};
|
|
80
|
-
export interface Buffers {
|
|
81
|
-
colors: Float32Array;
|
|
82
|
-
sizes: Float32Array;
|
|
83
|
-
shapes: Float32Array;
|
|
84
|
-
linkColors: Float32Array;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Every per-point and per-link attribute the GPU needs, from one slice and the live theme.
|
|
88
|
-
*
|
|
89
|
-
* `host` is the element the tokens are read against, which is what makes `var(--primary)` a legal
|
|
90
|
-
* value in a look: the browser resolves it for the tree the canvas actually sits in, so the same
|
|
91
|
-
* look answers differently in light and in dark.
|
|
92
|
-
*
|
|
93
|
-
* `display` is deliberately not an argument. Everything the reader's sliders control is a *global
|
|
94
|
-
* scalar*, and a global scalar belongs in a uniform — see `appearance` — not multiplied into every
|
|
95
|
-
* size and every RGBA quad that then have to be re-uploaded.
|
|
96
|
-
*
|
|
97
|
-
* The size ramp now spans **this slice**, not the corpus. That is the trade ADR-0001 names: a global
|
|
98
|
-
* ordering is what a whole-corpus load gets for free and a bounded one cannot have. It is also
|
|
99
|
-
* arguably the better question — the biggest node *here* is what a reader is looking at.
|
|
100
|
-
*/
|
|
101
|
-
export declare function buffers(slice: Slice, look: Look, host: Element, channels?: Channels): Buffers;
|
|
102
|
-
/**
|
|
103
|
-
* The points one hop from `index`, in both directions, **within the drawn slice**.
|
|
104
|
-
*
|
|
105
|
-
* This is the renderer's adjacency, not the graph's: it answers what is on screen, which is what a
|
|
106
|
-
* hover highlight wants. The graph's own answer — what is adjacent whether or not it is drawn — is a
|
|
107
|
-
* `neighbourhood` query, and a source that supports one answers it.
|
|
108
|
-
*
|
|
109
|
-
* Ours because 3.0 dropped `getAdjacentIndices` and the method that looks like its replacement is
|
|
110
|
-
* not one: `getConnectedLinkIndices` filters on `n.has(d)`, so it answers only with links whose
|
|
111
|
-
* *other* endpoint is also in the argument — an induced subgraph, which for a single point is its
|
|
112
|
-
* self-loops. The adjacency lists themselves are still public on `graph.graph`, and each entry is a
|
|
113
|
-
* `[otherPointIndex, linkIndex]` pair, so the neighbourhood is the first element of each.
|
|
114
|
-
*/
|
|
115
|
-
export declare function neighboursOf(graph: Graph, index: number): number[];
|
|
116
|
-
/** The simulation coefficients, in cosmos.gl's spelling. Shared by construction and every change. */
|
|
117
|
-
export declare function forces(sim: Sim): GraphConfig;
|
|
118
|
-
/**
|
|
119
|
-
* Everything the picture needs that is *one number for the whole canvas* — cosmos.gl's uniforms.
|
|
120
|
-
*
|
|
121
|
-
* That is the line between this and `buffers`, and it is the renderer's own: a uniform is read fresh
|
|
122
|
-
* from the config on every draw, so changing one rebuilds no array and uploads nothing — which is
|
|
123
|
-
* why a look change costs the GPU a `setConfigPartial` and no upload at all.
|
|
124
|
-
*
|
|
125
|
-
* It took a `Display` beside the look, and the two multipliers it carried are gone: each scaled a
|
|
126
|
-
* number this same look already computes from the axis that owns it, so the panel offered two ways
|
|
127
|
-
* to say one thing and the slider could always overrule the measurement.
|
|
128
|
-
*/
|
|
129
|
-
export declare function appearance(look: Look, host: Element): GraphConfig;
|
|
130
|
-
//# sourceMappingURL=graph-model.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-model.d.ts","sourceRoot":"","sources":["../src/graph-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAI3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAyC,KAAK,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,QAAQ;IACvB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,MAAM,CAEnE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,SAAc;qBAI7C,MAAM,KAAG,MAAM;qBAcf,MAAM,KAAG,KAAK;EAGlC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,YAAY,CAAC;CAC1B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,OAAO,EACb,QAAQ,GAAE,QAAa,GACtB,OAAO,CAgFT;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMlE;AAED,qGAAqG;AACrG,wBAAgB,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,WAAW,CAS5C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,WAAW,CA4BjE"}
|
package/dist/graph-model.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { CHART_SLOTS as A, categoricalColor as D, categoricalCapacity as E } from "@kanzo-tech/ui";
|
|
2
|
-
import { resolveToken as p, toHex as m } from "./css-color.js";
|
|
3
|
-
import { SHAPE_ORDER as F, SHAPE_OTHER as H, SHAPE_INDEX as S } from "./graph-looks.js";
|
|
4
|
-
function w(n) {
|
|
5
|
-
return n !== void 0 && /^(var\(|#|rgb|hsl|oklch|oklab|lab|lch|color\()/i.test(n);
|
|
6
|
-
}
|
|
7
|
-
function L(n, i = A) {
|
|
8
|
-
const e = w(n.fill) ? n.fill : void 0, s = n.symbol !== void 0;
|
|
9
|
-
return {
|
|
10
|
-
color: (o) => e || (o >= i ? "var(--muted-foreground)" : D(o, void 0, i)),
|
|
11
|
-
// The shape order runs out at four, so the fifth ordinal and anything past it land on
|
|
12
|
-
// `SHAPE_OTHER` — which is what makes the scale's claim true. Falling back to `circle` would
|
|
13
|
-
// hand category 5 the glyph category 0 already wears.
|
|
14
|
-
//
|
|
15
|
-
// A **name**, not cosmos.gl's enum index. This returned a `ShapeId` — the union of the numbers
|
|
16
|
-
// `setPointShapes` takes — so a legend drawing what this scale answered was holding the
|
|
17
|
-
// renderer's internal numbering. `buffers` does the translation below, at the one point where a
|
|
18
|
-
// number is what the GPU wants.
|
|
19
|
-
shape: (o) => s ? F[o] ?? H : "circle"
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function _(n, i, e, s = {}) {
|
|
23
|
-
const o = L(s, E(e)), v = /* @__PURE__ */ new Map(), C = (t) => {
|
|
24
|
-
let r = v.get(t);
|
|
25
|
-
return r || (r = p(e, o.color(t)), v.set(t, r)), r;
|
|
26
|
-
}, k = n.positions.length / 2, f = new Float32Array(k * 4), y = new Float32Array(k), d = new Float32Array(k), a = n.sizes;
|
|
27
|
-
let g = 0, b = 1;
|
|
28
|
-
if (a && a.length > 0) {
|
|
29
|
-
let t = Number.POSITIVE_INFINITY, r = 0;
|
|
30
|
-
for (let l = 0; l < a.length; l++) {
|
|
31
|
-
const u = a[l];
|
|
32
|
-
u < t && (t = u), u > r && (r = u);
|
|
33
|
-
}
|
|
34
|
-
g = Math.sqrt(Number.isFinite(t) ? t : 0), b = Math.sqrt(r) - g || 1;
|
|
35
|
-
}
|
|
36
|
-
const P = Math.min(n.marks, k);
|
|
37
|
-
for (let t = 0; t < P; t++) {
|
|
38
|
-
const r = n.categories[t] ?? 0;
|
|
39
|
-
f.set(C(r), t * 4);
|
|
40
|
-
const l = a ? (Math.sqrt(a[t]) - g) / b : 0;
|
|
41
|
-
y[t] = i.size[0] + l * (i.size[1] - i.size[0]), d[t] = S[o.shape(r)];
|
|
42
|
-
}
|
|
43
|
-
const h = n.links.length / 2, O = new Float32Array(h * 4), c = s.stroke ? p(e, s.stroke) : null;
|
|
44
|
-
for (let t = 0; t < h; t++) {
|
|
45
|
-
const r = n.links[t * 2] ?? 0, l = c ? c[0] : f[r * 4] ?? 0.7, u = c ? c[1] : f[r * 4 + 1] ?? 0.7, R = c ? c[2] : f[r * 4 + 2] ?? 0.7;
|
|
46
|
-
O.set([l, u, R, 1], t * 4);
|
|
47
|
-
}
|
|
48
|
-
return { colors: f, sizes: y, shapes: d, linkColors: O };
|
|
49
|
-
}
|
|
50
|
-
function T(n, i) {
|
|
51
|
-
const { sourceIndexToTargetIndices: e, targetIndexToSourceIndices: s } = n.graph;
|
|
52
|
-
return [
|
|
53
|
-
...((e == null ? void 0 : e[i]) ?? []).map((o) => o[0]),
|
|
54
|
-
...((s == null ? void 0 : s[i]) ?? []).map((o) => o[0])
|
|
55
|
-
];
|
|
56
|
-
}
|
|
57
|
-
function q(n) {
|
|
58
|
-
return {
|
|
59
|
-
simulationGravity: n.gravity,
|
|
60
|
-
simulationRepulsion: n.repulsion,
|
|
61
|
-
simulationLinkSpring: n.linkSpring,
|
|
62
|
-
simulationLinkDistance: n.linkDistance,
|
|
63
|
-
simulationFriction: n.friction,
|
|
64
|
-
simulationCluster: n.cluster
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function G(n, i) {
|
|
68
|
-
return {
|
|
69
|
-
// Always the theme's surface. Nebula used to pin a near-black of its own, which made it the one
|
|
70
|
-
// look that ignored light mode — and put its fixed dark plane at odds with the light chrome
|
|
71
|
-
// sitting on top of it.
|
|
72
|
-
backgroundColor: m(p(i, "var(--background)")),
|
|
73
|
-
// Points hold their screen size in every look. This was a field until all three settled on the
|
|
74
|
-
// same value, at which point it was a field with one possible answer.
|
|
75
|
-
scalePointsOnZoom: !1,
|
|
76
|
-
renderLinks: n.link.render,
|
|
77
|
-
/** Edge opacity — the look's, multiplying each link's buffer alpha. */
|
|
78
|
-
linkOpacity: n.link.opacity,
|
|
79
|
-
linkDefaultWidth: n.link.width,
|
|
80
|
-
linkBlending: n.link.blend,
|
|
81
|
-
curvedLinks: n.link.curve > 0,
|
|
82
|
-
curvedLinkControlPointDistance: n.link.curve,
|
|
83
|
-
linkVisibilityDistanceRange: n.link.fade,
|
|
84
|
-
linkVisibilityMinTransparency: 0.12,
|
|
85
|
-
renderHoveredPointRing: !0,
|
|
86
|
-
hoveredPointRingColor: m(p(i, "var(--primary)")),
|
|
87
|
-
focusedPointRingColor: m(p(i, "var(--primary)")),
|
|
88
|
-
// The two greyouts are not the same kind of number, whatever the names suggest. A greyed link
|
|
89
|
-
// multiplies (`opacity *= greyoutOpacity`), so it stays under the slider; a greyed point takes
|
|
90
|
-
// this *instead of* `pointOpacity` — the point shader is an if/else. So a global point opacity,
|
|
91
|
-
// if this canvas ever grows one, would not reach a dimmed node, and its floor would be 0.1 flat.
|
|
92
|
-
pointGreyoutOpacity: 0.1,
|
|
93
|
-
linkGreyoutOpacity: 0.025
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
export {
|
|
97
|
-
G as appearance,
|
|
98
|
-
_ as buffers,
|
|
99
|
-
q as forces,
|
|
100
|
-
w as isColour,
|
|
101
|
-
T as neighboursOf,
|
|
102
|
-
L as scaleOf
|
|
103
|
-
};
|
|
104
|
-
//# sourceMappingURL=graph-model.js.map
|
package/dist/graph-model.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-model.js","sources":["../src/graph-model.ts"],"sourcesContent":["import type { Graph, GraphConfig } from \"@cosmos.gl/graph\";\n// The categorical scheme is on the root barrel, not `/analytics`: a graph paints from it and\n// never opens a database, so the chart half is not what it needs.\nimport { CHART_SLOTS, categoricalCapacity, categoricalColor } from \"@kanzo-tech/ui\";\nimport type { Slice } from \"./bounded\";\nimport { resolveToken, toHex, type Rgba } from \"./css-color\";\nimport { SHAPE_INDEX, SHAPE_ORDER, SHAPE_OTHER, type Look, type Shape } from \"./graph-looks\";\nimport type { Sim } from \"./graph-sim\";\n\n/**\n * From a slice to the GPU, and nothing about where the slice came from.\n *\n * `load()` used to live here — a relation in, every id, every row, an id→index map and a global\n * ranking out. ADR-0001 deleted it: the working set was N, so the ceiling was whatever N the machine\n * could hold, and 1,225 ms of first paint at 200,000 nodes was the measurement that ended the\n * argument. What replaces it is not a faster loader but a different question — `BoundedSource`\n * answers *what should I draw*, `useQueryLoop` asks it, and this turns the answer into buffers.\n *\n * The visible consequence is that **a category is an ordinal here, never a name.** A slice carries\n * `Uint16Array` category codes because carrying twenty thousand label strings to colour twenty\n * thousand dots is paying for a vocabulary the GPU cannot read. What each ordinal is *called* is a\n * question for a legend, and a legend asks the source.\n */\n\n/**\n * What each channel is bound to — Plot's names, and Plot's rule about what a value means.\n *\n * **A CSS colour is a constant; anything else is a column name.** `fill=\"kind\"` spends colour on a\n * category; `fill=\"var(--foreground)\"` paints every point one ink and leaves colour free to mean the\n * selection. That is not our invention: it is how Plot reads the same string, and it is what makes\n * \"monochrome\" expressible without a look that rebinds an encoding.\n *\n * The rule is spelled narrowly on purpose: a constant is `var(…)`, a hex, or a CSS colour function.\n * **A bare word is always a column**, so a corpus with a column called `red` is not a trap. The cost\n * is that the 148 CSS named colours are not accepted as constants, which is the trade a one-line\n * rule buys over a table nobody would keep current.\n */\nexport interface Channels {\n /** Which column colours a point, or a CSS colour every point wears. */\n fill?: string;\n /**\n * Which column a point's **shape** carries — Plot's `symbol`, and a peer of colour rather than a\n * decoration.\n *\n * Bound to the same column as `fill`, this is redundant encoding and costs nothing. Bound to a\n * *different* column it would need a second categorical array in the slice and in every source,\n * which is not paid for — and it is also the pairing the literature warns about, so the limit and\n * the advice point the same way. A slice carries one categorical column, and this spends shape on\n * it.\n */\n symbol?: string;\n /**\n * What tints a link. **Absent, each link takes the colour of the vertex it leaves**; a constant\n * makes links plain structure.\n *\n * No column form yet: a per-link datum — weight, confidence, recency — is a second array a slice\n * does not carry.\n */\n stroke?: string;\n}\n\n/**\n * Whether a channel's value is a **colour** — and therefore a constant rather than a column.\n *\n * Plot's test, narrowed to what a token-based system actually writes. Exported because the split it\n * decides happens in two places: the buffers paint the constant, and the query must not be asked to\n * fetch a column called `var(--foreground)`.\n */\nexport function isColour(value: string | undefined): value is string {\n return value !== undefined && /^(var\\(|#|rgb|hsl|oklch|oklab|lab|lch|color\\()/i.test(value);\n}\n\n/**\n * The categorical scale the bindings imply — what colour and what shape a category ordinal wears.\n *\n * One answer for the GPU buffers, the hover card and the legend, because three answers is how a\n * legend ends up disagreeing with the canvas it explains. Values are CSS strings: the DOM resolves\n * them itself, and the buffer path resolves them once on its way to the GPU.\n *\n * `capacity` is where Other begins, and it is the document's number rather than the token\n * vocabulary's: a set derived from a client's brand names 6 to 8 real categories, not always 8. The\n * colour past it would come out muted anyway — `compile()` writes `var(--muted-foreground)` into\n * those slots — but the graph has to *count* the same way, or a legend claims eight kinds it cannot\n * tell apart.\n */\nexport function scaleOf(channels: Channels, capacity = CHART_SLOTS) {\n const constant = isColour(channels.fill) ? channels.fill : undefined;\n const shaped = channels.symbol !== undefined;\n return {\n color: (ordinal: number): string => {\n if (constant) return constant;\n return ordinal >= capacity\n ? \"var(--muted-foreground)\"\n : categoricalColor(ordinal, undefined, capacity);\n },\n // The shape order runs out at four, so the fifth ordinal and anything past it land on\n // `SHAPE_OTHER` — which is what makes the scale's claim true. Falling back to `circle` would\n // hand category 5 the glyph category 0 already wears.\n //\n // A **name**, not cosmos.gl's enum index. This returned a `ShapeId` — the union of the numbers\n // `setPointShapes` takes — so a legend drawing what this scale answered was holding the\n // renderer's internal numbering. `buffers` does the translation below, at the one point where a\n // number is what the GPU wants.\n shape: (ordinal: number): Shape =>\n shaped ? (SHAPE_ORDER[ordinal] ?? SHAPE_OTHER) : \"circle\",\n };\n}\n\nexport interface Buffers {\n colors: Float32Array;\n sizes: Float32Array;\n shapes: Float32Array;\n linkColors: Float32Array;\n}\n\n/**\n * Every per-point and per-link attribute the GPU needs, from one slice and the live theme.\n *\n * `host` is the element the tokens are read against, which is what makes `var(--primary)` a legal\n * value in a look: the browser resolves it for the tree the canvas actually sits in, so the same\n * look answers differently in light and in dark.\n *\n * `display` is deliberately not an argument. Everything the reader's sliders control is a *global\n * scalar*, and a global scalar belongs in a uniform — see `appearance` — not multiplied into every\n * size and every RGBA quad that then have to be re-uploaded.\n *\n * The size ramp now spans **this slice**, not the corpus. That is the trade ADR-0001 names: a global\n * ordering is what a whole-corpus load gets for free and a bounded one cannot have. It is also\n * arguably the better question — the biggest node *here* is what a reader is looking at.\n */\nexport function buffers(\n slice: Slice,\n look: Look,\n host: Element,\n channels: Channels = {},\n): Buffers {\n const scale = scaleOf(channels, categoricalCapacity(host));\n\n /** Ordinal → resolved colour, memoised: a slice of 20,000 points wears at most a handful. */\n const rgba = new Map<number, Rgba>();\n const colourOf = (ordinal: number): Rgba => {\n let resolved = rgba.get(ordinal);\n if (!resolved) {\n resolved = resolveToken(host, scale.color(ordinal));\n rgba.set(ordinal, resolved);\n }\n return resolved;\n };\n\n const n = slice.positions.length / 2;\n const colors = new Float32Array(n * 4);\n const sizes = new Float32Array(n);\n const shapes = new Float32Array(n);\n\n // Whatever the source ranks by, square-rooted: a degree distribution is heavy-tailed and a linear\n // ramp leaves everything but the three biggest hubs on the floor. It used to read a second column\n // in aggregate mode — how many vertices a super-node stood for — and there are no super-nodes.\n const ramp = slice.sizes;\n let lo = 0;\n let span = 1;\n if (ramp && ramp.length > 0) {\n let min = Number.POSITIVE_INFINITY;\n let max = 0;\n for (let i = 0; i < ramp.length; i++) {\n const value = ramp[i] as number;\n if (value < min) min = value;\n if (value > max) max = value;\n }\n lo = Math.sqrt(Number.isFinite(min) ? min : 0);\n span = Math.sqrt(max) - lo || 1;\n }\n\n /**\n * Where the marks stop and the anchors begin.\n *\n * An anchor exists so an edge leaving the window has an end to be drawn to, and it is **not a\n * mark**: radius zero and alpha zero, so it cannot paint, cannot be picked and cannot be occluded\n * by. The zeroing is the loop bound and nothing else: the buffers are allocated zero-filled, so\n * stopping at `marks` leaves every anchor at radius zero and alpha zero. Writing it out again\n * afterwards would be a second statement of the same fact that no mutation can distinguish from\n * the first — which is how a guard goes green for the wrong reason.\n */\n const marks = Math.min(slice.marks, n);\n\n for (let i = 0; i < marks; i++) {\n const ordinal = slice.categories[i] ?? 0;\n // The slot colour is used as given. Nebula used to lighten it by degree, which is exactly the\n // kind of adjustment a validated palette cannot survive: every slot was measured for lightness\n // band, chroma floor and separation, and nudging one on the way to the GPU voids all three.\n colors.set(colourOf(ordinal), i * 4);\n const t = ramp ? (Math.sqrt(ramp[i] as number) - lo) / span : 0;\n sizes[i] = look.size[0] + t * (look.size[1] - look.size[0]);\n // The one place a glyph becomes a number: `setPointShapes` takes cosmos.gl's enum, and this is\n // the only consumer of it. Everything above and outside says `\"cross\"`.\n shapes[i] = SHAPE_INDEX[scale.shape(ordinal)] as number;\n }\n\n const count = slice.links.length / 2;\n const linkColors = new Float32Array(count * 4);\n // Absent, a link takes the colour of the vertex it leaves; a constant makes links plain structure.\n const neutral = channels.stroke ? resolveToken(host, channels.stroke) : null;\n for (let e = 0; e < count; e++) {\n const src = slice.links[e * 2] ?? 0;\n const r = neutral ? neutral[0] : (colors[src * 4] ?? 0.7);\n const g = neutral ? neutral[1] : (colors[src * 4 + 1] ?? 0.7);\n const b = neutral ? neutral[2] : (colors[src * 4 + 2] ?? 0.7);\n // Alpha is 1, and it is *reserved* — for a datum that genuinely differs per link: edge weight,\n // confidence, recency. It used to carry `look.opacity × display.linkOpacity`, which is the same\n // number on every link, and paying for that cost a full re-upload of this array on every tick of\n // the Edge opacity slider. That product is a uniform now (`appearance`), and the shader\n // multiplies the two: `color.a * linkOpacity * …`. Do not spend this channel again.\n linkColors.set([r, g, b, 1], e * 4);\n }\n\n return { colors, sizes, shapes, linkColors };\n}\n\n/**\n * The points one hop from `index`, in both directions, **within the drawn slice**.\n *\n * This is the renderer's adjacency, not the graph's: it answers what is on screen, which is what a\n * hover highlight wants. The graph's own answer — what is adjacent whether or not it is drawn — is a\n * `neighbourhood` query, and a source that supports one answers it.\n *\n * Ours because 3.0 dropped `getAdjacentIndices` and the method that looks like its replacement is\n * not one: `getConnectedLinkIndices` filters on `n.has(d)`, so it answers only with links whose\n * *other* endpoint is also in the argument — an induced subgraph, which for a single point is its\n * self-loops. The adjacency lists themselves are still public on `graph.graph`, and each entry is a\n * `[otherPointIndex, linkIndex]` pair, so the neighbourhood is the first element of each.\n */\nexport function neighboursOf(graph: Graph, index: number): number[] {\n const { sourceIndexToTargetIndices, targetIndexToSourceIndices } = graph.graph;\n return [\n ...(sourceIndexToTargetIndices?.[index] ?? []).map((pair) => pair[0]),\n ...(targetIndexToSourceIndices?.[index] ?? []).map((pair) => pair[0]),\n ];\n}\n\n/** The simulation coefficients, in cosmos.gl's spelling. Shared by construction and every change. */\nexport function forces(sim: Sim): GraphConfig {\n return {\n simulationGravity: sim.gravity,\n simulationRepulsion: sim.repulsion,\n simulationLinkSpring: sim.linkSpring,\n simulationLinkDistance: sim.linkDistance,\n simulationFriction: sim.friction,\n simulationCluster: sim.cluster,\n };\n}\n\n/**\n * Everything the picture needs that is *one number for the whole canvas* — cosmos.gl's uniforms.\n *\n * That is the line between this and `buffers`, and it is the renderer's own: a uniform is read fresh\n * from the config on every draw, so changing one rebuilds no array and uploads nothing — which is\n * why a look change costs the GPU a `setConfigPartial` and no upload at all.\n *\n * It took a `Display` beside the look, and the two multipliers it carried are gone: each scaled a\n * number this same look already computes from the axis that owns it, so the panel offered two ways\n * to say one thing and the slider could always overrule the measurement.\n */\nexport function appearance(look: Look, host: Element): GraphConfig {\n return {\n // Always the theme's surface. Nebula used to pin a near-black of its own, which made it the one\n // look that ignored light mode — and put its fixed dark plane at odds with the light chrome\n // sitting on top of it.\n backgroundColor: toHex(resolveToken(host, \"var(--background)\")),\n // Points hold their screen size in every look. This was a field until all three settled on the\n // same value, at which point it was a field with one possible answer.\n scalePointsOnZoom: false,\n renderLinks: look.link.render,\n /** Edge opacity — the look's, multiplying each link's buffer alpha. */\n linkOpacity: look.link.opacity,\n linkDefaultWidth: look.link.width,\n linkBlending: look.link.blend,\n curvedLinks: look.link.curve > 0,\n curvedLinkControlPointDistance: look.link.curve,\n linkVisibilityDistanceRange: look.link.fade,\n linkVisibilityMinTransparency: 0.12,\n renderHoveredPointRing: true,\n hoveredPointRingColor: toHex(resolveToken(host, \"var(--primary)\")),\n focusedPointRingColor: toHex(resolveToken(host, \"var(--primary)\")),\n // The two greyouts are not the same kind of number, whatever the names suggest. A greyed link\n // multiplies (`opacity *= greyoutOpacity`), so it stays under the slider; a greyed point takes\n // this *instead of* `pointOpacity` — the point shader is an if/else. So a global point opacity,\n // if this canvas ever grows one, would not reach a dimmed node, and its floor would be 0.1 flat.\n pointGreyoutOpacity: 0.1,\n linkGreyoutOpacity: 0.025,\n };\n}\n"],"names":["isColour","value","scaleOf","channels","capacity","CHART_SLOTS","constant","shaped","ordinal","categoricalColor","SHAPE_ORDER","SHAPE_OTHER","buffers","slice","look","host","scale","categoricalCapacity","rgba","colourOf","resolved","resolveToken","n","colors","sizes","shapes","ramp","lo","span","min","max","i","marks","t","SHAPE_INDEX","count","linkColors","neutral","e","src","r","g","b","neighboursOf","graph","index","sourceIndexToTargetIndices","targetIndexToSourceIndices","pair","forces","sim","appearance","toHex"],"mappings":";;;AAoEO,SAASA,EAASC,GAA4C;AACnE,SAAOA,MAAU,UAAa,kDAAkD,KAAKA,CAAK;AAC5F;AAeO,SAASC,EAAQC,GAAoBC,IAAWC,GAAa;AAClE,QAAMC,IAAWN,EAASG,EAAS,IAAI,IAAIA,EAAS,OAAO,QACrDI,IAASJ,EAAS,WAAW;AACnC,SAAO;AAAA,IACL,OAAO,CAACK,MACFF,MACGE,KAAWJ,IACd,4BACAK,EAAiBD,GAAS,QAAWJ,CAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUnD,OAAO,CAACI,MACND,IAAUG,EAAYF,CAAO,KAAKG,IAAe;AAAA,EAAA;AAEvD;AAwBO,SAASC,EACdC,GACAC,GACAC,GACAZ,IAAqB,CAAA,GACZ;AACT,QAAMa,IAAQd,EAAQC,GAAUc,EAAoBF,CAAI,CAAC,GAGnDG,wBAAW,IAAA,GACXC,IAAW,CAACX,MAA0B;AAC1C,QAAIY,IAAWF,EAAK,IAAIV,CAAO;AAC/B,WAAKY,MACHA,IAAWC,EAAaN,GAAMC,EAAM,MAAMR,CAAO,CAAC,GAClDU,EAAK,IAAIV,GAASY,CAAQ,IAErBA;AAAA,EACT,GAEME,IAAIT,EAAM,UAAU,SAAS,GAC7BU,IAAS,IAAI,aAAaD,IAAI,CAAC,GAC/BE,IAAQ,IAAI,aAAaF,CAAC,GAC1BG,IAAS,IAAI,aAAaH,CAAC,GAK3BI,IAAOb,EAAM;AACnB,MAAIc,IAAK,GACLC,IAAO;AACX,MAAIF,KAAQA,EAAK,SAAS,GAAG;AAC3B,QAAIG,IAAM,OAAO,mBACbC,IAAM;AACV,aAASC,IAAI,GAAGA,IAAIL,EAAK,QAAQK,KAAK;AACpC,YAAM9B,IAAQyB,EAAKK,CAAC;AACpB,MAAI9B,IAAQ4B,MAAKA,IAAM5B,IACnBA,IAAQ6B,MAAKA,IAAM7B;AAAA,IACzB;AACA,IAAA0B,IAAK,KAAK,KAAK,OAAO,SAASE,CAAG,IAAIA,IAAM,CAAC,GAC7CD,IAAO,KAAK,KAAKE,CAAG,IAAIH,KAAM;AAAA,EAChC;AAYA,QAAMK,IAAQ,KAAK,IAAInB,EAAM,OAAOS,CAAC;AAErC,WAASS,IAAI,GAAGA,IAAIC,GAAOD,KAAK;AAC9B,UAAMvB,IAAUK,EAAM,WAAWkB,CAAC,KAAK;AAIvC,IAAAR,EAAO,IAAIJ,EAASX,CAAO,GAAGuB,IAAI,CAAC;AACnC,UAAME,IAAIP,KAAQ,KAAK,KAAKA,EAAKK,CAAC,CAAW,IAAIJ,KAAMC,IAAO;AAC9D,IAAAJ,EAAMO,CAAC,IAAIjB,EAAK,KAAK,CAAC,IAAImB,KAAKnB,EAAK,KAAK,CAAC,IAAIA,EAAK,KAAK,CAAC,IAGzDW,EAAOM,CAAC,IAAIG,EAAYlB,EAAM,MAAMR,CAAO,CAAC;AAAA,EAC9C;AAEA,QAAM2B,IAAQtB,EAAM,MAAM,SAAS,GAC7BuB,IAAa,IAAI,aAAaD,IAAQ,CAAC,GAEvCE,IAAUlC,EAAS,SAASkB,EAAaN,GAAMZ,EAAS,MAAM,IAAI;AACxE,WAASmC,IAAI,GAAGA,IAAIH,GAAOG,KAAK;AAC9B,UAAMC,IAAM1B,EAAM,MAAMyB,IAAI,CAAC,KAAK,GAC5BE,IAAIH,IAAUA,EAAQ,CAAC,IAAKd,EAAOgB,IAAM,CAAC,KAAK,KAC/CE,IAAIJ,IAAUA,EAAQ,CAAC,IAAKd,EAAOgB,IAAM,IAAI,CAAC,KAAK,KACnDG,IAAIL,IAAUA,EAAQ,CAAC,IAAKd,EAAOgB,IAAM,IAAI,CAAC,KAAK;AAMzD,IAAAH,EAAW,IAAI,CAACI,GAAGC,GAAGC,GAAG,CAAC,GAAGJ,IAAI,CAAC;AAAA,EACpC;AAEA,SAAO,EAAE,QAAAf,GAAQ,OAAAC,GAAO,QAAAC,GAAQ,YAAAW,EAAA;AAClC;AAeO,SAASO,EAAaC,GAAcC,GAAyB;AAClE,QAAM,EAAE,4BAAAC,GAA4B,4BAAAC,EAAA,IAA+BH,EAAM;AACzE,SAAO;AAAA,IACL,KAAIE,KAAA,gBAAAA,EAA6BD,OAAU,CAAA,GAAI,IAAI,CAACG,MAASA,EAAK,CAAC,CAAC;AAAA,IACpE,KAAID,KAAA,gBAAAA,EAA6BF,OAAU,CAAA,GAAI,IAAI,CAACG,MAASA,EAAK,CAAC,CAAC;AAAA,EAAA;AAExE;AAGO,SAASC,EAAOC,GAAuB;AAC5C,SAAO;AAAA,IACL,mBAAmBA,EAAI;AAAA,IACvB,qBAAqBA,EAAI;AAAA,IACzB,sBAAsBA,EAAI;AAAA,IAC1B,wBAAwBA,EAAI;AAAA,IAC5B,oBAAoBA,EAAI;AAAA,IACxB,mBAAmBA,EAAI;AAAA,EAAA;AAE3B;AAaO,SAASC,EAAWrC,GAAYC,GAA4B;AACjE,SAAO;AAAA;AAAA;AAAA;AAAA,IAIL,iBAAiBqC,EAAM/B,EAAaN,GAAM,mBAAmB,CAAC;AAAA;AAAA;AAAA,IAG9D,mBAAmB;AAAA,IACnB,aAAaD,EAAK,KAAK;AAAA;AAAA,IAEvB,aAAaA,EAAK,KAAK;AAAA,IACvB,kBAAkBA,EAAK,KAAK;AAAA,IAC5B,cAAcA,EAAK,KAAK;AAAA,IACxB,aAAaA,EAAK,KAAK,QAAQ;AAAA,IAC/B,gCAAgCA,EAAK,KAAK;AAAA,IAC1C,6BAA6BA,EAAK,KAAK;AAAA,IACvC,+BAA+B;AAAA,IAC/B,wBAAwB;AAAA,IACxB,uBAAuBsC,EAAM/B,EAAaN,GAAM,gBAAgB,CAAC;AAAA,IACjE,uBAAuBqC,EAAM/B,EAAaN,GAAM,gBAAgB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAKjE,qBAAqB;AAAA,IACrB,oBAAoB;AAAA,EAAA;AAExB;"}
|
package/dist/graph-sim.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-sim.d.ts","sourceRoot":"","sources":["../src/graph-sim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,iEAAiE;AACjE,MAAM,WAAW,GAAG;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,GAAG,CAatF;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,GAAe,CAAC;AAE1C;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAGpD"}
|
package/dist/graph-sim.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-sim.js","sources":["../src/graph-sim.ts"],"sourcesContent":["/**\n * The force coefficients, and the one function that builds them from the axes a person chose.\n *\n * `graph-looks.ts`'s sibling, and deliberately its twin: a type the renderer consumes, one builder\n * that reads declared string values, and a default that is the builder called with nothing. What was\n * here before is what was there before — an interface, a `DEFAULT_SIM` table beside it, and a dock\n * that held six of them in React state and drew six sliders by hand.\n *\n * **A coefficient is not appearance, and it is still a preference.** `section.ts` says why it shares\n * the manifest: `tokens` are the colours a document may move, `prefs` are what the person on the\n * screen decides, and somebody tuning a layout until it settles is deciding.\n */\n\n/** Force coefficients, handed straight to the GPU simulation. */\nexport interface Sim {\n gravity: number;\n repulsion: number;\n linkSpring: number;\n linkDistance: number;\n friction: number;\n /** Pull toward the node's group position on the cluster ring. Zero lets the links decide alone. */\n cluster: number;\n}\n\n/**\n * Coefficients that settle a few-hundred-node graph into something readable.\n *\n * Chosen against a corpus of that size, and they are a starting point rather than a law: a graph\n * two orders of magnitude larger wants less repulsion and more friction, and the measurements in\n * `BENCHMARKS.md` say a live simulation is finished by around 200,000 points regardless. What a\n * corpus of a given size wants is computed rather than chosen — see `adaptive` — and a host that\n * knows its corpus should start its users at that answer through the tenant policy.\n *\n * The values are strings for the reason `lookFrom`'s are: a contributed preference is a string in\n * all three kinds, so an unrecognised namespace rides through a write untouched. A key that is\n * missing, or that carries a value the section never offered, takes the default below — which is\n * what a graph simulated before any of this existed.\n */\nexport function simFrom(values: Readonly<Record<string, string | undefined>> = {}): Sim {\n const num = (key: string, fallback: number) => {\n const value = Number.parseFloat(values[key] ?? \"\");\n return Number.isFinite(value) ? value : fallback;\n };\n return {\n gravity: num(\"gravity\", 0.14),\n repulsion: num(\"repulsion\", 1.1),\n linkSpring: num(\"link-spring\", 0.6),\n linkDistance: num(\"link-distance\", 18),\n friction: num(\"friction\", 0.86),\n cluster: num(\"cluster\", 0.1),\n };\n}\n\n/**\n * What a canvas simulates with when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be** — for the reason its twin was: it is `simFrom()`, a\n * second public name for a value already available on request, exported only because `sim` took a\n * whole `Sim` and a host changing `gravity` had to supply the other five.\n */\nexport const DEFAULT_SIM: Sim = simFrom();\n\n/**\n * The coefficients a caller wants different, over the ones this package chose.\n *\n * Flat, so a spread is the whole merge. `undefined` returns the shared constant rather than a copy:\n * `useRenderer` re-heats when the forces change and tells them apart by reference, so a fresh object\n * per render would put energy back into a settled layout on every render.\n */\nexport function resolveSim(patch?: Partial<Sim>): Sim {\n if (!patch) return DEFAULT_SIM;\n return { ...DEFAULT_SIM, ...patch };\n}\n"],"names":["simFrom","values","num","key","fallback","value","DEFAULT_SIM","resolveSim","patch"],"mappings":"AAsCO,SAASA,EAAQC,IAAuD,IAAS;AACtF,QAAMC,IAAM,CAACC,GAAaC,MAAqB;AAC7C,UAAMC,IAAQ,OAAO,WAAWJ,EAAOE,CAAG,KAAK,EAAE;AACjD,WAAO,OAAO,SAASE,CAAK,IAAIA,IAAQD;AAAA,EAC1C;AACA,SAAO;AAAA,IACL,SAASF,EAAI,WAAW,IAAI;AAAA,IAC5B,WAAWA,EAAI,aAAa,GAAG;AAAA,IAC/B,YAAYA,EAAI,eAAe,GAAG;AAAA,IAClC,cAAcA,EAAI,iBAAiB,EAAE;AAAA,IACrC,UAAUA,EAAI,YAAY,IAAI;AAAA,IAC9B,SAASA,EAAI,WAAW,GAAG;AAAA,EAAA;AAE/B;AASO,MAAMI,IAAmBN,EAAA;AASzB,SAASO,EAAWC,GAA2B;AACpD,SAAKA,IACE,EAAE,GAAGF,GAAa,GAAGE,EAAA,IADTF;AAErB;"}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/resident.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resident.d.ts","sourceRoot":"","sources":["../src/resident.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAEvC;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,MAAM,CAAA;CAAE,CAAC;AAYnE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,CAE9D;AAED,8FAA8F;AAC9F,wBAAgB,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAE/C;AAED,wBAAgB,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAEhD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,iCAAiC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;IAC9C,0FAA0F;IAC1F,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxC,+EAA+E;IAC/E,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CAAC;IAClD,+FAA+F;IAC/F,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;CACnD;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,QAAQ,CAwCxD"}
|