@rokkit/graph 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -2
- package/dist/Graph.svelte +121 -72
- package/dist/GraphLegend.svelte +112 -0
- package/dist/GraphLegend.svelte.d.ts +33 -0
- package/dist/GraphState.svelte.d.ts +107 -15
- package/dist/GraphState.svelte.js +186 -3
- package/dist/controls/BundleControl.svelte +30 -0
- package/dist/controls/BundleControl.svelte.d.ts +16 -0
- package/dist/controls/DensityControl.svelte +39 -0
- package/dist/controls/DensityControl.svelte.d.ts +16 -0
- package/dist/controls/DepthControl.svelte +34 -0
- package/dist/controls/DepthControl.svelte.d.ts +17 -0
- package/dist/controls/EdgeStyleControl.svelte +31 -0
- package/dist/controls/EdgeStyleControl.svelte.d.ts +14 -0
- package/dist/controls/ZoomControl.svelte +47 -0
- package/dist/controls/ZoomControl.svelte.d.ts +7 -0
- package/dist/controls/zoom.d.ts +6 -0
- package/dist/controls/zoom.js +12 -0
- package/dist/diagrams/CallTree.svelte +135 -0
- package/dist/diagrams/CallTree.svelte.d.ts +37 -0
- package/dist/diagrams/DependencyDiagram.svelte +124 -0
- package/dist/diagrams/DependencyDiagram.svelte.d.ts +30 -0
- package/dist/diagrams/DiagramFrame.svelte +36 -0
- package/dist/diagrams/DiagramFrame.svelte.d.ts +20 -0
- package/dist/diagrams/ErDiagram.svelte +128 -0
- package/dist/diagrams/ErDiagram.svelte.d.ts +33 -0
- package/dist/diagrams/Neighborhood.svelte +127 -0
- package/dist/diagrams/Neighborhood.svelte.d.ts +33 -0
- package/dist/diagrams/StructureDiagram.svelte +131 -0
- package/dist/diagrams/StructureDiagram.svelte.d.ts +31 -0
- package/dist/diagrams/Sunburst.svelte +124 -0
- package/dist/diagrams/Sunburst.svelte.d.ts +32 -0
- package/dist/diagrams/Treemap.svelte +124 -0
- package/dist/diagrams/Treemap.svelte.d.ts +32 -0
- package/dist/icons.d.ts +5 -0
- package/dist/icons.js +5 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +23 -1
- package/dist/layout/arc.d.ts +13 -0
- package/dist/layout/arc.js +58 -0
- package/dist/layout/bundle.d.ts +30 -0
- package/dist/layout/bundle.js +80 -0
- package/dist/layout/cluster.js +2 -0
- package/dist/layout/flow.d.ts +8 -0
- package/dist/layout/flow.js +141 -0
- package/dist/layout/hierarchy.d.ts +42 -0
- package/dist/layout/hierarchy.js +175 -0
- package/dist/layout/index.d.ts +7 -1
- package/dist/layout/index.js +8 -2
- package/dist/layout/neighborhood.js +164 -28
- package/dist/layout/options.d.ts +44 -0
- package/dist/layout/options.js +106 -0
- package/dist/layout/order.d.ts +11 -0
- package/dist/layout/order.js +153 -0
- package/dist/layout/points.js +27 -35
- package/dist/layout/radial.d.ts +16 -0
- package/dist/layout/radial.js +214 -0
- package/dist/layout/rank.d.ts +15 -0
- package/dist/layout/rank.js +105 -0
- package/dist/layout/rings.d.ts +20 -0
- package/dist/layout/rings.js +92 -0
- package/dist/layout/sizing.d.ts +37 -0
- package/dist/layout/sizing.js +79 -0
- package/dist/layout/squarify.d.ts +14 -0
- package/dist/layout/squarify.js +114 -0
- package/dist/layout/structure.d.ts +9 -0
- package/dist/layout/structure.js +367 -0
- package/dist/layout/sunburst.d.ts +8 -0
- package/dist/layout/sunburst.js +126 -0
- package/dist/layout/types.d.ts +141 -0
- package/dist/layout/world.d.ts +9 -0
- package/dist/layout/world.js +144 -0
- package/dist/model/normalize.js +47 -1
- package/dist/model/tree.d.ts +27 -0
- package/dist/model/tree.js +182 -0
- package/dist/types.d.ts +55 -3
- package/package.json +9 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A call graph as a tidy tree or dendrogram around a circle.
|
|
4
|
+
*
|
|
5
|
+
* Angle separates the subtrees, radius carries the depth — which is the question a call
|
|
6
|
+
* graph asks. Packing it into boxes instead answers "how much is there" and, at any real
|
|
7
|
+
* size, reads as a stack of colliding labels.
|
|
8
|
+
*
|
|
9
|
+
* `levels` is the control that makes it legible over a codebase: a dendrogram of a whole
|
|
10
|
+
* repo puts every leaf on one rim, each a fraction of a degree wide. Show the crates, then
|
|
11
|
+
* set `root` to drill into one — a dendrogram of dendrograms.
|
|
12
|
+
*/
|
|
13
|
+
import Graph from '../Graph.svelte'
|
|
14
|
+
import GraphLegend from '../GraphLegend.svelte'
|
|
15
|
+
import DiagramFrame from './DiagramFrame.svelte'
|
|
16
|
+
import DepthControl from '../controls/DepthControl.svelte'
|
|
17
|
+
import ZoomControl from '../controls/ZoomControl.svelte'
|
|
18
|
+
import { GraphState } from '../GraphState.svelte.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
|
+
/** `tree` reads radius as depth; `dendrogram` pins every leaf to the rim. */
|
|
33
|
+
radialMode?: 'tree' | 'dendrogram'
|
|
34
|
+
/** How many levels out from the root to draw. Unset draws the whole tree. */
|
|
35
|
+
levels?: number
|
|
36
|
+
/** The node to centre on — drilling. Unset roots at whatever nothing calls. */
|
|
37
|
+
root?: string | null
|
|
38
|
+
/** What a node's area encodes. `degree` counts edges. */
|
|
39
|
+
sizeBy?: string
|
|
40
|
+
sizeScale?: 'linear' | 'log'
|
|
41
|
+
controls?: boolean
|
|
42
|
+
/** Deepest level the depth control offers. */
|
|
43
|
+
maxLevels?: number
|
|
44
|
+
legend?: boolean
|
|
45
|
+
value?: string | null
|
|
46
|
+
preset?: GraphPreset
|
|
47
|
+
mode?: 'light' | 'dark'
|
|
48
|
+
arrows?: boolean
|
|
49
|
+
label?: string
|
|
50
|
+
onselect?: (id: string | null) => void
|
|
51
|
+
class?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
state: provided,
|
|
56
|
+
nodes = [],
|
|
57
|
+
edges = [],
|
|
58
|
+
fields = {},
|
|
59
|
+
radialMode = 'tree',
|
|
60
|
+
levels = $bindable(undefined),
|
|
61
|
+
root = $bindable(null),
|
|
62
|
+
sizeBy = 'degree',
|
|
63
|
+
sizeScale = 'linear',
|
|
64
|
+
controls = false,
|
|
65
|
+
maxLevels = 4,
|
|
66
|
+
legend = false,
|
|
67
|
+
value = $bindable(undefined),
|
|
68
|
+
preset = undefined,
|
|
69
|
+
mode = 'light',
|
|
70
|
+
arrows = true,
|
|
71
|
+
label = undefined,
|
|
72
|
+
onselect = undefined,
|
|
73
|
+
class: className = ''
|
|
74
|
+
}: Props = $props()
|
|
75
|
+
|
|
76
|
+
let zoom = $state(1)
|
|
77
|
+
|
|
78
|
+
const config = () => ({
|
|
79
|
+
nodes,
|
|
80
|
+
edges,
|
|
81
|
+
fields,
|
|
82
|
+
layout: 'radial',
|
|
83
|
+
radialMode,
|
|
84
|
+
levels,
|
|
85
|
+
root,
|
|
86
|
+
sizeBy,
|
|
87
|
+
sizeScale,
|
|
88
|
+
value,
|
|
89
|
+
preset,
|
|
90
|
+
mode,
|
|
91
|
+
label,
|
|
92
|
+
onselect
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// svelte-ignore state_referenced_locally
|
|
96
|
+
const graph = provided ?? new GraphState(config())
|
|
97
|
+
|
|
98
|
+
$effect(() => {
|
|
99
|
+
if (!provided) {
|
|
100
|
+
graph.update(config())
|
|
101
|
+
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A supplied state is the caller's: their data, preset, mode and selection stay theirs
|
|
106
|
+
// and are never reset here. What this component DOES own is the options its own
|
|
107
|
+
// controls drive — the layout above all, since choosing this component is choosing it.
|
|
108
|
+
// `apply` merges; `update` would revert everything it does not name.
|
|
109
|
+
const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
|
|
110
|
+
config()
|
|
111
|
+
graph.apply(mine)
|
|
112
|
+
})
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
|
|
116
|
+
inside is always defined, so the frame would render an empty control bar over
|
|
117
|
+
every diagram that asked for none. -->
|
|
118
|
+
{#snippet controlBar()}
|
|
119
|
+
<DepthControl levels={levels ?? maxLevels} max={maxLevels} onchange={(v) => (levels = v)} />
|
|
120
|
+
<ZoomControl {zoom} onchange={(v) => (zoom = v)} />
|
|
121
|
+
{/snippet}
|
|
122
|
+
|
|
123
|
+
{#snippet legendBar()}
|
|
124
|
+
<GraphLegend state={graph} kinds />
|
|
125
|
+
{/snippet}
|
|
126
|
+
|
|
127
|
+
<DiagramFrame
|
|
128
|
+
class={className}
|
|
129
|
+
overlay={controls ? controlBar : undefined}
|
|
130
|
+
footer={legend ? legendBar : undefined}
|
|
131
|
+
>
|
|
132
|
+
{#snippet canvas()}
|
|
133
|
+
<Graph state={graph} bind:zoom {arrows} />
|
|
134
|
+
{/snippet}
|
|
135
|
+
</DiagramFrame>
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Share one state across this diagram and, say, an entity table beside it. Given one,
|
|
7
|
+
* the caller owns it: this component reads it and never calls `update`, exactly as
|
|
8
|
+
* `Graph` does — two owners writing one state fight on every render.
|
|
9
|
+
*/
|
|
10
|
+
state?: GraphState;
|
|
11
|
+
nodes?: unknown[];
|
|
12
|
+
edges?: unknown[];
|
|
13
|
+
fields?: GraphFields;
|
|
14
|
+
/** `tree` reads radius as depth; `dendrogram` pins every leaf to the rim. */
|
|
15
|
+
radialMode?: 'tree' | 'dendrogram';
|
|
16
|
+
/** How many levels out from the root to draw. Unset draws the whole tree. */
|
|
17
|
+
levels?: number;
|
|
18
|
+
/** The node to centre on — drilling. Unset roots at whatever nothing calls. */
|
|
19
|
+
root?: string | null;
|
|
20
|
+
/** What a node's area encodes. `degree` counts edges. */
|
|
21
|
+
sizeBy?: string;
|
|
22
|
+
sizeScale?: 'linear' | 'log';
|
|
23
|
+
controls?: boolean;
|
|
24
|
+
/** Deepest level the depth control offers. */
|
|
25
|
+
maxLevels?: number;
|
|
26
|
+
legend?: boolean;
|
|
27
|
+
value?: string | null;
|
|
28
|
+
preset?: GraphPreset;
|
|
29
|
+
mode?: 'light' | 'dark';
|
|
30
|
+
arrows?: boolean;
|
|
31
|
+
label?: string;
|
|
32
|
+
onselect?: (id: string | null) => void;
|
|
33
|
+
class?: string;
|
|
34
|
+
};
|
|
35
|
+
declare const CallTree: import("svelte").Component<Props, {}, "value" | "levels" | "root">;
|
|
36
|
+
type CallTree = ReturnType<typeof CallTree>;
|
|
37
|
+
export default CallTree;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* What reads, writes, calls and contains what — the other half of a schema.
|
|
4
|
+
*
|
|
5
|
+
* dbd's v2 model splits `tables`/`refs` from `entities`/`deps` because an ER renderer
|
|
6
|
+
* wants one and a call-graph renderer wants the other. Pass
|
|
7
|
+
* `toGraphInput(model, 'dependencies')` and this draws the second.
|
|
8
|
+
*
|
|
9
|
+
* Keyed by RELATION rather than by kind: on this canvas the verb is the information, and
|
|
10
|
+
* `reads` versus `writes` is the distinction a reader is here for.
|
|
11
|
+
*/
|
|
12
|
+
import Graph from '../Graph.svelte'
|
|
13
|
+
import GraphLegend from '../GraphLegend.svelte'
|
|
14
|
+
import DiagramFrame from './DiagramFrame.svelte'
|
|
15
|
+
import DensityControl from '../controls/DensityControl.svelte'
|
|
16
|
+
import EdgeStyleControl from '../controls/EdgeStyleControl.svelte'
|
|
17
|
+
import ZoomControl from '../controls/ZoomControl.svelte'
|
|
18
|
+
import { GraphState } from '../GraphState.svelte.js'
|
|
19
|
+
import type { Density, EdgeStyle } from '../layout/types.js'
|
|
20
|
+
import type { GraphFields } from '../types.js'
|
|
21
|
+
import type { GraphPreset } from '../preset.js'
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
/**
|
|
25
|
+
* Share one state across this diagram and, say, an entity table beside it. Given one,
|
|
26
|
+
* the caller owns it: this component reads it and never calls `update`, exactly as
|
|
27
|
+
* `Graph` does — two owners writing one state fight on every render.
|
|
28
|
+
*/
|
|
29
|
+
state?: GraphState
|
|
30
|
+
nodes?: unknown[]
|
|
31
|
+
edges?: unknown[]
|
|
32
|
+
fields?: GraphFields
|
|
33
|
+
density?: Density
|
|
34
|
+
edgeStyle?: EdgeStyle
|
|
35
|
+
groupTint?: boolean
|
|
36
|
+
controls?: boolean
|
|
37
|
+
legend?: boolean
|
|
38
|
+
value?: string | null
|
|
39
|
+
preset?: GraphPreset
|
|
40
|
+
mode?: 'light' | 'dark'
|
|
41
|
+
arrows?: boolean
|
|
42
|
+
label?: string
|
|
43
|
+
onselect?: (id: string | null) => void
|
|
44
|
+
class?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let {
|
|
48
|
+
state: provided,
|
|
49
|
+
nodes = [],
|
|
50
|
+
edges = [],
|
|
51
|
+
fields = {},
|
|
52
|
+
density = $bindable('names'),
|
|
53
|
+
edgeStyle = $bindable('curved'),
|
|
54
|
+
groupTint = true,
|
|
55
|
+
controls = false,
|
|
56
|
+
legend = false,
|
|
57
|
+
value = $bindable(undefined),
|
|
58
|
+
preset = undefined,
|
|
59
|
+
mode = 'light',
|
|
60
|
+
arrows = true,
|
|
61
|
+
label = undefined,
|
|
62
|
+
onselect = undefined,
|
|
63
|
+
class: className = ''
|
|
64
|
+
}: Props = $props()
|
|
65
|
+
|
|
66
|
+
let zoom = $state(1)
|
|
67
|
+
|
|
68
|
+
const config = () => ({
|
|
69
|
+
nodes,
|
|
70
|
+
edges,
|
|
71
|
+
fields,
|
|
72
|
+
layout: 'flow',
|
|
73
|
+
density,
|
|
74
|
+
edgeStyle,
|
|
75
|
+
groupTint,
|
|
76
|
+
value,
|
|
77
|
+
preset,
|
|
78
|
+
mode,
|
|
79
|
+
label,
|
|
80
|
+
onselect
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// svelte-ignore state_referenced_locally
|
|
84
|
+
const graph = provided ?? new GraphState(config())
|
|
85
|
+
|
|
86
|
+
$effect(() => {
|
|
87
|
+
if (!provided) {
|
|
88
|
+
graph.update(config())
|
|
89
|
+
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A supplied state is the caller's: their data, preset, mode and selection stay theirs
|
|
94
|
+
// and are never reset here. What this component DOES own is the options its own
|
|
95
|
+
// controls drive — the layout above all, since choosing this component is choosing it.
|
|
96
|
+
// `apply` merges; `update` would revert everything it does not name.
|
|
97
|
+
const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
|
|
98
|
+
config()
|
|
99
|
+
graph.apply(mine)
|
|
100
|
+
})
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
|
|
104
|
+
inside is always defined, so the frame would render an empty control bar over
|
|
105
|
+
every diagram that asked for none. -->
|
|
106
|
+
{#snippet controlBar()}
|
|
107
|
+
<DensityControl {density} onchange={(v) => (density = v)} />
|
|
108
|
+
<EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
|
|
109
|
+
<ZoomControl {zoom} onchange={(v) => (zoom = v)} />
|
|
110
|
+
{/snippet}
|
|
111
|
+
|
|
112
|
+
{#snippet legendBar()}
|
|
113
|
+
<GraphLegend state={graph} relations kinds />
|
|
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} />
|
|
123
|
+
{/snippet}
|
|
124
|
+
</DiagramFrame>
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
groupTint?: boolean;
|
|
18
|
+
controls?: boolean;
|
|
19
|
+
legend?: boolean;
|
|
20
|
+
value?: string | null;
|
|
21
|
+
preset?: GraphPreset;
|
|
22
|
+
mode?: 'light' | 'dark';
|
|
23
|
+
arrows?: boolean;
|
|
24
|
+
label?: string;
|
|
25
|
+
onselect?: (id: string | null) => void;
|
|
26
|
+
class?: string;
|
|
27
|
+
};
|
|
28
|
+
declare const DependencyDiagram: import("svelte").Component<Props, {}, "value" | "density" | "edgeStyle">;
|
|
29
|
+
type DependencyDiagram = ReturnType<typeof DependencyDiagram>;
|
|
30
|
+
export default DependencyDiagram;
|
|
@@ -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>
|