@kanzo-tech/graph 0.14.0 → 0.15.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.
Files changed (113) hide show
  1. package/README.md +12 -15
  2. package/dist/core/categories.d.ts +18 -6
  3. package/dist/core/categories.d.ts.map +1 -1
  4. package/dist/core/categories.js +41 -10
  5. package/dist/core/categories.js.map +1 -1
  6. package/dist/core/channels.d.ts +10 -5
  7. package/dist/core/channels.d.ts.map +1 -1
  8. package/dist/core/channels.js +11 -13
  9. package/dist/core/channels.js.map +1 -1
  10. package/dist/core/detail.d.ts +14 -8
  11. package/dist/core/detail.d.ts.map +1 -1
  12. package/dist/core/detail.js +33 -15
  13. package/dist/core/detail.js.map +1 -1
  14. package/dist/core/filter.d.ts +3 -1
  15. package/dist/core/filter.d.ts.map +1 -1
  16. package/dist/core/filter.js +54 -50
  17. package/dist/core/filter.js.map +1 -1
  18. package/dist/core/load.d.ts +58 -0
  19. package/dist/core/load.d.ts.map +1 -0
  20. package/dist/core/load.js +115 -0
  21. package/dist/core/load.js.map +1 -0
  22. package/dist/core/state.d.ts +27 -55
  23. package/dist/core/state.d.ts.map +1 -1
  24. package/dist/core/store.d.ts +1 -3
  25. package/dist/core/store.d.ts.map +1 -1
  26. package/dist/core/store.js +160 -175
  27. package/dist/core/store.js.map +1 -1
  28. package/dist/core/types.d.ts +21 -17
  29. package/dist/core/types.d.ts.map +1 -1
  30. package/dist/index.d.ts +5 -7
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +19 -25
  33. package/dist/index.js.map +1 -1
  34. package/dist/parts/gesture.d.ts +1 -10
  35. package/dist/parts/gesture.d.ts.map +1 -1
  36. package/dist/parts/gesture.js +28 -28
  37. package/dist/parts/gesture.js.map +1 -1
  38. package/dist/parts/graph-canvas.d.ts.map +1 -1
  39. package/dist/parts/graph-canvas.js +119 -108
  40. package/dist/parts/graph-canvas.js.map +1 -1
  41. package/dist/parts/graph-inspector.d.ts +4 -4
  42. package/dist/parts/graph-inspector.d.ts.map +1 -1
  43. package/dist/parts/graph-inspector.js +55 -57
  44. package/dist/parts/graph-inspector.js.map +1 -1
  45. package/dist/parts/graph-legend.d.ts +4 -4
  46. package/dist/parts/graph-legend.d.ts.map +1 -1
  47. package/dist/parts/graph-legend.js +21 -21
  48. package/dist/parts/graph-legend.js.map +1 -1
  49. package/dist/parts/overlays.d.ts +2 -4
  50. package/dist/parts/overlays.d.ts.map +1 -1
  51. package/dist/parts/overlays.js +45 -45
  52. package/dist/parts/overlays.js.map +1 -1
  53. package/dist/react/use-graph-state.d.ts +4 -2
  54. package/dist/react/use-graph-state.d.ts.map +1 -1
  55. package/dist/react/use-graph-state.js +10 -6
  56. package/dist/react/use-graph-state.js.map +1 -1
  57. package/dist/react/use-graph.d.ts +1 -4
  58. package/dist/react/use-graph.d.ts.map +1 -1
  59. package/dist/react/use-graph.js +38 -49
  60. package/dist/react/use-graph.js.map +1 -1
  61. package/dist/render/graph-looks.d.ts.map +1 -1
  62. package/dist/render/graph-looks.js.map +1 -1
  63. package/dist/render/graph-model.d.ts +6 -8
  64. package/dist/render/graph-model.d.ts.map +1 -1
  65. package/dist/render/graph-model.js +51 -57
  66. package/dist/render/graph-model.js.map +1 -1
  67. package/dist/render/graph-sim.d.ts +2 -3
  68. package/dist/render/graph-sim.d.ts.map +1 -1
  69. package/dist/render/graph-sim.js.map +1 -1
  70. package/dist/render/renderer.d.ts +6 -11
  71. package/dist/render/renderer.d.ts.map +1 -1
  72. package/dist/render/renderer.js +120 -173
  73. package/dist/render/renderer.js.map +1 -1
  74. package/dist/section.d.ts.map +1 -1
  75. package/dist/section.js +3 -7
  76. package/dist/section.js.map +1 -1
  77. package/package.json +11 -11
  78. package/dist/core/refine.d.ts +0 -14
  79. package/dist/core/refine.d.ts.map +0 -1
  80. package/dist/core/refine.js +0 -20
  81. package/dist/core/refine.js.map +0 -1
  82. package/dist/core/resident.d.ts +0 -79
  83. package/dist/core/resident.d.ts.map +0 -1
  84. package/dist/core/resident.js +0 -44
  85. package/dist/core/resident.js.map +0 -1
  86. package/dist/core/scheduler.d.ts +0 -42
  87. package/dist/core/scheduler.d.ts.map +0 -1
  88. package/dist/core/scheduler.js +0 -77
  89. package/dist/core/scheduler.js.map +0 -1
  90. package/dist/core/tile-matrix.d.ts +0 -37
  91. package/dist/core/tile-matrix.d.ts.map +0 -1
  92. package/dist/core/tile-matrix.js +0 -49
  93. package/dist/core/tile-matrix.js.map +0 -1
  94. package/dist/core/tile.d.ts +0 -54
  95. package/dist/core/tile.d.ts.map +0 -1
  96. package/dist/core/tile.js +0 -76
  97. package/dist/core/tile.js.map +0 -1
  98. package/dist/core/tileset.d.ts +0 -59
  99. package/dist/core/tileset.d.ts.map +0 -1
  100. package/dist/core/tileset.js +0 -177
  101. package/dist/core/tileset.js.map +0 -1
  102. package/dist/render/adaptive.d.ts +0 -27
  103. package/dist/render/adaptive.d.ts.map +0 -1
  104. package/dist/render/adaptive.js +0 -25
  105. package/dist/render/adaptive.js.map +0 -1
  106. package/dist/render/compose.d.ts +0 -51
  107. package/dist/render/compose.d.ts.map +0 -1
  108. package/dist/render/compose.js +0 -107
  109. package/dist/render/compose.js.map +0 -1
  110. package/dist/render/encode.d.ts +0 -42
  111. package/dist/render/encode.d.ts.map +0 -1
  112. package/dist/render/encode.js +0 -68
  113. package/dist/render/encode.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"overlays.js","sources":["../../src/parts/overlays.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { Resident, VertexId } from \"../core/resident\";\n\n/**\n * Everything that floats over the canvas and has to keep up with it: the hub labels, the hover\n * card, and the grid's lock to the graph's own space.\n *\n * One rAF for all three, because they answer the same question — *where is the camera now* — and\n * three independent loops would read the same transform three times a frame. React never runs: the\n * overlays move by imperative style writes, and a re-render per frame would be a re-render per\n * frame.\n *\n * **An overlay is attached to a vertex, not to a slot.** Everything here outlives an answer — a\n * label element is kept across renders, a hover survives a query — so the tracked set is identities\n * and the buffer index is resolved through `Resident` at the moment of painting. Held as indices, a\n * label would keep its position and change which node it was naming the first time the resident set\n * moved, with the text and the dot disagreeing and nothing raised.\n *\n * This lived inside the canvas component among seven other concerns, and that is not a filing\n * detail: the scheduler below once kept a cancelled `requestAnimationFrame` handle in `frame`,\n * which silently disabled every overlay for the life of the page. It took a long time to find in a\n * 700-line component and would have been obvious here.\n */\n\n/**\n * Dot spacing at zoom 1. The painter keeps the on-screen spacing inside [GRID, 2·GRID).\n *\n * **Not on the barrel any more.** Both call sites that imported it wrote the same line —\n * `backgroundSize: \\`${GRID}px ${GRID}px\\`` — as the *initial* value of a style `paint` overwrites\n * on its first frame. So the number was public to spell a value this hook was about to replace, and\n * the effect below writes it instead: the element the hook owns is seeded by the hook that owns it.\n */\nconst GRID = 22;\n\n/** How far the hover card clears its node, and the margin it keeps from the canvas edge. */\nconst CARD_GAP = 14;\nconst CARD_EDGE = 8;\n\n/** How far a label floats above its node, how tall its box is, and the air it demands around it. */\nconst LABEL_LIFT = 10;\nconst LABEL_HEIGHT = 12;\nconst LABEL_GAP = 3;\n\nconst clamp = (value: number, low: number, high: number) =>\n Math.min(Math.max(value, low), Math.max(low, high));\n\nexport interface GraphOverlays {\n /** The box the overlays are positioned within — the canvas' own bounds. */\n hostRef: React.RefObject<HTMLDivElement | null>;\n gridRef: React.RefObject<HTMLDivElement | null>;\n cardRef: React.RefObject<HTMLDivElement | null>;\n /** A `ref` callback for a given vertex's label. */\n labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;\n /** The labelled vertices, in the order the declutter pass should place them. */\n setLabelOrder: (vertices: VertexId[]) => void;\n /** The vertex the card names; its box is measured again on the next paint. */\n setHovered: (vertex: VertexId | null) => void;\n /** Where the hovered point is, in space — from the renderer, never read back from the GPU. */\n hoverAt: (position: [number, number] | null) => void;\n /**\n * Re-register the labelled points with cosmos.gl.\n *\n * Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be\n * driven from outside because effects run in declaration order, so this hook's own effects cannot\n * see a graph that a later hook is about to construct — and that construction is exactly what\n * clears the registration.\n */\n track: () => void;\n /** Ask for a repaint. Coalesced — many calls in a frame cost one. */\n schedule: () => void;\n}\n\n/**\n * The labels, the hover card and the grid, positioned from the renderer every frame: points are\n * tracked by index through the resident map, and every overlay is held by identity.\n */\nexport function useOverlays(api: { getGraph: () => Graph | null; getResident: () => Resident }): GraphOverlays {\n // Both are built once by `useGraph` and are stable for the life of the component, which is what\n // makes them safe to name in the dependency arrays below.\n const { getGraph, getResident } = api;\n const hostRef = useRef<HTMLDivElement>(null);\n const gridRef = useRef<HTMLDivElement>(null);\n const cardRef = useRef<HTMLDivElement>(null);\n const labelEls = useRef(new Map<VertexId, HTMLElement>());\n /** Label widths, measured once each — reading `offsetWidth` every frame would force layout. */\n const widths = useRef(new Map<VertexId, number>());\n const order = useRef<VertexId[]>([]);\n const hoveredRef = useRef<VertexId | null>(null);\n const hoveredAt = useRef<[number, number] | null>(null);\n /** The card's box, measured once per hover, for the same reason. */\n const cardSize = useRef<{ width: number; height: number } | null>(null);\n /** The canvas' own box, kept by a `ResizeObserver` — see the effect below. */\n const box = useRef<{ width: number; height: number } | null>(null);\n const frame = useRef(0);\n\n /**\n * Tell cosmos.gl which points the labels are watching. The hovered one is not among them: a change\n * of tracked set costs a readback, and the renderer already reports where the hovered point is.\n *\n * Registration is *not* self-maintaining. `Points.updatePositions()` ends in an argument-less\n * `trackPointsByIndices()` that clears it, and that runs whenever `isPointPositionsUpdateNeeded`\n * is set — which only `setPointPositions` does. So the registration survives every look change and\n * every slider, and is lost exactly once per graph: at construction, on the `render()` that\n * follows `setPointPositions`. Hence a caller-driven re-register rather than a one-shot.\n */\n const track = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n graph.trackPointPositionsByIndices(getResident().indicesOf(order.current));\n }, [getGraph, getResident]);\n\n const paint = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n const resident = getResident();\n /**\n * Positions come from the tracking API, not from `getPointPositions()`.\n *\n * The difference is what gets read back per frame. `getPointPositions()` is a synchronous\n * `readPixels` of the *whole* position framebuffer — 10,000 bytes at this corpus size, plus an\n * O(n) array build — on every animation frame the simulation runs. Tracking reads a\n * `ceil(√k)²` texture for the k points that actually carry an overlay: 576 bytes for Atlas'\n * 26 labels and a hovered node. It also caches while the simulation is stopped, so a settled\n * graph costs no readback at all until something moves.\n */\n // Read lazily: with labels off and nothing hovered, the only overlay left is the grid, which\n // needs the transform and not the points.\n let positions: ReadonlyMap<number, [number, number]> | null = null;\n /**\n * Where a vertex is on screen, or `null` when it is not drawn at all.\n *\n * Two ways to be absent and they are one answer here: not resident — the query moved on and this\n * vertex is not in the current buffers — or resident and not yet tracked. Both mean *do not draw\n * an overlay for it*, and the alternative to asking is drawing it at whatever the stale index now\n * holds, which is a label on the wrong node.\n */\n const at = (vertex: VertexId): [number, number] | null => {\n const index = resident.indexOf(vertex);\n if (index === undefined) return null;\n positions ??= graph.getTrackedPointPositionsMap();\n return positions.get(index) ?? null;\n };\n\n // Placed boxes, in importance order. A label that would land on one already down is dropped\n // rather than drawn over it — an unreadable pile of overlapping names is worse than a sparser\n // set of legible ones.\n const placed: [number, number, number, number][] = [];\n const bounds = box.current;\n for (const vertex of order.current) {\n const element = labelEls.current.get(vertex);\n if (!element) continue;\n const point = at(vertex);\n if (!point) {\n element.style.opacity = \"0\";\n continue;\n }\n const [x, y] = graph.spaceToScreenPosition(point);\n let width = widths.current.get(vertex);\n if (width === undefined) {\n width = element.offsetWidth;\n widths.current.set(vertex, width);\n }\n const x1 = x - width / 2;\n const x2 = x1 + width;\n const y2 = y - LABEL_LIFT;\n const y1 = y2 - LABEL_HEIGHT;\n const offscreen =\n bounds !== null && (x2 < 0 || y2 < 0 || x1 > bounds.width || y1 > bounds.height);\n const collides = placed.some((r) => x1 < r[2] && x2 > r[0] && y1 < r[3] && y2 > r[1]);\n if (offscreen || collides) {\n element.style.opacity = \"0\";\n continue;\n }\n placed.push([x1 - LABEL_GAP, y1 - LABEL_GAP, x2 + LABEL_GAP, y2 + LABEL_GAP]);\n // Positioned at the box that was just tested, in pixels. The previous version measured a\n // rectangle here and then drew the label somewhere else — `translate(-50%, -160%)` offsets by\n // percentages of the element's own size, so the collision box sat about nine pixels below the\n // text it was meant to protect and neighbouring labels overlapped anyway.\n element.style.transform = `translate(${Math.round(x1)}px, ${Math.round(y1)}px)`;\n element.style.opacity = \"1\";\n }\n // The grid belongs to the graph's space, not to the viewport: it slides with a pan and\n // subdivides on zoom, so the spacing on screen never leaves [GRID, 2·GRID). Without that a\n // fixed grid reads as wallpaper and the canvas stops feeling like somewhere you can move.\n const grid = gridRef.current;\n if (grid) {\n const k = graph.getZoomLevel();\n if (k > 0) {\n const step = (GRID * k) / 2 ** Math.floor(Math.log2(k));\n const [ox, oy] = graph.spaceToScreenPosition([0, 0]);\n const wrap = (v: number) => ((v % step) + step) % step;\n grid.style.backgroundSize = `${step}px ${step}px`;\n grid.style.backgroundPosition = `${wrap(ox)}px ${wrap(oy)}px`;\n }\n }\n\n // The card sits above the node, flips below when the top runs out, and is held inside the\n // canvas on both axes. It used to be centred with percentage transforms, which cannot know\n // about an edge — and since this layer clips, a node near a border showed half a tooltip.\n const card = cardRef.current;\n const hovered = hoveredRef.current;\n if (card && hovered !== null && bounds) {\n const point = resident.indexOf(hovered) === undefined ? null : hoveredAt.current;\n if (point) {\n const [x, y] = graph.spaceToScreenPosition(point);\n let size = cardSize.current;\n if (!size) {\n size = { width: card.offsetWidth, height: card.offsetHeight };\n cardSize.current = size;\n }\n const above = y - CARD_GAP - size.height;\n const below = y + CARD_GAP;\n const top = above >= CARD_EDGE ? above : below;\n card.style.transform = `translate(${Math.round(\n clamp(x - size.width / 2, CARD_EDGE, bounds.width - size.width - CARD_EDGE),\n )}px, ${Math.round(clamp(top, CARD_EDGE, bounds.height - size.height - CARD_EDGE))}px)`;\n card.style.opacity = \"1\";\n }\n }\n }, [getGraph, getResident]);\n\n // The canvas' box, measured when it changes rather than when it is read. `getBoundingClientRect()`\n // inside `paint` was one forced layout per animation frame, in a painter that caches `offsetWidth`\n // for exactly that reason.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const observer = new ResizeObserver(([entry]) => {\n const size = entry?.contentRect;\n if (size) box.current = { width: size.width, height: size.height };\n });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n /**\n * The grid's spacing at rest, written before the camera has said anything.\n *\n * `paint` sets this every frame from the live zoom, but only once there is a graph and a zoom to\n * read — and the element is mounted well before that. The two hosts that drew a grid were each\n * writing this same line inline off an exported `GRID`, which is a constant published so a call\n * site could spell the value this hook was about to overwrite. Seeding it here is the same picture\n * with the number staying where the painter that maintains it lives.\n *\n * The `backgroundImage` is not seeded: what the dots are made of is the host's decision — the\n * border colour, a gradient, whatever the surface wants — and only the *spacing* has to agree with\n * the camera.\n */\n useEffect(() => {\n const grid = gridRef.current;\n if (grid) grid.style.backgroundSize = `${GRID}px ${GRID}px`;\n }, []);\n\n const schedule = useCallback(() => {\n if (frame.current) return;\n frame.current = requestAnimationFrame(() => {\n frame.current = 0;\n paint();\n });\n }, [paint]);\n\n useEffect(\n () => () => {\n if (frame.current) cancelAnimationFrame(frame.current);\n // Clearing the handle is the whole point of this cleanup, not the cancel. `frame` doubles as\n // the \"a paint is already queued\" flag, and StrictMode runs setup → cleanup → setup on the\n // same instance, so the refs survive. Leaving a cancelled handle behind made every later\n // `schedule()` believe a frame was still pending and return early — permanently.\n frame.current = 0;\n },\n [],\n );\n\n const labelRef = useCallback(\n (vertex: VertexId) => (element: HTMLElement | null) => {\n if (element) labelEls.current.set(vertex, element);\n else labelEls.current.delete(vertex);\n },\n [],\n );\n\n const setLabelOrder = useCallback((vertices: VertexId[]) => {\n order.current = vertices;\n widths.current.clear();\n }, []);\n\n const setHovered = useCallback((vertex: VertexId | null) => {\n hoveredRef.current = vertex;\n cardSize.current = null;\n }, []);\n\n const hoverAt = useCallback(\n (position: [number, number] | null) => {\n hoveredAt.current = position;\n schedule();\n },\n [schedule],\n );\n\n return { hostRef, gridRef, cardRef, labelRef, setLabelOrder, setHovered, hoverAt, track, schedule };\n}\n"],"names":[],"mappings":";;AAmCA;AA4CO;AAGL;AA2BE;AACA;AACyE;AAIzE;AACA;AACA;AAaA;AASA;AACE;AACA;AAE+B;AAQjC;AACE;AACA;AACA;AACA;AACE;AACA;AAAA;AAEF;AACA;AACA;AAIA;AAOA;AACE;AACA;AAAA;AAEF;AAMwB;AAK1B;AACA;AACE;AACA;AACE;AAGA;AACyD;AAC3D;AAMF;AAEA;AACE;AACA;AACE;AACA;AACA;AAIA;AAGA;AAAyC;AACmC;AAEvD;AACvB;AACF;AAMF;AACE;AACA;AACA;AACE;AACA;AAA0D;AAE5D;AACsB;AAiBtB;AACA;AAAuD;AAGzD;AACE;AAEE;AACA;AACD;AAGH;AAAA;AAEI;AAKgB;AAClB;AACA;AAGF;AAAiB;AAEb;AACmC;AACrC;AACA;AAIA;AACe;AAIf;AACmB;AAGL;AAEZ;AACA;AACF;AACS;AAGX;AACF;;;;"}
