@rokkit/graph 1.6.0 → 1.8.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 (82) hide show
  1. package/README.md +66 -2
  2. package/dist/Graph.svelte +219 -169
  3. package/dist/GraphLegend.svelte +112 -0
  4. package/dist/GraphLegend.svelte.d.ts +33 -0
  5. package/dist/GraphState.svelte.d.ts +115 -15
  6. package/dist/GraphState.svelte.js +213 -5
  7. package/dist/controls/BundleControl.svelte +30 -0
  8. package/dist/controls/BundleControl.svelte.d.ts +16 -0
  9. package/dist/controls/DensityControl.svelte +39 -0
  10. package/dist/controls/DensityControl.svelte.d.ts +16 -0
  11. package/dist/controls/DepthControl.svelte +34 -0
  12. package/dist/controls/DepthControl.svelte.d.ts +17 -0
  13. package/dist/controls/EdgeStyleControl.svelte +31 -0
  14. package/dist/controls/EdgeStyleControl.svelte.d.ts +14 -0
  15. package/dist/controls/ZoomControl.svelte +47 -0
  16. package/dist/controls/ZoomControl.svelte.d.ts +7 -0
  17. package/dist/controls/zoom.d.ts +6 -0
  18. package/dist/controls/zoom.js +12 -0
  19. package/dist/diagrams/CallTree.svelte +135 -0
  20. package/dist/diagrams/CallTree.svelte.d.ts +37 -0
  21. package/dist/diagrams/DependencyDiagram.svelte +124 -0
  22. package/dist/diagrams/DependencyDiagram.svelte.d.ts +30 -0
  23. package/dist/diagrams/DependencyMatrix.svelte +177 -0
  24. package/dist/diagrams/DependencyMatrix.svelte.d.ts +34 -0
  25. package/dist/diagrams/DiagramFrame.svelte +36 -0
  26. package/dist/diagrams/DiagramFrame.svelte.d.ts +20 -0
  27. package/dist/diagrams/ErDiagram.svelte +128 -0
  28. package/dist/diagrams/ErDiagram.svelte.d.ts +33 -0
  29. package/dist/diagrams/Neighborhood.svelte +127 -0
  30. package/dist/diagrams/Neighborhood.svelte.d.ts +33 -0
  31. package/dist/diagrams/StructureDiagram.svelte +131 -0
  32. package/dist/diagrams/StructureDiagram.svelte.d.ts +31 -0
  33. package/dist/diagrams/Sunburst.svelte +124 -0
  34. package/dist/diagrams/Sunburst.svelte.d.ts +32 -0
  35. package/dist/diagrams/Treemap.svelte +124 -0
  36. package/dist/diagrams/Treemap.svelte.d.ts +32 -0
  37. package/dist/icons.d.ts +5 -0
  38. package/dist/icons.js +5 -0
  39. package/dist/index.d.ts +21 -1
  40. package/dist/index.js +25 -1
  41. package/dist/layout/arc.d.ts +13 -0
  42. package/dist/layout/arc.js +58 -0
  43. package/dist/layout/bundle.d.ts +30 -0
  44. package/dist/layout/bundle.js +80 -0
  45. package/dist/layout/cluster.js +2 -0
  46. package/dist/layout/edges.js +11 -0
  47. package/dist/layout/flow.d.ts +8 -0
  48. package/dist/layout/flow.js +141 -0
  49. package/dist/layout/hierarchy.d.ts +42 -0
  50. package/dist/layout/hierarchy.js +175 -0
  51. package/dist/layout/index.d.ts +7 -1
  52. package/dist/layout/index.js +8 -2
  53. package/dist/layout/matrix.d.ts +38 -0
  54. package/dist/layout/matrix.js +90 -0
  55. package/dist/layout/neighborhood.js +164 -28
  56. package/dist/layout/options.d.ts +44 -0
  57. package/dist/layout/options.js +106 -0
  58. package/dist/layout/order.d.ts +11 -0
  59. package/dist/layout/order.js +153 -0
  60. package/dist/layout/points.js +27 -35
  61. package/dist/layout/radial.d.ts +16 -0
  62. package/dist/layout/radial.js +214 -0
  63. package/dist/layout/rank.d.ts +15 -0
  64. package/dist/layout/rank.js +105 -0
  65. package/dist/layout/rings.d.ts +20 -0
  66. package/dist/layout/rings.js +92 -0
  67. package/dist/layout/sizing.d.ts +37 -0
  68. package/dist/layout/sizing.js +79 -0
  69. package/dist/layout/squarify.d.ts +14 -0
  70. package/dist/layout/squarify.js +114 -0
  71. package/dist/layout/structure.d.ts +9 -0
  72. package/dist/layout/structure.js +367 -0
  73. package/dist/layout/sunburst.d.ts +8 -0
  74. package/dist/layout/sunburst.js +126 -0
  75. package/dist/layout/types.d.ts +145 -0
  76. package/dist/layout/world.d.ts +9 -0
  77. package/dist/layout/world.js +144 -0
  78. package/dist/model/normalize.js +66 -4
  79. package/dist/model/tree.d.ts +27 -0
  80. package/dist/model/tree.js +182 -0
  81. package/dist/types.d.ts +75 -3
  82. package/package.json +9 -4
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The shell every named diagram shares: a canvas, controls over it, a legend under it.
4
+ *
5
+ * Internal on purpose. A consumer composes by picking a diagram — `ErDiagram`, `CallTree`
6
+ * — not by assembling a frame, exactly as they pick `BarChart` rather than assembling a
7
+ * `Plot` with a geom. This exists so the seven diagrams do not each re-declare the same
8
+ * positioned box.
9
+ */
10
+ import type { Snippet } from 'svelte'
11
+
12
+ type Props = {
13
+ canvas: Snippet
14
+ /** Controls drawn OVER the canvas, anchored to its corners. */
15
+ overlay?: Snippet
16
+ /** A legend or caption under the canvas, in the layout flow rather than over it. */
17
+ footer?: Snippet
18
+ class?: string
19
+ }
20
+
21
+ let { canvas, overlay, footer, class: className = '' }: Props = $props()
22
+ </script>
23
+
24
+ <div data-graph-diagram class={className}>
25
+ <!-- The canvas fills what is left after the footer, so a legend never overlaps the
26
+ picture and never gets clipped by it. -->
27
+ <div data-graph-diagram-canvas>
28
+ {@render canvas()}
29
+ {#if overlay}
30
+ <div data-graph-diagram-overlay>{@render overlay()}</div>
31
+ {/if}
32
+ </div>
33
+ {#if footer}
34
+ <div data-graph-diagram-footer>{@render footer()}</div>
35
+ {/if}
36
+ </div>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The shell every named diagram shares: a canvas, controls over it, a legend under it.
3
+ *
4
+ * Internal on purpose. A consumer composes by picking a diagram — `ErDiagram`, `CallTree`
5
+ * — not by assembling a frame, exactly as they pick `BarChart` rather than assembling a
6
+ * `Plot` with a geom. This exists so the seven diagrams do not each re-declare the same
7
+ * positioned box.
8
+ */
9
+ import type { Snippet } from 'svelte';
10
+ type Props = {
11
+ canvas: Snippet;
12
+ /** Controls drawn OVER the canvas, anchored to its corners. */
13
+ overlay?: Snippet;
14
+ /** A legend or caption under the canvas, in the layout flow rather than over it. */
15
+ footer?: Snippet;
16
+ class?: string;
17
+ };
18
+ declare const DiagramFrame: import("svelte").Component<Props, {}, "">;
19
+ type DiagramFrame = ReturnType<typeof DiagramFrame>;
20
+ export default DiagramFrame;
@@ -0,0 +1,128 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Table entities and their foreign keys, ranked so direction reads off the geometry.
4
+ *
5
+ * An ER diagram is table entities and their relationships — a view is a derived projection
6
+ * and a routine is behaviour, so neither belongs here. Pass `toGraphInput(model, 'er')`
7
+ * from `@rokkit/graph/schema` and the split is made for you.
8
+ *
9
+ * `flow` gives up group boxes to rank by reference direction, so `groupTint` is on by
10
+ * default: without it the schema a table belongs to becomes invisible, which is the one
11
+ * thing the older cluster arrangement did show.
12
+ */
13
+ import Graph from '../Graph.svelte'
14
+ import GraphLegend from '../GraphLegend.svelte'
15
+ import DiagramFrame from './DiagramFrame.svelte'
16
+ import DensityControl from '../controls/DensityControl.svelte'
17
+ import EdgeStyleControl from '../controls/EdgeStyleControl.svelte'
18
+ import ZoomControl from '../controls/ZoomControl.svelte'
19
+ import { GraphState } from '../GraphState.svelte.js'
20
+ import type { Density, EdgeStyle } from '../layout/types.js'
21
+ import type { GraphFields } from '../types.js'
22
+ import type { GraphPreset } from '../preset.js'
23
+
24
+ type Props = {
25
+ /**
26
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
27
+ * the caller owns it: this component reads it and never calls `update`, exactly as
28
+ * `Graph` does — two owners writing one state fight on every render.
29
+ */
30
+ state?: GraphState
31
+ nodes?: unknown[]
32
+ edges?: unknown[]
33
+ fields?: GraphFields
34
+ density?: Density
35
+ edgeStyle?: EdgeStyle
36
+ /** Paint each card with its schema colour. On by default — `flow` has no group boxes. */
37
+ groupTint?: boolean
38
+ /** Show the on-canvas density, edge-style and zoom controls. */
39
+ controls?: boolean
40
+ /** Show a key under the canvas. Keyed by GROUP, which is what the tint encodes. */
41
+ legend?: boolean
42
+ value?: string | null
43
+ preset?: GraphPreset
44
+ mode?: 'light' | 'dark'
45
+ arrows?: boolean
46
+ label?: string
47
+ onselect?: (id: string | null) => void
48
+ class?: string
49
+ }
50
+
51
+ let {
52
+ state: provided,
53
+ nodes = [],
54
+ edges = [],
55
+ fields = {},
56
+ density = $bindable('keys'),
57
+ edgeStyle = $bindable('curved'),
58
+ groupTint = true,
59
+ controls = false,
60
+ legend = false,
61
+ value = $bindable(undefined),
62
+ preset = undefined,
63
+ mode = 'light',
64
+ arrows = true,
65
+ label = undefined,
66
+ onselect = undefined,
67
+ class: className = ''
68
+ }: Props = $props()
69
+
70
+ let zoom = $state(1)
71
+
72
+ const config = () => ({
73
+ nodes,
74
+ edges,
75
+ fields,
76
+ layout: 'flow',
77
+ density,
78
+ edgeStyle,
79
+ groupTint,
80
+ value,
81
+ preset,
82
+ mode,
83
+ label,
84
+ onselect
85
+ })
86
+
87
+ // svelte-ignore state_referenced_locally
88
+ const graph = provided ?? new GraphState(config())
89
+
90
+ $effect(() => {
91
+ if (!provided) {
92
+ graph.update(config())
93
+
94
+ return
95
+ }
96
+
97
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
98
+ // and are never reset here. What this component DOES own is the options its own
99
+ // controls drive — the layout above all, since choosing this component is choosing it.
100
+ // `apply` merges; `update` would revert everything it does not name.
101
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
102
+ config()
103
+ graph.apply(mine)
104
+ })
105
+ </script>
106
+
107
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
108
+ inside is always defined, so the frame would render an empty control bar over
109
+ every diagram that asked for none. -->
110
+ {#snippet controlBar()}
111
+ <DensityControl {density} onchange={(v) => (density = v)} />
112
+ <EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
113
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
114
+ {/snippet}
115
+
116
+ {#snippet legendBar()}
117
+ <GraphLegend state={graph} groups />
118
+ {/snippet}
119
+
120
+ <DiagramFrame
121
+ class={className}
122
+ overlay={controls ? controlBar : undefined}
123
+ footer={legend ? legendBar : undefined}
124
+ >
125
+ {#snippet canvas()}
126
+ <Graph state={graph} bind:zoom {arrows} />
127
+ {/snippet}
128
+ </DiagramFrame>
@@ -0,0 +1,33 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { Density, EdgeStyle } from '../layout/types.js';
3
+ import type { GraphFields } from '../types.js';
4
+ import type { GraphPreset } from '../preset.js';
5
+ type Props = {
6
+ /**
7
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
8
+ * the caller owns it: this component reads it and never calls `update`, exactly as
9
+ * `Graph` does — two owners writing one state fight on every render.
10
+ */
11
+ state?: GraphState;
12
+ nodes?: unknown[];
13
+ edges?: unknown[];
14
+ fields?: GraphFields;
15
+ density?: Density;
16
+ edgeStyle?: EdgeStyle;
17
+ /** Paint each card with its schema colour. On by default — `flow` has no group boxes. */
18
+ groupTint?: boolean;
19
+ /** Show the on-canvas density, edge-style and zoom controls. */
20
+ controls?: boolean;
21
+ /** Show a key under the canvas. Keyed by GROUP, which is what the tint encodes. */
22
+ legend?: boolean;
23
+ value?: string | null;
24
+ preset?: GraphPreset;
25
+ mode?: 'light' | 'dark';
26
+ arrows?: boolean;
27
+ label?: string;
28
+ onselect?: (id: string | null) => void;
29
+ class?: string;
30
+ };
31
+ declare const ErDiagram: import("svelte").Component<Props, {}, "value" | "density" | "edgeStyle">;
32
+ type ErDiagram = ReturnType<typeof ErDiagram>;
33
+ export default ErDiagram;
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ /**
3
+ * One node and what reaches it, walked a hop at a time.
4
+ *
5
+ * The answer to "what touches this" — and at `depth: 2`, to "what does changing this
6
+ * reach", which is the question a reader has before editing something. Cards are built at
7
+ * full detail unconditionally: a portrait of one node's surroundings that hides the
8
+ * columns its edges land on defeats its own purpose, which is why there is no density
9
+ * control here.
10
+ */
11
+ import Graph from '../Graph.svelte'
12
+ import GraphLegend from '../GraphLegend.svelte'
13
+ import DiagramFrame from './DiagramFrame.svelte'
14
+ import DepthControl from '../controls/DepthControl.svelte'
15
+ import EdgeStyleControl from '../controls/EdgeStyleControl.svelte'
16
+ import ZoomControl from '../controls/ZoomControl.svelte'
17
+ import { GraphState } from '../GraphState.svelte.js'
18
+ import type { EdgeStyle } from '../layout/types.js'
19
+ import type { GraphFields } from '../types.js'
20
+ import type { GraphPreset } from '../preset.js'
21
+
22
+ type Props = {
23
+ /**
24
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
25
+ * the caller owns it: this component reads it and never calls `update`, exactly as
26
+ * `Graph` does — two owners writing one state fight on every render.
27
+ */
28
+ state?: GraphState
29
+ nodes?: unknown[]
30
+ edges?: unknown[]
31
+ fields?: GraphFields
32
+ /** The node to centre. Defaults to the selection. */
33
+ focus?: string | null
34
+ /** How many hops out. One answers "what touches this"; two, "what does this reach". */
35
+ depth?: number
36
+ edgeStyle?: EdgeStyle
37
+ controls?: boolean
38
+ maxDepth?: number
39
+ legend?: boolean
40
+ value?: string | null
41
+ preset?: GraphPreset
42
+ mode?: 'light' | 'dark'
43
+ arrows?: boolean
44
+ label?: string
45
+ onselect?: (id: string | null) => void
46
+ class?: string
47
+ }
48
+
49
+ let {
50
+ state: provided,
51
+ nodes = [],
52
+ edges = [],
53
+ fields = {},
54
+ focus = $bindable(null),
55
+ depth = $bindable(1),
56
+ edgeStyle = $bindable('curved'),
57
+ controls = false,
58
+ maxDepth = 3,
59
+ legend = false,
60
+ value = $bindable(undefined),
61
+ preset = undefined,
62
+ mode = 'light',
63
+ arrows = true,
64
+ label = undefined,
65
+ onselect = undefined,
66
+ class: className = ''
67
+ }: Props = $props()
68
+
69
+ let zoom = $state(1)
70
+
71
+ const config = () => ({
72
+ nodes,
73
+ edges,
74
+ fields,
75
+ layout: 'neighborhood',
76
+ focus,
77
+ depth,
78
+ edgeStyle,
79
+ value,
80
+ preset,
81
+ mode,
82
+ label,
83
+ onselect
84
+ })
85
+
86
+ // svelte-ignore state_referenced_locally
87
+ const graph = provided ?? new GraphState(config())
88
+
89
+ $effect(() => {
90
+ if (!provided) {
91
+ graph.update(config())
92
+
93
+ return
94
+ }
95
+
96
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
97
+ // and are never reset here. What this component DOES own is the options its own
98
+ // controls drive — the layout above all, since choosing this component is choosing it.
99
+ // `apply` merges; `update` would revert everything it does not name.
100
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
101
+ config()
102
+ graph.apply(mine)
103
+ })
104
+ </script>
105
+
106
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
107
+ inside is always defined, so the frame would render an empty control bar over
108
+ every diagram that asked for none. -->
109
+ {#snippet controlBar()}
110
+ <DepthControl levels={depth} max={maxDepth} onchange={(v) => (depth = v)} />
111
+ <EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
112
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
113
+ {/snippet}
114
+
115
+ {#snippet legendBar()}
116
+ <GraphLegend state={graph} kinds relations />
117
+ {/snippet}
118
+
119
+ <DiagramFrame
120
+ class={className}
121
+ overlay={controls ? controlBar : undefined}
122
+ footer={legend ? legendBar : undefined}
123
+ >
124
+ {#snippet canvas()}
125
+ <Graph state={graph} bind:zoom {arrows} />
126
+ {/snippet}
127
+ </DiagramFrame>
@@ -0,0 +1,33 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { EdgeStyle } from '../layout/types.js';
3
+ import type { GraphFields } from '../types.js';
4
+ import type { GraphPreset } from '../preset.js';
5
+ type Props = {
6
+ /**
7
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
8
+ * the caller owns it: this component reads it and never calls `update`, exactly as
9
+ * `Graph` does — two owners writing one state fight on every render.
10
+ */
11
+ state?: GraphState;
12
+ nodes?: unknown[];
13
+ edges?: unknown[];
14
+ fields?: GraphFields;
15
+ /** The node to centre. Defaults to the selection. */
16
+ focus?: string | null;
17
+ /** How many hops out. One answers "what touches this"; two, "what does this reach". */
18
+ depth?: number;
19
+ edgeStyle?: EdgeStyle;
20
+ controls?: boolean;
21
+ maxDepth?: number;
22
+ legend?: boolean;
23
+ value?: string | null;
24
+ preset?: GraphPreset;
25
+ mode?: 'light' | 'dark';
26
+ arrows?: boolean;
27
+ label?: string;
28
+ onselect?: (id: string | null) => void;
29
+ class?: string;
30
+ };
31
+ declare const Neighborhood: import("svelte").Component<Props, {}, "focus" | "value" | "edgeStyle" | "depth">;
32
+ type Neighborhood = ReturnType<typeof Neighborhood>;
33
+ export default Neighborhood;
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A codebase as a radial dendrogram, with its calls bundled through the structure.
4
+ *
5
+ * What `CallTree` cannot be over a real repo. A call graph's own spanning tree has hundreds
6
+ * of roots, so capping its depth prunes almost nothing and the rim comes out a grey smear.
7
+ * Containment — crate, module, file — is a real tree with one root, which is what makes
8
+ * "show two levels, then drill" mean something.
9
+ *
10
+ * The calls are drawn ON that tree rather than defining it, bundled so edges travelling the
11
+ * same way through the hierarchy pull together. `bundleTension` is the Straight/Bundled
12
+ * toggle: 0 is a plain chord, 0.85 the full route.
13
+ */
14
+ import Graph from '../Graph.svelte'
15
+ import GraphLegend from '../GraphLegend.svelte'
16
+ import DiagramFrame from './DiagramFrame.svelte'
17
+ import DepthControl from '../controls/DepthControl.svelte'
18
+ import BundleControl from '../controls/BundleControl.svelte'
19
+ import ZoomControl from '../controls/ZoomControl.svelte'
20
+ import { GraphState } from '../GraphState.svelte.js'
21
+ import type { GraphFields } from '../types.js'
22
+ import type { GraphPreset } from '../preset.js'
23
+
24
+ type Props = {
25
+ /** Share one state with views beside this one. See `ErDiagram` for the contract. */
26
+ state?: GraphState
27
+ nodes?: unknown[]
28
+ edges?: unknown[]
29
+ fields?: GraphFields
30
+ /** How many ancestor bands to draw, counting from the focus. */
31
+ levels?: number
32
+ /** The subtree to render as the whole canvas. `[]` is the root. Drilling, not zooming. */
33
+ focusPath?: string[]
34
+ /** How hard edges are pulled onto the tree, 0..1. 0 is a straight chord. */
35
+ bundleTension?: number
36
+ /** What a leaf's dot is sized by. */
37
+ sizeBy?: string
38
+ sizeScale?: 'linear' | 'log'
39
+ controls?: boolean
40
+ maxLevels?: number
41
+ legend?: boolean
42
+ value?: string | null
43
+ preset?: GraphPreset
44
+ mode?: 'light' | 'dark'
45
+ label?: string
46
+ onselect?: (id: string | null) => void
47
+ class?: string
48
+ }
49
+
50
+ let {
51
+ state: provided,
52
+ nodes = [],
53
+ edges = [],
54
+ fields = {},
55
+ levels = $bindable(2),
56
+ focusPath = $bindable([]),
57
+ bundleTension = $bindable(0.85),
58
+ sizeBy = 'degree',
59
+ sizeScale = 'linear',
60
+ controls = false,
61
+ maxLevels = 4,
62
+ legend = false,
63
+ value = $bindable(undefined),
64
+ preset = undefined,
65
+ mode = 'light',
66
+ label = undefined,
67
+ onselect = undefined,
68
+ class: className = ''
69
+ }: Props = $props()
70
+
71
+ let zoom = $state(1)
72
+
73
+ const config = () => ({
74
+ nodes,
75
+ edges,
76
+ fields,
77
+ layout: 'structure',
78
+ levels,
79
+ focusPath,
80
+ bundleTension,
81
+ sizeBy,
82
+ sizeScale,
83
+ value,
84
+ preset,
85
+ mode,
86
+ label,
87
+ onselect
88
+ })
89
+
90
+ // svelte-ignore state_referenced_locally
91
+ const graph = provided ?? new GraphState(config())
92
+
93
+ $effect(() => {
94
+ if (!provided) {
95
+ graph.update(config())
96
+
97
+ return
98
+ }
99
+
100
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
101
+ // and are never reset here. What this component DOES own is the options its own
102
+ // controls drive — the layout above all, since choosing this component is choosing it.
103
+ // `apply` merges; `update` would revert everything it does not name.
104
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
105
+ config()
106
+ graph.apply(mine)
107
+ })
108
+ </script>
109
+
110
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
111
+ inside is always defined, so the frame would render an empty control bar over
112
+ every diagram that asked for none. -->
113
+ {#snippet controlBar()}
114
+ <DepthControl {levels} max={maxLevels} onchange={(v) => (levels = v)} />
115
+ <BundleControl {bundleTension} onchange={(v) => (bundleTension = v)} />
116
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
117
+ {/snippet}
118
+
119
+ {#snippet legendBar()}
120
+ <GraphLegend state={graph} relations groups />
121
+ {/snippet}
122
+
123
+ <DiagramFrame
124
+ class={className}
125
+ overlay={controls ? controlBar : undefined}
126
+ footer={legend ? legendBar : undefined}
127
+ >
128
+ {#snippet canvas()}
129
+ <Graph state={graph} bind:zoom arrows={false} />
130
+ {/snippet}
131
+ </DiagramFrame>
@@ -0,0 +1,31 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { GraphFields } from '../types.js';
3
+ import type { GraphPreset } from '../preset.js';
4
+ type Props = {
5
+ /** Share one state with views beside this one. See `ErDiagram` for the contract. */
6
+ state?: GraphState;
7
+ nodes?: unknown[];
8
+ edges?: unknown[];
9
+ fields?: GraphFields;
10
+ /** How many ancestor bands to draw, counting from the focus. */
11
+ levels?: number;
12
+ /** The subtree to render as the whole canvas. `[]` is the root. Drilling, not zooming. */
13
+ focusPath?: string[];
14
+ /** How hard edges are pulled onto the tree, 0..1. 0 is a straight chord. */
15
+ bundleTension?: number;
16
+ /** What a leaf's dot is sized by. */
17
+ sizeBy?: string;
18
+ sizeScale?: 'linear' | 'log';
19
+ controls?: boolean;
20
+ maxLevels?: number;
21
+ legend?: boolean;
22
+ value?: string | null;
23
+ preset?: GraphPreset;
24
+ mode?: 'light' | 'dark';
25
+ label?: string;
26
+ onselect?: (id: string | null) => void;
27
+ class?: string;
28
+ };
29
+ declare const StructureDiagram: import("svelte").Component<Props, {}, "value" | "focusPath" | "levels" | "bundleTension">;
30
+ type StructureDiagram = ReturnType<typeof StructureDiagram>;
31
+ export default StructureDiagram;
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The same containment as a treemap, drawn as nested wedges.
4
+ *
5
+ * Angle carries the measure and RADIUS carries the depth, so how deep the tree goes is the
6
+ * first thing you read — which is exactly what a treemap makes you hunt for. A treemap
7
+ * spends its pixels better; neither is the right one, which is why both exist.
8
+ *
9
+ * Same tree, same measure, same captions as `Treemap`, so switching between them keeps
10
+ * your place.
11
+ */
12
+ import Graph from '../Graph.svelte'
13
+ import GraphLegend from '../GraphLegend.svelte'
14
+ import DiagramFrame from './DiagramFrame.svelte'
15
+ import DepthControl from '../controls/DepthControl.svelte'
16
+ import ZoomControl from '../controls/ZoomControl.svelte'
17
+ import { GraphState } from '../GraphState.svelte.js'
18
+ import type { GraphFields } from '../types.js'
19
+ import type { GraphPreset } from '../preset.js'
20
+
21
+ type Props = {
22
+ /**
23
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
24
+ * the caller owns it: this component reads it and never calls `update`, exactly as
25
+ * `Graph` does — two owners writing one state fight on every render.
26
+ */
27
+ state?: GraphState
28
+ nodes?: unknown[]
29
+ edges?: unknown[]
30
+ fields?: GraphFields
31
+ /** What area encodes. A `measures` key, or `weight`, or `degree`. */
32
+ sizeBy?: string
33
+ /** How many levels below the focus to materialise. */
34
+ levels?: number
35
+ /** The subtree to render as the whole canvas. `[]` is the root. Drilling, not zooming. */
36
+ focusPath?: string[]
37
+ controls?: boolean
38
+ maxLevels?: number
39
+ legend?: boolean
40
+ value?: string | null
41
+ preset?: GraphPreset
42
+ mode?: 'light' | 'dark'
43
+ label?: string
44
+ onselect?: (id: string | null) => void
45
+ class?: string
46
+ }
47
+
48
+ let {
49
+ state: provided,
50
+ nodes = [],
51
+ edges = [],
52
+ fields = {},
53
+ sizeBy = 'weight',
54
+ levels = $bindable(2),
55
+ focusPath = $bindable([]),
56
+ controls = false,
57
+ maxLevels = 4,
58
+ legend = false,
59
+ value = $bindable(undefined),
60
+ preset = undefined,
61
+ mode = 'light',
62
+ label = undefined,
63
+ onselect = undefined,
64
+ class: className = ''
65
+ }: Props = $props()
66
+
67
+ let zoom = $state(1)
68
+
69
+ const config = () => ({
70
+ nodes,
71
+ edges,
72
+ fields,
73
+ layout: 'sunburst',
74
+ sizeBy,
75
+ levels,
76
+ focusPath,
77
+ value,
78
+ preset,
79
+ mode,
80
+ label,
81
+ onselect
82
+ })
83
+
84
+ // svelte-ignore state_referenced_locally
85
+ const graph = provided ?? new GraphState(config())
86
+
87
+ $effect(() => {
88
+ if (!provided) {
89
+ graph.update(config())
90
+
91
+ return
92
+ }
93
+
94
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
95
+ // and are never reset here. What this component DOES own is the options its own
96
+ // controls drive — the layout above all, since choosing this component is choosing it.
97
+ // `apply` merges; `update` would revert everything it does not name.
98
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
99
+ config()
100
+ graph.apply(mine)
101
+ })
102
+ </script>
103
+
104
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
105
+ inside is always defined, so the frame would render an empty control bar over
106
+ every diagram that asked for none. -->
107
+ {#snippet controlBar()}
108
+ <DepthControl {levels} max={maxLevels} onchange={(v) => (levels = v)} />
109
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
110
+ {/snippet}
111
+
112
+ {#snippet legendBar()}
113
+ <GraphLegend state={graph} groups />
114
+ {/snippet}
115
+
116
+ <DiagramFrame
117
+ class={className}
118
+ overlay={controls ? controlBar : undefined}
119
+ footer={legend ? legendBar : undefined}
120
+ >
121
+ {#snippet canvas()}
122
+ <Graph state={graph} bind:zoom arrows={false} />
123
+ {/snippet}
124
+ </DiagramFrame>