1
+ {"version":3,"file":"overlays.js","sources":["../../src/parts/overlays.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { VertexId } from \"../core/types\";\n\n/**\n * Everything that floats over the canvas and has to keep up with it: the hub labels, the hover\n * card, and the grid's lock to the graph's own space.\n *\n * One rAF for all three, because they answer the same question — *where is the camera now* — and\n * three independent loops would read the same transform three times a frame. React never runs: the\n * overlays move by imperative style writes, and a re-render per frame would be a re-render per\n * frame.\n *\n * **An overlay is attached to a vertex**, and a vertex's id is its index in the buffers, so the\n * tracked set is the ids themselves. A vertex the page's filter hides has no position, and no overlay.\n *\n * This lived inside the canvas component among seven other concerns, and that is not a filing\n * detail: the scheduler below once kept a cancelled `requestAnimationFrame` handle in `frame`,\n * which silently disabled every overlay for the life of the page. It took a long time to find in a\n * 700-line component and would have been obvious here.\n */\n\n/**\n * Dot spacing at zoom 1. The painter keeps the on-screen spacing inside [GRID, 2·GRID).\n *\n * **Not on the barrel any more.** Both call sites that imported it wrote the same line —\n * `backgroundSize: \\`${GRID}px ${GRID}px\\`` — as the *initial* value of a style `paint` overwrites\n * on its first frame. So the number was public to spell a value this hook was about to replace, and\n * the effect below writes it instead: the element the hook owns is seeded by the hook that owns it.\n */\nconst GRID = 22;\n\n/** How far the hover card clears its node, and the margin it keeps from the canvas edge. */\nconst CARD_GAP = 14;\nconst CARD_EDGE = 8;\n\n/** How far a label floats above its node, how tall its box is, and the air it demands around it. */\nconst LABEL_LIFT = 10;\nconst LABEL_HEIGHT = 12;\nconst LABEL_GAP = 3;\n\nconst clamp = (value: number, low: number, high: number) =>\n Math.min(Math.max(value, low), Math.max(low, high));\n\nexport interface GraphOverlays {\n /** The box the overlays are positioned within — the canvas' own bounds. */\n hostRef: React.RefObject<HTMLDivElement | null>;\n gridRef: React.RefObject<HTMLDivElement | null>;\n cardRef: React.RefObject<HTMLDivElement | null>;\n /** A `ref` callback for a given vertex's label. */\n labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;\n /** The labelled vertices, in the order the declutter pass should place them. */\n setLabelOrder: (vertices: VertexId[]) => void;\n /** The vertex the card names; its box is measured again on the next paint. */\n setHovered: (vertex: VertexId | null) => void;\n /** Where the hovered point is, in space — from the renderer, never read back from the GPU. */\n hoverAt: (position: [number, number] | null) => void;\n /**\n * Re-register the labelled points with cosmos.gl.\n *\n * Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be\n * driven from outside because effects run in declaration order, so this hook's own effects cannot\n * see a graph that a later hook is about to construct — and that construction is exactly what\n * clears the registration.\n */\n track: () => void;\n /** Ask for a repaint. Coalesced — many calls in a frame cost one. */\n schedule: () => void;\n}\n\n/**\n * The labels, the hover card and the grid, positioned from the renderer every frame.\n */\nexport function useOverlays(api: { getGraph: () => Graph | null }): GraphOverlays {\n // Built once by the canvas and stable for its life, which is what makes it safe to name in the\n // dependency arrays below.\n const { getGraph } = api;\n const hostRef = useRef<HTMLDivElement>(null);\n const gridRef = useRef<HTMLDivElement>(null);\n const cardRef = useRef<HTMLDivElement>(null);\n const labelEls = useRef(new Map<VertexId, HTMLElement>());\n /** Label widths, measured once each — reading `offsetWidth` every frame would force layout. */\n const widths = useRef(new Map<VertexId, number>());\n const order = useRef<VertexId[]>([]);\n const hoveredRef = useRef<VertexId | null>(null);\n const hoveredAt = useRef<[number, number] | null>(null);\n /** The card's box, measured once per hover, for the same reason. */\n const cardSize = useRef<{ width: number; height: number } | null>(null);\n /** The canvas' own box, kept by a `ResizeObserver` — see the effect below. */\n const box = useRef<{ width: number; height: number } | null>(null);\n const frame = useRef(0);\n\n /**\n * Tell cosmos.gl which points the labels are watching. The hovered one is not among them: a change\n * of tracked set costs a readback, and the renderer already reports where the hovered point is.\n *\n * Registration is *not* self-maintaining. `Points.updatePositions()` ends in an argument-less\n * `trackPointsByIndices()` that clears it, and that runs whenever `isPointPositionsUpdateNeeded`\n * is set — which only `setPointPositions` does. So the registration survives every look change and\n * every slider, and is lost exactly once per graph: at construction, on the `render()` that\n * follows `setPointPositions`. Hence a caller-driven re-register rather than a one-shot.\n */\n const track = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n graph.trackPointPositionsByIndices(order.current);\n }, [getGraph]);\n\n const paint = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n /**\n * Positions come from the tracking API, not from `getPointPositions()`.\n *\n * The difference is what gets read back per frame. `getPointPositions()` is a synchronous\n * `readPixels` of the *whole* position framebuffer — 10,000 bytes at this corpus size, plus an\n * O(n) array build — on every animation frame the simulation runs. Tracking reads a\n * `ceil(√k)²` texture for the k points that actually carry an overlay: 576 bytes for Atlas'\n * 26 labels and a hovered node. It also caches while the simulation is stopped, so a settled\n * graph costs no readback at all until something moves.\n */\n // Read lazily: with labels off and nothing hovered, the only overlay left is the grid, which\n // needs the transform and not the points.\n let positions: ReadonlyMap<number, [number, number]> | null = null;\n /** Where a vertex is in space, or `null` when it is not tracked yet or has no position. */\n const at = (vertex: VertexId): [number, number] | null => {\n positions ??= graph.getTrackedPointPositionsMap();\n const point = positions.get(vertex);\n return point && !Number.isNaN(point[0]) ? point : null;\n };\n\n // Placed boxes, in importance order. A label that would land on one already down is dropped\n // rather than drawn over it — an unreadable pile of overlapping names is worse than a sparser\n // set of legible ones.\n const placed: [number, number, number, number][] = [];\n const bounds = box.current;\n for (const vertex of order.current) {\n const element = labelEls.current.get(vertex);\n if (!element) continue;\n const point = at(vertex);\n if (!point) {\n element.style.opacity = \"0\";\n continue;\n }\n const [x, y] = graph.spaceToScreenPosition(point);\n let width = widths.current.get(vertex);\n if (width === undefined) {\n width = element.offsetWidth;\n widths.current.set(vertex, width);\n }\n const x1 = x - width / 2;\n const x2 = x1 + width;\n const y2 = y - LABEL_LIFT;\n const y1 = y2 - LABEL_HEIGHT;\n const offscreen =\n bounds !== null && (x2 < 0 || y2 < 0 || x1 > bounds.width || y1 > bounds.height);\n const collides = placed.some((r) => x1 < r[2] && x2 > r[0] && y1 < r[3] && y2 > r[1]);\n if (offscreen || collides) {\n element.style.opacity = \"0\";\n continue;\n }\n placed.push([x1 - LABEL_GAP, y1 - LABEL_GAP, x2 + LABEL_GAP, y2 + LABEL_GAP]);\n // Positioned at the box that was just tested, in pixels. The previous version measured a\n // rectangle here and then drew the label somewhere else — `translate(-50%, -160%)` offsets by\n // percentages of the element's own size, so the collision box sat about nine pixels below the\n // text it was meant to protect and neighbouring labels overlapped anyway.\n element.style.transform = `translate(${Math.round(x1)}px, ${Math.round(y1)}px)`;\n element.style.opacity = \"1\";\n }\n // The grid belongs to the graph's space, not to the viewport: it slides with a pan and\n // subdivides on zoom, so the spacing on screen never leaves [GRID, 2·GRID). Without that a\n // fixed grid reads as wallpaper and the canvas stops feeling like somewhere you can move.\n const grid = gridRef.current;\n if (grid) {\n const k = graph.getZoomLevel();\n if (k > 0) {\n const step = (GRID * k) / 2 ** Math.floor(Math.log2(k));\n const [ox, oy] = graph.spaceToScreenPosition([0, 0]);\n const wrap = (v: number) => ((v % step) + step) % step;\n grid.style.backgroundSize = `${step}px ${step}px`;\n grid.style.backgroundPosition = `${wrap(ox)}px ${wrap(oy)}px`;\n }\n }\n\n // The card sits above the node, flips below when the top runs out, and is held inside the\n // canvas on both axes. It used to be centred with percentage transforms, which cannot know\n // about an edge — and since this layer clips, a node near a border showed half a tooltip.\n const card = cardRef.current;\n const hovered = hoveredRef.current;\n if (card && hovered !== null && bounds) {\n const point = hoveredAt.current;\n if (point) {\n const [x, y] = graph.spaceToScreenPosition(point);\n let size = cardSize.current;\n if (!size) {\n size = { width: card.offsetWidth, height: card.offsetHeight };\n cardSize.current = size;\n }\n const above = y - CARD_GAP - size.height;\n const below = y + CARD_GAP;\n const top = above >= CARD_EDGE ? above : below;\n card.style.transform = `translate(${Math.round(\n clamp(x - size.width / 2, CARD_EDGE, bounds.width - size.width - CARD_EDGE),\n )}px, ${Math.round(clamp(top, CARD_EDGE, bounds.height - size.height - CARD_EDGE))}px)`;\n card.style.opacity = \"1\";\n }\n }\n }, [getGraph]);\n\n // The canvas' box, measured when it changes rather than when it is read. `getBoundingClientRect()`\n // inside `paint` was one forced layout per animation frame, in a painter that caches `offsetWidth`\n // for exactly that reason.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const observer = new ResizeObserver(([entry]) => {\n const size = entry?.contentRect;\n if (size) box.current = { width: size.width, height: size.height };\n });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n /**\n * The grid's spacing at rest, written before the camera has said anything.\n *\n * `paint` sets this every frame from the live zoom, but only once there is a graph and a zoom to\n * read — and the element is mounted well before that. The two hosts that drew a grid were each\n * writing this same line inline off an exported `GRID`, which is a constant published so a call\n * site could spell the value this hook was about to overwrite. Seeding it here is the same picture\n * with the number staying where the painter that maintains it lives.\n *\n * The `backgroundImage` is not seeded: what the dots are made of is the host's decision — the\n * border colour, a gradient, whatever the surface wants — and only the *spacing* has to agree with\n * the camera.\n */\n useEffect(() => {\n const grid = gridRef.current;\n if (grid) grid.style.backgroundSize = `${GRID}px ${GRID}px`;\n }, []);\n\n const schedule = useCallback(() => {\n if (frame.current) return;\n frame.current = requestAnimationFrame(() => {\n frame.current = 0;\n paint();\n });\n }, [paint]);\n\n useEffect(\n () => () => {\n if (frame.current) cancelAnimationFrame(frame.current);\n // Clearing the handle is the whole point of this cleanup, not the cancel. `frame` doubles as\n // the \"a paint is already queued\" flag, and StrictMode runs setup → cleanup → setup on the\n // same instance, so the refs survive. Leaving a cancelled handle behind made every later\n // `schedule()` believe a frame was still pending and return early — permanently.\n frame.current = 0;\n },\n [],\n );\n\n const labelRef = useCallback(\n (vertex: VertexId) => (element: HTMLElement | null) => {\n if (element) labelEls.current.set(vertex, element);\n else labelEls.current.delete(vertex);\n },\n [],\n );\n\n const setLabelOrder = useCallback((vertices: VertexId[]) => {\n order.current = vertices;\n widths.current.clear();\n }, []);\n\n const setHovered = useCallback((vertex: VertexId | null) => {\n hoveredRef.current = vertex;\n cardSize.current = null;\n }, []);\n\n const hoverAt = useCallback(\n (position: [number, number] | null) => {\n hoveredAt.current = position;\n schedule();\n },\n [schedule],\n );\n\n return { hostRef, gridRef, cardRef, labelRef, setLabelOrder, setHovered, hoverAt, track, schedule };\n}\n"],"names":[],"mappings":";;AAgCA;AA2CO;AAGL;AA2BE;AACA;AACgD;AAIhD;AACA;AAaA;AAEA;AACE;AACA;AACA;AAAkD;AAQpD;AACE;AACA;AACA;AACA;AACE;AACA;AAAA;AAEF;AACA;AACA;AAIA;AAOA;AACE;AACA;AAAA;AAEF;AAMwB;AAK1B;AACA;AACE;AACA;AACE;AAGA;AACyD;AAC3D;AAMF;AAEA;AACE;AACA;AACE;AACA;AACA;AAIA;AAGA;AAAyC;AACmC;AAEvD;AACvB;AACF;AAMF;AACE;AACA;AACA;AACE;AACA;AAA0D;AAE5D;AACsB;AAiBtB;AACA;AAAuD;AAGzD;AACE;AAEE;AACA;AACD;AAGH;AAAA;AAEI;AAKgB;AAClB;AACA;AAGF;AAAiB;AAEb;AACmC;AACrC;AACA;AAIA;AACe;AAIf;AACmB;AAGL;AAEZ;AACA;AACF;AACS;AAGX;AACF;;;;"}
@@ -1,11 +1,13 @@
1
- import { GraphState } from '../core/store';
1
+ import { GraphSnapshot, GraphState } from '../core/store';
2
2
  /**
3
3
  * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's
4
4
  * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a
5
- * tile, a tick of the layout — so reading all of it would re-render a toolbar on every hover.
5
+ * load, a tick of the layout — so reading all of it would re-render a toolbar on every hover.
6
6
  *
7
7
  * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice
8
8
  * equals the last one, so a selector may build an object as long as it passes a comparison for it.
9
9
  */
10
10
  export declare function useGraphState<T>(selector: (state: GraphState) => T, isEqual?: (a: T, b: T) => boolean): T;
11
+ /** The parts' selector over what only they and the renderer read. Not on the barrel. */
12
+ export declare function useGraphSnapshot<T>(selector: (state: GraphSnapshot) => T): T;
11
13
  //# sourceMappingURL=use-graph-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-state.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,EAAE,OAAO,GAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAmB,GAAG,CAAC,CAgBpH"}
1
+ {"version":3,"file":"use-graph-state.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG/D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,EAAE,OAAO,GAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAmB,GAAG,CAAC,CAgBpH;AAED,wFAAwF;AACxF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,CAAC,GAAG,CAAC,CAE5E"}
@@ -1,16 +1,20 @@
1
1
  "use client";
2
2
  import { useRef as c, useSyncExternalStore as l } from "react";
3
3
  import { useGraphContext as i } from "./graph-root.js";
4
- function p(s, o = Object.is) {
5
- const n = i(), r = c(null), a = () => {
6
- const e = n.getState(), t = r.current;
4
+ function p(r, n = Object.is) {
5
+ const s = i(), u = c(null), o = () => {
6
+ const e = s.getState(), t = u.current;
7
7
  if (t && t.state === e) return t.value;
8
- const u = s(e);
9
- return t && o(t.value, u) ? (r.current = { state: e, value: t.value }, t.value) : (r.current = { state: e, value: u }, u);
8
+ const a = r(e);
9
+ return t && n(t.value, a) ? (u.current = { state: e, value: t.value }, t.value) : (u.current = { state: e, value: a }, a);
10
10
  };
11
- return l(n.subscribe, a, a);
11
+ return l(s.subscribe, o, o);
12
+ }
13
+ function v(r) {
14
+ return p((n) => r(n));
12
15
  }
13
16
  export {
17
+ v as useGraphSnapshot,
14
18
  p as useGraphState
15
19
  };
16
20
  //# sourceMappingURL=use-graph-state.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-state.js","sources":["../../src/react/use-graph-state.ts"],"sourcesContent":["\"use client\";\n\nimport { useRef, useSyncExternalStore } from \"react\";\nimport type { GraphState } from \"../core/store\";\nimport { useGraphContext } from \"./graph-root\";\n\n/**\n * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's\n * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a\n * tile, a tick of the layout — so reading all of it would re-render a toolbar on every hover.\n *\n * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice\n * equals the last one, so a selector may build an object as long as it passes a comparison for it.\n */\nexport function useGraphState<T>(selector: (state: GraphState) => T, isEqual: (a: T, b: T) => boolean = Object.is): T {\n const api = useGraphContext();\n const memo = useRef<{ state: GraphState; value: T } | null>(null);\n const read = () => {\n const state = api.getState();\n const last = memo.current;\n if (last && last.state === state) return last.value;\n const value = selector(state);\n if (last && isEqual(last.value, value)) {\n memo.current = { state, value: last.value };\n return last.value;\n }\n memo.current = { state, value };\n return value;\n };\n return useSyncExternalStore(api.subscribe, read, read);\n}\n"],"names":[],"mappings":";;;AAcO;AACL;AAGE;AAEA;AACA;AACA;AAKO;AAET;AACF;;;;"}
1
+ {"version":3,"file":"use-graph-state.js","sources":["../../src/react/use-graph-state.ts"],"sourcesContent":["\"use client\";\n\nimport { useRef, useSyncExternalStore } from \"react\";\nimport type { GraphSnapshot, GraphState } from \"../core/store\";\nimport { useGraphContext } from \"./graph-root\";\n\n/**\n * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's\n * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a\n * load, a tick of the layout — so reading all of it would re-render a toolbar on every hover.\n *\n * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice\n * equals the last one, so a selector may build an object as long as it passes a comparison for it.\n */\nexport function useGraphState<T>(selector: (state: GraphState) => T, isEqual: (a: T, b: T) => boolean = Object.is): T {\n const api = useGraphContext();\n const memo = useRef<{ state: GraphState; value: T } | null>(null);\n const read = () => {\n const state = api.getState();\n const last = memo.current;\n if (last && last.state === state) return last.value;\n const value = selector(state);\n if (last && isEqual(last.value, value)) {\n memo.current = { state, value: last.value };\n return last.value;\n }\n memo.current = { state, value };\n return value;\n };\n return useSyncExternalStore(api.subscribe, read, read);\n}\n\n/** The parts' selector over what only they and the renderer read. Not on the barrel. */\nexport function useGraphSnapshot<T>(selector: (state: GraphSnapshot) => T): T {\n return useGraphState((state) => selector(state as GraphSnapshot));\n}\n"],"names":[],"mappings":";;;AAcO;AACL;AAGE;AAEA;AACA;AACA;AAKO;AAET;AACF;AAGO;AACL;AACF;;;;;"}
@@ -1,6 +1,5 @@
1
- import { Resident, VertexId } from '../core/resident';
2
1
  import { GraphOptions, GraphState, GraphStore } from '../core/store';
3
- import { GraphCommands, SelectionSource, Tool } from '../core/types';
2
+ import { GraphCommands, SelectionSource, Tool, VertexId } from '../core/types';
4
3
  import { Renderer, RendererEvents } from '../render/renderer';
5
4
  export type UseGraphProps = GraphOptions;
6
5
  /**
@@ -16,8 +15,6 @@ export interface GraphApi extends GraphCommands {
16
15
  subscribe(listener: () => void): () => void;
17
16
  /** The state now, for a callback; a render reads it with `useGraphState`. */
18
17
  getState(): GraphState;
19
- /** Who is drawn right now, to resolve a buffer index to an identity. */
20
- getResident(): Resident;
21
18
  }
22
19
  /** What the parts in this package reach and a host does not: the element and the renderer. */
23
20
  interface Internals {
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph.d.ts","sourceRoot":"","sources":["../../src/react/use-graph.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAkB,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExF,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,MAAM,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7F,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,6EAA6E;IAC7E,QAAQ,IAAI,UAAU,CAAC;IACvB,wEAAwE;IACxE,WAAW,IAAI,QAAQ,CAAC;CACzB;AAED,8FAA8F;AAC9F,UAAU,SAAS;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IAClE,QAAQ,IAAI,QAAQ,GAAG,IAAI,CAAC;CAC7B;AAID,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,CAIpD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CAyBvD"}
1
+ {"version":3,"file":"use-graph.d.ts","sourceRoot":"","sources":["../../src/react/use-graph.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EAAkB,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExF,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,MAAM,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7F,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,6EAA6E;IAC7E,QAAQ,IAAI,UAAU,CAAC;CACxB;AAED,8FAA8F;AAC9F,UAAU,SAAS;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IAClE,QAAQ,IAAI,QAAQ,GAAG,IAAI,CAAC;CAC7B;AAID,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,CAIpD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CAyBvD"}
@@ -1,87 +1,76 @@
1
1
  "use client";
2
- import { useRef as R, useCallback as k, useState as v, useEffect as l } from "react";
3
- import { createGraph as N } from "../core/store.js";
4
- import { createRenderer as z } from "../render/renderer.js";
2
+ import { useRef as k, useCallback as R, useState as B, useEffect as i } from "react";
3
+ import { createGraph as E } from "../core/store.js";
4
+ import { createRenderer as G } from "../render/renderer.js";
5
5
  const f = /* @__PURE__ */ new WeakMap();
6
- function E(n) {
6
+ function N(n) {
7
7
  const e = f.get(n);
8
8
  if (!e) throw new Error("a graph part was given an api useGraph did not build");
9
9
  return e;
10
10
  }
11
- function D(n) {
12
- const e = R(n);
11
+ function A(n) {
12
+ const e = k(n);
13
13
  e.current = n;
14
- const s = k(
14
+ const r = R(
15
15
  (F) => ({
16
16
  ...F,
17
- onFailure: (u) => e.current.onFailure(u),
18
- onSelect: (u) => {
19
- var r, a;
20
- return (a = (r = e.current).onSelect) == null ? void 0 : a.call(r, u);
17
+ onFailure: (s) => e.current.onFailure(s),
18
+ onSelect: (s) => {
19
+ var l, a;
20
+ return (a = (l = e.current).onSelect) == null ? void 0 : a.call(l, s);
21
21
  },
22
- onFocus: (u) => {
23
- var r, a;
24
- return (a = (r = e.current).onFocus) == null ? void 0 : a.call(r, u);
22
+ onFocus: (s) => {
23
+ var l, a;
24
+ return (a = (l = e.current).onFocus) == null ? void 0 : a.call(l, s);
25
25
  }
26
26
  }),
27
27
  []
28
- ), [i] = v(() => G(N(s(n)))), { store: t } = E(i), { categories: c, corpus: o, fill: m, filterBy: p, limit: d, look: b, r: S, sim: g, simulate: h, stroke: y, symbol: B, title: O, type: w } = n;
29
- return l(() => {
30
- t.setOptions(s(e.current));
31
- }, [t, s, c, o, m, p, d, b, S, g, h, y, B, O, w]), l(() => t.subscribe(() => {
32
- }), [t]), i;
28
+ ), [c] = B(() => T(E(r(n)))), { store: t } = N(c), { categories: u, corpus: o, fill: m, filterBy: p, look: b, r: d, sim: S, simulate: h, stroke: g, symbol: w, title: y } = n;
29
+ return i(() => {
30
+ t.setOptions(r(e.current));
31
+ }, [t, r, u, o, m, p, b, d, S, h, g, w, y]), i(() => t.subscribe(() => {
32
+ }), [t]), c;
33
33
  }
34
- function G(n) {
34
+ function T(n) {
35
35
  let e = null;
36
- const s = (t) => (...c) => {
36
+ const r = (t) => (...u) => {
37
37
  var o;
38
- return (o = e == null ? void 0 : e[t]) == null ? void 0 : o.call(e, ...c);
39
- }, i = {
40
- zoomBy: s("zoomBy"),
41
- fit: s("fit"),
42
- frameBox: s("frameBox"),
43
- pause: s("pause"),
44
- resume: s("resume"),
45
- restart: s("restart"),
46
- unpin: s("unpin"),
38
+ return (o = e == null ? void 0 : e[t]) == null ? void 0 : o.call(e, ...u);
39
+ }, c = {
40
+ zoomBy: r("zoomBy"),
41
+ fit: r("fit"),
42
+ pause: r("pause"),
43
+ resume: r("resume"),
44
+ restart: r("restart"),
45
+ unpin: r("unpin"),
47
46
  // Selecting and focusing are state, and hold without a renderer; centring is the camera's.
48
47
  reveal: (t) => {
49
48
  if (e) return e.reveal(t);
50
49
  n.select([t], "node", "Node"), n.focus(t);
51
50
  },
52
- frameSelection: s("frameSelection"),
51
+ frameSelection: r("frameSelection"),
53
52
  clear: () => {
54
53
  n.select(null), n.focus(null);
55
54
  },
56
- select: (t, c, o) => n.select(t, c, o),
55
+ select: (t, u, o) => n.select(t, u, o),
57
56
  setFocus: (t) => n.focus(t),
58
57
  setTool: (t) => n.setTool(t),
59
58
  subscribe: (t) => n.subscribe(t),
60
- getState: () => n.getSnapshot(),
61
- getResident: () => (e == null ? void 0 : e.resident()) ?? T
59
+ getState: () => n.getSnapshot()
62
60
  };
63
- return f.set(i, {
61
+ return f.set(c, {
64
62
  store: n,
65
63
  renderer: () => e,
66
- attach(t, c) {
67
- const o = z(t, n, c);
64
+ attach(t, u) {
65
+ const o = G(t, n, u);
68
66
  return e = o, n.setRenderable(o !== null), () => {
69
67
  o == null || o.destroy(), e === o && (e = null);
70
68
  };
71
69
  }
72
- }), i;
70
+ }), c;
73
71
  }
74
- const T = {
75
- size: 0,
76
- indexOf: () => {
77
- },
78
- at: () => {
79
- },
80
- indicesOf: () => [],
81
- verticesAt: () => []
82
- };
83
72
  export {
84
- E as internalsOf,
85
- D as useGraph
73
+ N as internalsOf,
74
+ A as useGraph
86
75
  };
87
76
  //# sourceMappingURL=use-graph.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Resident, VertexId } from \"../core/resident\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n /** Who is drawn right now, to resolve a buffer index to an identity. */\n getResident(): Resident;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (message) => latest.current.onFailure(message),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories, corpus, fill, filterBy, limit, look, r, sim, simulate, stroke, symbol, title, type } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, corpus, fill, filterBy, limit, look, r, sim, simulate, stroke, symbol, title, type]);\n\n // Subscribed here as well as by the parts, so the store's first-subscriber and last-subscriber\n // moves follow the root's lifetime and not whichever part happened to mount first.\n useEffect(() => store.subscribe(() => {}), [store]);\n\n return api;\n}\n\nfunction build(store: GraphStore): GraphApi {\n let renderer: Renderer | null = null;\n const on =\n <K extends keyof GraphCommands>(name: K) =>\n (...args: Parameters<GraphCommands[K]>) =>\n (renderer?.[name] as ((...a: Parameters<GraphCommands[K]>) => void) | undefined)?.(...args);\n const api: GraphApi = {\n zoomBy: on(\"zoomBy\"),\n fit: on(\"fit\"),\n frameBox: on(\"frameBox\"),\n pause: on(\"pause\"),\n resume: on(\"resume\"),\n restart: on(\"restart\"),\n unpin: on(\"unpin\"),\n // Selecting and focusing are state, and hold without a renderer; centring is the camera's.\n reveal: (vertex) => {\n if (renderer) return renderer.reveal(vertex);\n store.select([vertex], \"node\", \"Node\");\n store.focus(vertex);\n },\n frameSelection: on(\"frameSelection\"),\n clear: () => {\n store.select(null);\n store.focus(null);\n },\n select: (vertices, source, label) => store.select(vertices, source, label),\n setFocus: (vertex) => store.focus(vertex),\n setTool: (tool) => store.setTool(tool),\n subscribe: (listener) => store.subscribe(listener),\n getState: () => store.getSnapshot(),\n getResident: () => renderer?.resident() ?? NOBODY,\n };\n INTERNALS.set(api, {\n store,\n renderer: () => renderer,\n attach(host, events) {\n const mounted = createRenderer(host, store, events);\n renderer = mounted;\n store.setRenderable(mounted !== null);\n return () => {\n mounted?.destroy();\n if (renderer === mounted) renderer = null;\n };\n },\n });\n return api;\n}\n\nconst NOBODY: Resident = {\n size: 0,\n indexOf: () => undefined,\n at: () => undefined,\n indicesOf: () => [],\n verticesAt: () => [],\n};\n"],"names":[],"mappings":";;;;AAkCA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACA;AACA;AAAgB;AAC8B;AACvC;AACqD;;AAC/B;AAA0B;AAAA;;AAC9B;AAAyB;AAAA;AAAM;AAEtD;AAMF;AACE;AAAwC;AAKJ;AAGxC;AAEA;AACE;AACA;;AAGK;AAAqF;AACpE;AACD;AACN;AACU;AACN;AACE;AACE;AACJ;AAAA;AAGf;AACA;AACkB;AACpB;AACmC;AAEjC;AACgB;AAClB;AACyE;AACjC;AACH;AACY;AAC3B;AACqB;AAE7C;AAAmB;AACjB;AACgB;AAEd;AACA;AAGE;AACqC;AACvC;AACF;AAGJ;AAEA;AAAyB;AACjB;AACG;AAAA;AACL;AAAA;AACa;AAEnB;;;;;"}
1
+ {"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool, VertexId } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (message) => latest.current.onFailure(message),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title]);\n\n // Subscribed here as well as by the parts, so the store's first-subscriber and last-subscriber\n // moves follow the root's lifetime and not whichever part happened to mount first.\n useEffect(() => store.subscribe(() => {}), [store]);\n\n return api;\n}\n\nfunction build(store: GraphStore): GraphApi {\n let renderer: Renderer | null = null;\n const on =\n <K extends keyof GraphCommands>(name: K) =>\n (...args: Parameters<GraphCommands[K]>) =>\n (renderer?.[name] as ((...a: Parameters<GraphCommands[K]>) => void) | undefined)?.(...args);\n const api: GraphApi = {\n zoomBy: on(\"zoomBy\"),\n fit: on(\"fit\"),\n pause: on(\"pause\"),\n resume: on(\"resume\"),\n restart: on(\"restart\"),\n unpin: on(\"unpin\"),\n // Selecting and focusing are state, and hold without a renderer; centring is the camera's.\n reveal: (vertex) => {\n if (renderer) return renderer.reveal(vertex);\n store.select([vertex], \"node\", \"Node\");\n store.focus(vertex);\n },\n frameSelection: on(\"frameSelection\"),\n clear: () => {\n store.select(null);\n store.focus(null);\n },\n select: (vertices, source, label) => store.select(vertices, source, label),\n setFocus: (vertex) => store.focus(vertex),\n setTool: (tool) => store.setTool(tool),\n subscribe: (listener) => store.subscribe(listener),\n getState: () => store.getSnapshot(),\n };\n INTERNALS.set(api, {\n store,\n renderer: () => renderer,\n attach(host, events) {\n const mounted = createRenderer(host, store, events);\n renderer = mounted;\n store.setRenderable(mounted !== null);\n return () => {\n mounted?.destroy();\n if (renderer === mounted) renderer = null;\n };\n },\n });\n return api;\n}\n\n"],"names":[],"mappings":";;;;AA+BA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACA;AACA;AAAgB;AAC8B;AACvC;AACqD;;AAC/B;AAA0B;AAAA;;AAC9B;AAAyB;AAAA;AAAM;AAEtD;AAMF;AACE;AAAwC;AAKJ;AAGxC;AAEA;AACE;AACA;;AAGK;AAAqF;AACpE;AACD;AACN;AACI;AACE;AACE;AACJ;AAAA;AAGf;AACA;AACkB;AACpB;AACmC;AAEjC;AACgB;AAClB;AACyE;AACjC;AACH;AACY;AAC3B;AAExB;AAAmB;AACjB;AACgB;AAEd;AACA;AAGE;AACqC;AACvC;AACF;AAGJ;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/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"}
1
+ {"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/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;CAsBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,IAAI,CA0BxF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,IAAiB,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAQ5C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-looks.js","sources":["../../src/render/graph-looks.ts"],"sourcesContent":["// Three registers the same graph can be *drawn* in. Not three palettes, and — since a channel is a\n// binding on the request — not three encodings either.\n//\n// **A look is form and nothing else.** No colours: colour belongs to the theme's categorical\n// scheme, the way it belongs to a chart, so every surface showing the same categories gets the same\n// answer. And no encoding: what a channel carries is bound by whoever draws the graph, under Plot's\n// names. A look that decided whether identity reached the GPU as colour or as shape was a theme\n// reaching into an encoding — Vega-Lite states the general rule, that `config` sets defaults for\n// marks, scales, axes and legends and may not touch `encoding` — and its concrete cost was a `fill`\n// binding that painted nothing under the look named Ink.\n//\n// This is the grammar-of-graphics split, and it is the reference model rather than a local idea:\n// in Observable Plot colour is a property of the **scale**, never of the mark, and the mark carries\n// the channels. A look is the mark's geometry.\n//\n// The three names survive as **recommended pairings** — a form plus the bindings that were bundled\n// with it — offered by the host that draws the graph. A host offering three arrangements it authored\n// is not the same act as a preference silently discarding the caller's binding.\n\n/**\n * The glyphs this canvas draws — **by name**, because a name is what the concept is.\n *\n * This was five exports of one idea: a `SHAPE` object mapping names to numbers, a `ShapeId` type\n * that was the union of those numbers, `SHAPE_ORDER`, `SHAPE_OTHER` and `SHAPE_PATH` keyed by them.\n * The numbers were never ours. They are cosmos.gl's `setPointShapes` enum indices, and the jump from\n * `3` (diamond) to `7` (cross) is what gives that away — there is no `4`, `5` or `6` here because\n * Pentagon, Hexagon and Star are members this canvas does not draw. Publishing them made the\n * renderer's internal numbering part of a contract, so a host held `3` where it meant *diamond*, and\n * an upstream enum that renumbered would have moved every legend on every page silently.\n *\n * A string union says the same thing, reads at the call site — `<ShapeGlyph shape=\"cross\" />` — and\n * makes the mapping this file's private business, which is what it always was.\n */\nexport type Shape = \"circle\" | \"square\" | \"triangle\" | \"diamond\" | \"cross\";\n\n/**\n * The name → cosmos.gl enum index, and the one place that translation happens.\n *\n * `None` (`8`) has no name here on purpose: the point fragment shader `discard`s a `NONE` point that\n * carries no image, so \"past capacity\" spelled as `None` would delete the node from the picture, and\n * a category nobody can name is still a node with edges. `obligations.ts` grades that.\n *\n * Module-scoped and off the barrel. `buffers` reads it on the way to the GPU; nothing else needs it,\n * and anything that did would be reaching for the enum this type exists to hide.\n */\nexport const SHAPE_INDEX: Record<Shape, number> = {\n circle: 0,\n square: 1,\n triangle: 2,\n diamond: 3,\n cross: 7,\n};\n\n/**\n * The shape scale, in slot order — the sibling of the colour scale.\n *\n * Plot calls this channel `symbol` and gives it its own legend, which is the tell that it is a peer\n * of colour rather than a decoration. Four slots and no cycling: a fifth category cannot wear circle\n * again without claiming to be the first one.\n *\n * **Four because the shape channel's measured capacity is five, not because small shapes stop being\n * distinguishable.** Giovannangeli et al. (arXiv 2103.06084) put the ceiling at **5 for shape**\n * against **7 for colour**; `SHAPE_ORDER`'s four plus `SHAPE_OTHER` is exactly five distinguishable\n * glyphs, and the two scales differ in cardinality because the channels do. The colour side already\n * says the same thing from the other end — Dracula's document publishes `--chart-capacity: 7`.\n *\n * **This comment used to give the size argument, and the size argument is wrong.** It said a\n * pentagon, a hexagon and a circle are one dot at Ink's floor. Smart & Szafir (CHI 2019,\n * doi:10.1145/3290605.3300899) measured 16 shapes across 6 mark sizes from 6 to 50 px and found\n * shape discrimination *robust* to size: the only significant variation is at 6 px, and it is 4.5\n * accuracy points against 50 px. The conclusion survived the argument that was given for it, which\n * is the most dangerous shape a comment can have — see `OBLIGATIONS` in `./obligations.ts` for what\n * the floor actually protects.\n */\nexport const SHAPE_ORDER: Shape[] = [\"circle\", \"square\", \"triangle\", \"diamond\"];\n\n/**\n * What a category past the scale wears — the shape channel's `--muted-foreground`.\n *\n * No slot wears it, which is the whole point: it says *not one of the four* rather than repeating\n * the first one. This is the half that used to be missing, and the comment above was false without\n * it — `SHAPE_ORDER[4]` is `undefined`, and the fallback was `circle`.\n */\nexport const SHAPE_OTHER: Shape = \"cross\";\n\n/**\n * The geometry a canvas draws — and nothing else.\n *\n * **No `id`, no `label`, no `blurb`.** Those three were a picker's metadata, and the picker was a\n * list of three names that turned out to be two pictures. What a person chooses is declared in\n * `section.ts` as axes and resolved by `lookFrom`; what a renderer consumes is this.\n */\nexport interface Look {\n /** Radius at the lowest degree in the corpus, and at the highest. */\n size: [number, number];\n link: {\n /**\n * Whether the edge layer is drawn at all.\n *\n * It was `Display.links`, a second vocabulary for the picture that sat beside this one with its\n * own default and its own switch. A form that draws no links is a form.\n */\n render: boolean;\n opacity: number;\n width: number;\n /**\n * How far a link bows off the straight line, as a fraction of its length. `0` is straight.\n *\n * Keep it small. Every link curves the same way, so at cosmos.gl's default of `0.5` a few\n * hundred of them read as one pinwheel and the picture looks like it is spinning — motion\n * where there is structure. A hint is enough to tell two parallel edges apart.\n */\n curve: number;\n /**\n * Whether links **add** where they overlap, instead of compositing over one another.\n *\n * cosmos.gl's default is on, which is a choice nobody here made and which the archive made\n * visible: 4,280 grey links at 0.45 summed to a white spray that swallowed 1,543 points of\n * 2–9 px. Every point was uploaded, none was legible, and the picture read as *the nodes are\n * not rendering*.\n *\n * On, it is a real register rather than a bug — additive light is what makes a dense graph read\n * as flow — so it belongs to the form that wants it and not to the renderer's defaults.\n */\n blend: boolean;\n /**\n * Screen lengths between which a link fades out — depth, for free.\n *\n * Keep the far end generous. cosmos.gl measures this in *screen* pixels, so a range that\n * looks reasonable while zoomed in erases the whole edge layer when you zoom out, which\n * reads as \"Show links stopped working\".\n */\n fade: [number, number];\n };\n /** How many of the highest-degree nodes carry a standing label. */\n labels: number;\n /** A darkened rim. Mood rather than a reading aid, which is why it is form and not display. */\n vignette: boolean;\n /**\n * The dot grid behind the graph, which pans and subdivides with the camera.\n *\n * Beside `vignette` because it is the same kind of thing — the backdrop a form sits on — and it\n * arrived from the same place the rim did not: a `Display` interface that spelled the backdrop,\n * the edge layer and two multipliers as a second set of appearance controls.\n */\n grid: boolean;\n // **There is no `pointScale` and no `linkOpacity`**, and they are the two fields `Display` had that\n // this does not. Each was a reader's multiplier over a number this builder already computes from\n // the axis that owns it — `size` from `marks`, `link.opacity` from `marks` — so the panel offered\n // two ways to say one thing and the second could always overrule the measurement. The measured\n // pair (0.28 legible, 0.42 dense) is the whole argument of `marks`; a slider on top of it is the\n // post-process `graph-model.ts` forbids one layer down, wearing a preference's clothes.\n //\n // What is lost with them is the fit-to-corpus case, and it was never a preference: `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;"}
1
+ {"version":3,"file":"graph-looks.js","sources":["../../src/render/graph-looks.ts"],"sourcesContent":["// Three registers the same graph can be *drawn* in. Not three palettes, and — since a channel is a\n// binding on the request — not three encodings either.\n//\n// **A look is form and nothing else.** No colours: colour belongs to the theme's categorical\n// scheme, the way it belongs to a chart, so every surface showing the same categories gets the same\n// answer. And no encoding: what a channel carries is bound by whoever draws the graph, under Plot's\n// names. A look that decided whether identity reached the GPU as colour or as shape was a theme\n// reaching into an encoding — Vega-Lite states the general rule, that `config` sets defaults for\n// marks, scales, axes and legends and may not touch `encoding` — and its concrete cost was a `fill`\n// binding that painted nothing under the look named Ink.\n//\n// This is the grammar-of-graphics split, and it is the reference model rather than a local idea:\n// in Observable Plot colour is a property of the **scale**, never of the mark, and the mark carries\n// the channels. A look is the mark's geometry.\n//\n// The three names survive as **recommended pairings** — a form plus the bindings that were bundled\n// with it — offered by the host that draws the graph. A host offering three arrangements it authored\n// is not the same act as a preference silently discarding the caller's binding.\n\n/**\n * The glyphs this canvas draws — **by name**, because a name is what the concept is.\n *\n * This was five exports of one idea: a `SHAPE` object mapping names to numbers, a `ShapeId` type\n * that was the union of those numbers, `SHAPE_ORDER`, `SHAPE_OTHER` and `SHAPE_PATH` keyed by them.\n * The numbers were never ours. They are cosmos.gl's `setPointShapes` enum indices, and the jump from\n * `3` (diamond) to `7` (cross) is what gives that away — there is no `4`, `5` or `6` here because\n * Pentagon, Hexagon and Star are members this canvas does not draw. Publishing them made the\n * renderer's internal numbering part of a contract, so a host held `3` where it meant *diamond*, and\n * an upstream enum that renumbered would have moved every legend on every page silently.\n *\n * A string union says the same thing, reads at the call site — `<ShapeGlyph shape=\"cross\" />` — and\n * makes the mapping this file's private business, which is what it always was.\n */\nexport type Shape = \"circle\" | \"square\" | \"triangle\" | \"diamond\" | \"cross\";\n\n/**\n * The name → cosmos.gl enum index, and the one place that translation happens.\n *\n * `None` (`8`) has no name here on purpose: the point fragment shader `discard`s a `NONE` point that\n * carries no image, so \"past capacity\" spelled as `None` would delete the node from the picture, and\n * a category nobody can name is still a node with edges. `obligations.ts` grades that.\n *\n * Module-scoped and off the barrel. `buffers` reads it on the way to the GPU; nothing else needs it,\n * and anything that did would be reaching for the enum this type exists to hide.\n */\nexport const SHAPE_INDEX: Record<Shape, number> = {\n circle: 0,\n square: 1,\n triangle: 2,\n diamond: 3,\n cross: 7,\n};\n\n/**\n * The shape scale, in slot order — the sibling of the colour scale.\n *\n * Plot calls this channel `symbol` and gives it its own legend, which is the tell that it is a peer\n * of colour rather than a decoration. Four slots and no cycling: a fifth category cannot wear circle\n * again without claiming to be the first one.\n *\n * **Four because the shape channel's measured capacity is five, not because small shapes stop being\n * distinguishable.** Giovannangeli et al. (arXiv 2103.06084) put the ceiling at **5 for shape**\n * against **7 for colour**; `SHAPE_ORDER`'s four plus `SHAPE_OTHER` is exactly five distinguishable\n * glyphs, and the two scales differ in cardinality because the channels do. The colour side already\n * says the same thing from the other end — Dracula's document publishes `--chart-capacity: 7`.\n *\n * **This comment used to give the size argument, and the size argument is wrong.** It said a\n * pentagon, a hexagon and a circle are one dot at Ink's floor. Smart & Szafir (CHI 2019,\n * doi:10.1145/3290605.3300899) measured 16 shapes across 6 mark sizes from 6 to 50 px and found\n * shape discrimination *robust* to size: the only significant variation is at 6 px, and it is 4.5\n * accuracy points against 50 px. The conclusion survived the argument that was given for it, which\n * is the most dangerous shape a comment can have — see `OBLIGATIONS` in `./obligations.ts` for what\n * the floor actually protects.\n */\nexport const SHAPE_ORDER: Shape[] = [\"circle\", \"square\", \"triangle\", \"diamond\"];\n\n/**\n * What a category past the scale wears — the shape channel's `--muted-foreground`.\n *\n * No slot wears it, which is the whole point: it says *not one of the four* rather than repeating\n * the first one. This is the half that used to be missing, and the comment above was false without\n * it — `SHAPE_ORDER[4]` is `undefined`, and the fallback was `circle`.\n */\nexport const SHAPE_OTHER: Shape = \"cross\";\n\n/**\n * The geometry a canvas draws — and nothing else.\n *\n * **No `id`, no `label`, no `blurb`.** Those three were a picker's metadata, and the picker was a\n * list of three names that turned out to be two pictures. What a person chooses is declared in\n * `section.ts` as axes and resolved by `lookFrom`; what a renderer consumes is this.\n */\nexport interface Look {\n /** Radius at the lowest degree in the corpus, and at the highest. */\n size: [number, number];\n link: {\n /**\n * Whether the edge layer is drawn at all.\n *\n * It was `Display.links`, a second vocabulary for the picture that sat beside this one with its\n * own default and its own switch. A form that draws no links is a form.\n */\n render: boolean;\n opacity: number;\n width: number;\n /**\n * How far a link bows off the straight line, as a fraction of its length. `0` is straight.\n *\n * Keep it small. Every link curves the same way, so at cosmos.gl's default of `0.5` a few\n * hundred of them read as one pinwheel and the picture looks like it is spinning — motion\n * where there is structure. A hint is enough to tell two parallel edges apart.\n */\n curve: number;\n /**\n * Whether links **add** where they overlap, instead of compositing over one another.\n *\n * cosmos.gl's default is on, which is a choice nobody here made and which the archive made\n * visible: 4,280 grey links at 0.45 summed to a white spray that swallowed 1,543 points of\n * 2–9 px. Every point was uploaded, none was legible, and the picture read as *the nodes are\n * not rendering*.\n *\n * On, it is a real register rather than a bug — additive light is what makes a dense graph read\n * as flow — so it belongs to the form that wants it and not to the renderer's defaults.\n */\n blend: boolean;\n /**\n * Screen lengths between which a link fades out — depth, for free.\n *\n * Keep the far end generous. cosmos.gl measures this in *screen* pixels, so a range that\n * looks reasonable while zoomed in erases the whole edge layer when you zoom out, which\n * reads as \"Show links stopped working\".\n */\n fade: [number, number];\n };\n /** How many of the highest-degree nodes carry a standing label. */\n labels: number;\n /** A darkened rim. Mood rather than a reading aid, which is why it is form and not display. */\n vignette: boolean;\n /**\n * The dot grid behind the graph, which pans and subdivides with the camera.\n *\n * Beside `vignette` because it is the same kind of thing — the backdrop a form sits on — and it\n * arrived from the same place the rim did not: a `Display` interface that spelled the backdrop,\n * the edge layer and two multipliers as a second set of appearance controls.\n */\n grid: boolean;\n // **There is no `pointScale` and no `linkOpacity`**, and they are the two fields `Display` had that\n // this does not. Each was a reader's multiplier over a number this builder already computes from\n // the axis that owns it — `size` from `marks`, `link.opacity` from `marks` — so the panel offered\n // two ways to say one thing and the second could always overrule the measurement. The measured\n // pair (0.28 legible, 0.42 dense) is the whole argument of `marks`; a slider on top of it is the\n // post-process `graph-model.ts` forbids one layer down, wearing a preference's clothes.\n //\n // What is lost with them is the fit-to-corpus case, and it was never a preference: a computed fit\n // belongs to the tenant's starting point — which is a policy, and which is now expressible.\n\n // There is no `filter`, and its absence is a rule rather than an omission. Nebula carried\n // `saturate(1.1)` on the canvas element — the one thing left in a Look that touched hue, and a\n // chroma multiplier is colour wearing geometry's clothes. Measured over Kanzo's eight slots\n // (2026-08-13, `saturate(1.1)` through the same filter engine the browser applies): it moved\n // every slot, by ΔE 0.85 to **8.05**, which is the size of the separation `deriveScheme`\n // *guarantees* between two different categories. It did not break that separation here —\n // the closest pair went from ΔE 32.36 to 30.38, with room to spare — so the reason it is gone\n // is not a failure it caused, it is that a look must not be able to cause one. The whole claim\n // of the colour layer is that what ships is what was derived and measured; a post-process on\n // the canvas voids it silently, and `graph-model.ts` already forbids the same move one layer\n // down (\"nudging one on the way to the GPU voids all three\").\n}\n\n/**\n * The form a canvas draws, from the axes a person chose.\n *\n * **One function and no table of constants**, which is the shape the axes forced and the reason\n * the three named looks are gone. They were three parallel tables of ten fields; six of those\n * fields separated two of the three by 7–17%, under this file's own threshold for a difference\n * meaning anything — a luminance JND of 6.48–11.30 ΔL*. What is left of them is this: every number\n * appears once, where the axis that owns it is read, and the axis is declared next door in\n * `section.ts` for a panel to draw.\n *\n * **Link opacity and width ride the mark**, and that is what the numbers said rather than a tidy\n * guess: 0.42 and 0.45 on the two dense forms against 0.28 on the legible one. A form that spends\n * more ink on points cannot also spend it on edges.\n *\n * **The legible mark's radius is the whole reason that mark exists.** Its floor protects the other\n * two channels from shape rather than shape from smallness, which is why it is the one to pair\n * `symbol` with: spending shape on identity *and* size on degree at once is what Giovannangeli et\n * al. (arXiv 2103.06084) measure as dropping performance drastically under even minor heterogeneity,\n * and smaller marks worsen it both ways — the luminance JND above, and a square reported larger than\n * any other shape at equal area in 82% of trials (Smart & Szafir, CHI 2019). Not \"a triangle and a\n * square are the same dot below four pixels\", which is what this said for months and is false; see\n * `SHAPE_ORDER` for the measurement that refutes it.\n *\n * **The values are strings because a contributed preference is a string**, in all three kinds, so an\n * unrecognised namespace rides through a write untouched. Parsing what a kind means is the reader's\n * job and it is two lines; `@kanzo-tech/theme` exports the same two, and importing them would add a\n * dependency to this package to carry no code — the same call `GRAPH_SECTION` already makes about the\n * manifest type.\n *\n * A key that is missing, or that carries a value the section never offered, takes the manifest's\n * default. Those defaults are what a graph drew before any of this existed.\n */\nexport function lookFrom(values: Readonly<Record<string, string | undefined>> = {}): Look {\n const on = (key: string, fallback: boolean) => {\n const value = values[key];\n return value === undefined ? fallback : value === \"true\";\n };\n const legible = values.marks === \"legible\";\n const labels = Number.parseFloat(values.labels ?? \"\");\n return {\n size: legible ? [4, 13] : [2, 8],\n link: {\n render: on(\"links\", true),\n opacity: legible ? 0.28 : 0.42,\n width: legible ? 0.5 : 0.6,\n // A hint, and `obligations.ts` says why: every link bows the same way, so cosmos.gl's default\n // of 0.5 reads as one pinwheel. A toggle rather than a range because a reader wants two\n // pictures — straight, and told apart — not a number to tune.\n curve: on(\"bowed-links\", true) ? 0.12 : 0,\n blend: on(\"additive-links\", false),\n // Shared by every form. It was three ranges within ±10% of each other, which is the\n // definition of a field nobody chose.\n fade: [200, 1400],\n },\n labels: Number.isFinite(labels) ? labels : 26,\n vignette: on(\"vignette\", false),\n grid: on(\"grid\", true),\n };\n}\n\n/**\n * What a canvas draws when nobody has chosen anything.\n *\n * **Not on the barrel, and it used to be.** It is literally `lookFrom()` — a second public name for\n * a value the package already hands out on request — and the reason it was exported is the one\n * `resolveLook` below removes: a host that wanted one field different had to start from the whole\n * object, because `look` took a whole `Look`. Spreading a default you were given is a copy of it,\n * and a copy is what stops tracking the original the next time a number here moves.\n */\nexport const DEFAULT_LOOK: Look = lookFrom();\n\n/**\n * A look in the pieces a caller wants different — everything else is this package's answer.\n *\n * Two levels, because a `Look` has exactly two: the fields, and `link`. Deep-merging arbitrarily\n * would be a guess about a shape that is right here in this file, and `size` and `fade` are tuples\n * that must be replaced whole rather than merged element-wise.\n */\nexport interface LookPatch extends Partial<Omit<Look, \"link\">> {\n link?: Partial<Look[\"link\"]>;\n}\n\n/**\n * A patch over the package's own default — the merge `DEFAULT_LOOK` existed so a host could do by\n * hand.\n *\n * **Nothing, and it is the shared constant rather than a copy of it.** That identity matters: the\n * buffers are rebuilt whenever the look's reference changes, so a fresh object per render would\n * mean a full colour/size/shape upload on every render of every canvas that never asked for one.\n */\nexport function resolveLook(patch?: LookPatch): Look {\n if (!patch) return DEFAULT_LOOK;\n return { ...DEFAULT_LOOK, ...patch, link: { ...DEFAULT_LOOK.link, ...patch.link } };\n}\n\n/**\n * The SVG path for a shape glyph inside a 12×12 box — the legend draws what the canvas draws.\n *\n * **Off the barrel, and `ShapeGlyph` is what replaced it.** One host imported this, to fill a\n * `<path>` in a legend key and a hover card. What that host actually wanted was *the glyph*, and\n * handing it the path data made it responsible for the viewBox, the fill and the fact that the box\n * is twelve units — three facts it had to keep in step with this file by reading the comment above.\n * A component carries all three and cannot fall out of step with itself.\n */\nexport const SHAPE_PATH: Record<Shape, string> = {\n circle: \"M6 1.6a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z\",\n square: \"M2 2h8v8H2Z\",\n triangle: \"M6 1.6 10.6 10H1.4Z\",\n diamond: \"M6 1 11 6l-5 5-5-5Z\",\n // The proportions are cosmos.gl's own `crossDistance`: a plus with arms at 0.8 of the radius and\n // a bar 0.3 thick, so the legend's glyph is the shape the shader draws.\n cross: \"M4.2 1.2h3.6v3h3v3.6h-3v3H4.2v-3h-3V4.2h3Z\",\n};\n"],"names":["SHAPE_INDEX","SHAPE_ORDER","SHAPE_OTHER","lookFrom","values","on","key","fallback","value","legible","labels","DEFAULT_LOOK","resolveLook","patch","SHAPE_PATH"],"mappings":"AA6CO,MAAMA,IAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AACT,GAuBaC,IAAuB,CAAC,UAAU,UAAU,YAAY,SAAS,GASjEC,IAAqB;AAsH3B,SAASC,EAASC,IAAuD,IAAU;AACxF,QAAMC,IAAK,CAACC,GAAaC,MAAsB;AAC7C,UAAMC,IAAQJ,EAAOE,CAAG;AACxB,WAAOE,MAAU,SAAYD,IAAWC,MAAU;AAAA,EACpD,GACMC,IAAUL,EAAO,UAAU,WAC3BM,IAAS,OAAO,WAAWN,EAAO,UAAU,EAAE;AACpD,SAAO;AAAA,IACL,MAAMK,IAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AAAA,IAC/B,MAAM;AAAA,MACJ,QAAQJ,EAAG,SAAS,EAAI;AAAA,MACxB,SAASI,IAAU,OAAO;AAAA,MAC1B,OAAOA,IAAU,MAAM;AAAA;AAAA;AAAA;AAAA,MAIvB,OAAOJ,EAAG,eAAe,EAAI,IAAI,OAAO;AAAA,MACxC,OAAOA,EAAG,kBAAkB,EAAK;AAAA;AAAA;AAAA,MAGjC,MAAM,CAAC,KAAK,IAAI;AAAA,IAAA;AAAA,IAElB,QAAQ,OAAO,SAASK,CAAM,IAAIA,IAAS;AAAA,IAC3C,UAAUL,EAAG,YAAY,EAAK;AAAA,IAC9B,MAAMA,EAAG,QAAQ,EAAI;AAAA,EAAA;AAEzB;AAWO,MAAMM,IAAqBR,EAAA;AAqB3B,SAASS,EAAYC,GAAyB;AACnD,SAAKA,IACE,EAAE,GAAGF,GAAc,GAAGE,GAAO,MAAM,EAAE,GAAGF,EAAa,MAAM,GAAGE,EAAM,OAAK,IAD7DF;AAErB;AAWO,MAAMG,IAAoC;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO;AACT;"}
@@ -1,6 +1,6 @@
1
1
  import { GraphConfig } from '@cosmos.gl/graph';
2
2
  import { Channels } from '../core/channels';
3
- import { Composition } from './compose';
3
+ import { Encoding, Geometry } from '../core/load';
4
4
  import { Look, Shape } from './graph-looks';
5
5
  import { Sim } from './graph-sim';
6
6
  /**
@@ -22,18 +22,16 @@ export interface Paint {
22
22
  sizes: Float32Array;
23
23
  shapes: Float32Array;
24
24
  linkColors: Float32Array;
25
- linkWidths: Float32Array;
26
25
  }
27
26
  /**
28
- * Every per-point and per-link attribute, from one composition and the live theme — what a look or a
29
- * theme change re-uploads, and all it re-uploads.
27
+ * Every per-point and per-link attribute, from the loaded graph and the live theme — what a binding,
28
+ * a look or a theme change re-uploads, and all it re-uploads.
30
29
  *
31
30
  * `host` is the element the tokens are read against, so `var(--primary)` resolves for the tree the
32
- * canvas sits in. The ramp is `√value` over **this composition** — the biggest node here is what a
33
- * reader is looking at. A far end past `marks` is drawn at radius zero and alpha zero: it is in the
34
- * buffers only so an edge has somewhere to end.
31
+ * canvas sits in. The ramp is `√value` over the whole graph, and a vertex with no value takes the
32
+ * smallest radius.
35
33
  */
36
- export declare function paint(composition: Composition, look: Look, host: Element, channels?: Channels): Paint;
34
+ export declare function paint(geometry: Geometry, encoding: Encoding, look: Look, host: Element, channels?: Channels): Paint;
37
35
  /** The simulation coefficients, in cosmos.gl's spelling. Shared by construction and every change. */
38
36
  export declare function forces(sim: Sim): GraphConfig;
39
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"graph-model.d.ts","sourceRoot":"","sources":["../../src/render/graph-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAyC,KAAK,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,SAAc;qBAI7C,MAAM,KAAG,MAAM;qBAIf,MAAM,KAAG,KAAK;EAElC;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,YAAY,CAAC;IACzB,UAAU,EAAE,YAAY,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,GAAE,QAAa,GAAG,KAAK,CAkDzG;AAED,qGAAqG;AACrG,wBAAgB,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,WAAW,CAS5C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,WAAW,CAkBjE"}
1
+ {"version":3,"file":"graph-model.d.ts","sourceRoot":"","sources":["../../src/render/graph-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAyC,KAAK,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,SAAc;qBAI7C,MAAM,KAAG,MAAM;qBAIf,MAAM,KAAG,KAAK;EAElC;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,YAAY,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,GAAE,QAAa,GAAG,KAAK,CAgDvH;AAED,qGAAqG;AACrG,wBAAgB,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,WAAW,CAS5C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,WAAW,CAkBjE"}
@@ -1,80 +1,74 @@
1
- import { CHART_SLOTS as P, categoricalColor as R, categoricalCapacity as F } from "@kanzo-tech/ui";
2
- import { isColour as S } from "../core/channels.js";
3
- import { toHex as y, resolveToken as c } from "./css-color.js";
4
- import { SHAPE_ORDER as D, SHAPE_OTHER as E, SHAPE_INDEX as H } from "./graph-looks.js";
5
- function M(r, i = P) {
6
- const l = S(r.fill) ? r.fill : void 0, s = r.symbol !== void 0;
1
+ import { CHART_SLOTS as R, categoricalColor as A, categoricalCapacity as S } from "@kanzo-tech/ui";
2
+ import { isColour as D } from "../core/channels.js";
3
+ import { toHex as v, resolveToken as k } from "./css-color.js";
4
+ import { SHAPE_ORDER as E, SHAPE_OTHER as F, SHAPE_INDEX as H } from "./graph-looks.js";
5
+ function T(n, t = R) {
6
+ const o = D(n.fill) ? n.fill : void 0, c = n.symbol !== void 0;
7
7
  return {
8
- color: (e) => l || (e >= i ? "var(--muted-foreground)" : R(e, void 0, i)),
9
- shape: (e) => s ? D[e] ?? E : "circle"
8
+ color: (e) => o || (e >= t ? "var(--muted-foreground)" : A(e, void 0, t)),
9
+ shape: (e) => c ? E[e] ?? F : "circle"
10
10
  };
11
11
  }
12
- function N(r, i, l, s = {}) {
13
- var b;
14
- const e = M(s, F(l)), v = /* @__PURE__ */ new Map(), A = (t) => {
15
- let n = v.get(t);
16
- return n || v.set(t, n = c(l, e.color(t))), n;
17
- }, d = r.positions.length / 2, o = new Float32Array(d * 4), m = new Float32Array(d), h = new Float32Array(d), u = r.sizes;
18
- let g = 0, w = 1;
19
- if (u && r.marks > 0) {
20
- let t = Number.POSITIVE_INFINITY, n = 0;
21
- for (let a = 0; a < r.marks; a++) {
22
- const k = u[a];
23
- k < t && (t = k), k > n && (n = k);
12
+ function M(n, t, o, c, e = {}) {
13
+ const y = T(e, S(c)), g = /* @__PURE__ */ new Map(), P = (i) => {
14
+ let r = g.get(i);
15
+ return r || g.set(i, r = k(c, y.color(i))), r;
16
+ }, u = n.size, f = new Float32Array(u * 4), b = new Float32Array(u), N = new Float32Array(u), d = t.sizes;
17
+ let m = 0, C = 1;
18
+ if (d) {
19
+ let i = Number.POSITIVE_INFINITY, r = 0;
20
+ for (let l = 0; l < u; l++) {
21
+ const a = d[l];
22
+ a < i && (i = a), a > r && (r = a);
24
23
  }
25
- g = Math.sqrt(Number.isFinite(t) ? t : 0), w = Math.sqrt(n) - g || 1;
24
+ m = Math.sqrt(Number.isFinite(i) ? i : 0), C = Math.sqrt(r) - m || 1;
26
25
  }
27
- for (let t = 0; t < r.marks; t++) {
28
- const n = r.categories[t] ?? 0;
29
- o.set(A(n), t * 4);
30
- const a = u ? (Math.sqrt(u[t]) - g) / w : 0;
31
- m[t] = i.size[0] + a * (i.size[1] - i.size[0]), h[t] = H[e.shape(n)];
26
+ for (let i = 0; i < u; i++) {
27
+ const r = t.ranks[i] ?? 0;
28
+ f.set(P(r), i * 4);
29
+ const l = d ? d[i] : Number.NaN, a = Number.isNaN(l) ? 0 : (Math.sqrt(l) - m) / C;
30
+ b[i] = o.size[0] + a * (o.size[1] - o.size[0]), N[i] = H[y.shape(r)];
32
31
  }
33
- const p = r.links.length / 2, C = new Float32Array(p * 4), O = new Float32Array(p), f = s.stroke ? c(l, s.stroke) : null;
34
- for (let t = 0; t < p; t++) {
35
- const n = r.links[t * 2] ?? 0;
36
- C.set(
37
- f ? [f[0], f[1], f[2], 1] : [o[n * 4] ?? 0.7, o[n * 4 + 1] ?? 0.7, o[n * 4 + 2] ?? 0.7, 1],
38
- t * 4
39
- );
40
- const a = ((b = r.weights) == null ? void 0 : b[t]) ?? 1;
41
- O[t] = i.link.width * (1 + Math.log2(Math.max(1, a)) / 2);
32
+ const O = n.links.length / 2, p = new Float32Array(O * 4), s = e.stroke ? k(c, e.stroke) : null;
33
+ for (let i = 0; i < O; i++) {
34
+ const r = n.links[i * 2] ?? 0;
35
+ p[i * 4] = s ? s[0] : f[r * 4] ?? 0.7, p[i * 4 + 1] = s ? s[1] : f[r * 4 + 1] ?? 0.7, p[i * 4 + 2] = s ? s[2] : f[r * 4 + 2] ?? 0.7, p[i * 4 + 3] = 1;
42
36
  }
43
- return { colors: o, sizes: m, shapes: h, linkColors: C, linkWidths: O };
37
+ return { colors: f, sizes: b, shapes: N, linkColors: p };
44
38
  }
45
- function _(r) {
39
+ function _(n) {
46
40
  return {
47
- simulationGravity: r.gravity,
48
- simulationRepulsion: r.repulsion,
49
- simulationLinkSpring: r.linkSpring,
50
- simulationLinkDistance: r.linkDistance,
51
- simulationFriction: r.friction,
52
- simulationCluster: r.cluster
41
+ simulationGravity: n.gravity,
42
+ simulationRepulsion: n.repulsion,
43
+ simulationLinkSpring: n.linkSpring,
44
+ simulationLinkDistance: n.linkDistance,
45
+ simulationFriction: n.friction,
46
+ simulationCluster: n.cluster
53
47
  };
54
48
  }
55
- function x(r, i) {
49
+ function h(n, t) {
56
50
  return {
57
- backgroundColor: y(c(i, "var(--background)")),
51
+ backgroundColor: v(k(t, "var(--background)")),
58
52
  scalePointsOnZoom: !1,
59
- renderLinks: r.link.render,
60
- linkOpacity: r.link.opacity,
61
- linkDefaultWidth: r.link.width,
62
- linkBlending: r.link.blend,
63
- curvedLinks: r.link.curve > 0,
64
- curvedLinkControlPointDistance: r.link.curve,
65
- linkVisibilityDistanceRange: r.link.fade,
53
+ renderLinks: n.link.render,
54
+ linkOpacity: n.link.opacity,
55
+ linkDefaultWidth: n.link.width,
56
+ linkBlending: n.link.blend,
57
+ curvedLinks: n.link.curve > 0,
58
+ curvedLinkControlPointDistance: n.link.curve,
59
+ linkVisibilityDistanceRange: n.link.fade,
66
60
  linkVisibilityMinTransparency: 0.12,
67
61
  renderHoveredPointRing: !0,
68
- hoveredPointRingColor: y(c(i, "var(--primary)")),
69
- focusedPointRingColor: y(c(i, "var(--primary)")),
62
+ hoveredPointRingColor: v(k(t, "var(--primary)")),
63
+ focusedPointRingColor: v(k(t, "var(--primary)")),
70
64
  pointGreyoutOpacity: 0.1,
71
65
  linkGreyoutOpacity: 0.025
72
66
  };
73
67
  }
74
68
  export {
75
- x as appearance,
69
+ h as appearance,
76
70
  _ as forces,
77
- N as paint,
78
- M as scaleOf
71
+ M as paint,
72
+ T as scaleOf
79
73
  };
80
74
  //# sourceMappingURL=graph-model.js.map