@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.
- package/README.md +66 -2
- package/dist/Graph.svelte +219 -169
- package/dist/GraphLegend.svelte +112 -0
- package/dist/GraphLegend.svelte.d.ts +33 -0
- package/dist/GraphState.svelte.d.ts +115 -15
- package/dist/GraphState.svelte.js +213 -5
- 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/DependencyMatrix.svelte +177 -0
- package/dist/diagrams/DependencyMatrix.svelte.d.ts +34 -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 +21 -1
- package/dist/index.js +25 -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/edges.js +11 -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/matrix.d.ts +38 -0
- package/dist/layout/matrix.js +90 -0
- 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 +145 -0
- package/dist/layout/world.d.ts +9 -0
- package/dist/layout/world.js +144 -0
- package/dist/model/normalize.js +66 -4
- package/dist/model/tree.d.ts +27 -0
- package/dist/model/tree.js +182 -0
- package/dist/types.d.ts +75 -3
- package/package.json +9 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many levels of a containment tree to materialise.
|
|
3
|
+
*
|
|
4
|
+
* The control that makes a large tree readable at all. A radial dendrogram over a whole
|
|
5
|
+
* codebase renders every leaf on one rim, where each is a fraction of a degree wide and
|
|
6
|
+
* nothing is legible — show the crates first, then drill. Depth and drilling are the same
|
|
7
|
+
* idea from two directions: this caps how far DOWN, `focusPath` moves where you start.
|
|
8
|
+
*/
|
|
9
|
+
type Props = {
|
|
10
|
+
levels?: number;
|
|
11
|
+
/** Deepest level offered. Beyond the tree's own depth the extra buttons do nothing. */
|
|
12
|
+
max?: number;
|
|
13
|
+
onchange?: (value: number) => void;
|
|
14
|
+
};
|
|
15
|
+
declare const DepthControl: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type DepthControl = ReturnType<typeof DepthControl>;
|
|
17
|
+
export default DepthControl;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Curved or orthogonal connectors.
|
|
4
|
+
*
|
|
5
|
+
* A toggle rather than a picker: there are exactly two, and a two-option `<select>` costs a
|
|
6
|
+
* reader more interaction than the thing it is choosing between is worth.
|
|
7
|
+
*/
|
|
8
|
+
import type { EdgeStyle } from '../layout/types.js'
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
edgeStyle?: EdgeStyle
|
|
12
|
+
onchange?: (value: EdgeStyle) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { edgeStyle = 'curved', onchange }: Props = $props()
|
|
16
|
+
|
|
17
|
+
// `curved` is the default, so `orthogonal` is the pressed state — a toggle reads as "the
|
|
18
|
+
// non-default is on", not as "one of two things is selected".
|
|
19
|
+
const orthogonal = $derived(edgeStyle === 'orthogonal')
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
data-graph-edge-style={edgeStyle}
|
|
25
|
+
aria-pressed={orthogonal}
|
|
26
|
+
title={orthogonal ? 'Right-angle connectors' : 'Curved connectors'}
|
|
27
|
+
onclick={() => onchange?.(orthogonal ? 'curved' : 'orthogonal')}
|
|
28
|
+
>
|
|
29
|
+
<span data-graph-edge-style-glyph aria-hidden="true">{orthogonal ? '⌐' : '︵'}</span>
|
|
30
|
+
<span data-graph-control-label>{orthogonal ? 'Angled' : 'Curved'}</span>
|
|
31
|
+
</button>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curved or orthogonal connectors.
|
|
3
|
+
*
|
|
4
|
+
* A toggle rather than a picker: there are exactly two, and a two-option `<select>` costs a
|
|
5
|
+
* reader more interaction than the thing it is choosing between is worth.
|
|
6
|
+
*/
|
|
7
|
+
import type { EdgeStyle } from '../layout/types.js';
|
|
8
|
+
type Props = {
|
|
9
|
+
edgeStyle?: EdgeStyle;
|
|
10
|
+
onchange?: (value: EdgeStyle) => void;
|
|
11
|
+
};
|
|
12
|
+
declare const EdgeStyleControl: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type EdgeStyleControl = ReturnType<typeof EdgeStyleControl>;
|
|
14
|
+
export default EdgeStyleControl;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Zoom in, out, and back to fit.
|
|
4
|
+
*
|
|
5
|
+
* Buttons rather than a slider: each is one tab stop and one keypress, so the diagram is
|
|
6
|
+
* navigable without a trackpad. The canvas handles ctrl+wheel and pinch itself — this is
|
|
7
|
+
* the discoverable path to the same value, not the only one.
|
|
8
|
+
*/
|
|
9
|
+
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP, clampZoom } from './zoom.js'
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
zoom?: number
|
|
13
|
+
onchange?: (value: number) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { zoom = 1, onchange }: Props = $props()
|
|
17
|
+
|
|
18
|
+
const percent = $derived(Math.round(zoom * 100))
|
|
19
|
+
|
|
20
|
+
// Clamped here as well as in the canvas: the buttons disable at the ends, but a caller can
|
|
21
|
+
// hand this control a value from anywhere, and reporting one outside the range would push
|
|
22
|
+
// the canvas past a limit it has already agreed to.
|
|
23
|
+
const step = (factor: number) => onchange?.(clampZoom(zoom * factor))
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div data-graph-zoom-controls role="group" aria-label="Zoom">
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
data-graph-zoom="out"
|
|
30
|
+
aria-label="Zoom out"
|
|
31
|
+
disabled={zoom <= ZOOM_MIN}
|
|
32
|
+
onclick={() => step(1 / ZOOM_STEP)}>−</button
|
|
33
|
+
>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
data-graph-zoom="reset"
|
|
37
|
+
aria-label="Reset zoom to fit"
|
|
38
|
+
onclick={() => onchange?.(1)}>{percent}%</button
|
|
39
|
+
>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
data-graph-zoom="in"
|
|
43
|
+
aria-label="Zoom in"
|
|
44
|
+
disabled={zoom >= ZOOM_MAX}
|
|
45
|
+
onclick={() => step(ZOOM_STEP)}>+</button
|
|
46
|
+
>
|
|
47
|
+
</div>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const ZOOM_MIN = 0.25;
|
|
2
|
+
export declare const ZOOM_MAX = 4;
|
|
3
|
+
/** One step of the +/- buttons, and of one wheel notch. */
|
|
4
|
+
export declare const ZOOM_STEP = 1.25;
|
|
5
|
+
/** `value` brought inside the range. */
|
|
6
|
+
export declare const clampZoom: (value: number) => number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* Zoom bounds, shared by the canvas and the zoom control.
|
|
2
|
+
*
|
|
3
|
+
* Both have to agree: the canvas clamps a ctrl+wheel gesture and the control disables its
|
|
4
|
+
* buttons at the ends, and if those two numbers drift the control goes dead a step before the
|
|
5
|
+
* gesture does — which reads as a broken button.
|
|
6
|
+
*/
|
|
7
|
+
export const ZOOM_MIN = 0.25;
|
|
8
|
+
export const ZOOM_MAX = 4;
|
|
9
|
+
/** One step of the +/- buttons, and of one wheel notch. */
|
|
10
|
+
export const ZOOM_STEP = 1.25;
|
|
11
|
+
/** `value` brought inside the range. */
|
|
12
|
+
export const clampZoom = (value) => Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, value));
|
|
@@ -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,177 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The dependency structure matrix: every node a row and a column, a cell where the row
|
|
4
|
+
* depends on the column.
|
|
5
|
+
*
|
|
6
|
+
* Providers come first, so a layered codebase is lower-triangular and a mark ABOVE the
|
|
7
|
+
* diagonal is a dependency against the grain — a cycle, or a layer reaching up. Groups sit
|
|
8
|
+
* on the diagonal as outlined blocks, so a cross-module violation is a mark outside its
|
|
9
|
+
* block. It stays readable at sizes where a node-link drawing is a hairball, which is the
|
|
10
|
+
* reason to reach for it.
|
|
11
|
+
*
|
|
12
|
+
* Share a `state` with a `DependencyDiagram` beside it and a row click selects the same
|
|
13
|
+
* node in both.
|
|
14
|
+
*/
|
|
15
|
+
import { GraphState } from '../GraphState.svelte.js'
|
|
16
|
+
import { buildMatrix } from '../layout/matrix.js'
|
|
17
|
+
import type { NodeAxis } from '../layout/types.js'
|
|
18
|
+
import type { GraphFields } from '../types.js'
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
/** Share one state with another view. The caller owns it; this component never updates it. */
|
|
22
|
+
state?: GraphState
|
|
23
|
+
nodes?: unknown[]
|
|
24
|
+
edges?: unknown[]
|
|
25
|
+
fields?: GraphFields
|
|
26
|
+
/** Keep each group contiguous and outline it on the diagonal. `null` for one flat order. */
|
|
27
|
+
groupBy?: NodeAxis | null
|
|
28
|
+
/** Cell size in pixels. */
|
|
29
|
+
cell?: number
|
|
30
|
+
value?: string | null
|
|
31
|
+
label?: string
|
|
32
|
+
onselect?: (id: string | null) => void
|
|
33
|
+
class?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
state: provided,
|
|
38
|
+
nodes = [],
|
|
39
|
+
edges = [],
|
|
40
|
+
fields = {},
|
|
41
|
+
groupBy = 'group',
|
|
42
|
+
cell = 16,
|
|
43
|
+
value = $bindable(undefined),
|
|
44
|
+
label = undefined,
|
|
45
|
+
onselect = undefined,
|
|
46
|
+
class: className = ''
|
|
47
|
+
}: Props = $props()
|
|
48
|
+
|
|
49
|
+
const config = () => ({
|
|
50
|
+
nodes,
|
|
51
|
+
edges,
|
|
52
|
+
fields,
|
|
53
|
+
value,
|
|
54
|
+
onselect: (id: string | null) => {
|
|
55
|
+
value = id
|
|
56
|
+
onselect?.(id)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// svelte-ignore state_referenced_locally
|
|
61
|
+
const graph = provided ?? new GraphState(config())
|
|
62
|
+
|
|
63
|
+
$effect(() => {
|
|
64
|
+
if (!provided) graph.update(config())
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const matrix = $derived(buildMatrix(graph.model, { groupBy: groupBy ?? undefined }))
|
|
68
|
+
|
|
69
|
+
// Labels are measured by length, not by the DOM — deterministic, like every layout here.
|
|
70
|
+
const labelW = $derived(
|
|
71
|
+
Math.min(220, Math.max(48, Math.max(0, ...matrix.labels.map((l) => l.length)) * 6.5 + 14))
|
|
72
|
+
)
|
|
73
|
+
const n = $derived(matrix.order.length)
|
|
74
|
+
const width = $derived(labelW + n * cell + 8)
|
|
75
|
+
const height = $derived(labelW + n * cell + 8)
|
|
76
|
+
const selected = $derived(graph.value)
|
|
77
|
+
|
|
78
|
+
const name = $derived(
|
|
79
|
+
label ??
|
|
80
|
+
`Dependency matrix: ${n} nodes, ${matrix.cells.length} dependencies, ${matrix.above} above the diagonal`
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
function toggle(id: string) {
|
|
84
|
+
if (selected === id) graph.clear()
|
|
85
|
+
else graph.select(id)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onKey(event: KeyboardEvent, id: string) {
|
|
89
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
90
|
+
event.preventDefault()
|
|
91
|
+
toggle(id)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const cellState = (source: string, target: string) =>
|
|
95
|
+
selected && (source === selected || target === selected) ? 'highlight' : undefined
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<div data-graph-matrix class={className} role="group" aria-label={name}>
|
|
99
|
+
<svg {width} {height} viewBox="0 0 {width} {height}">
|
|
100
|
+
<!-- Column headings, rotated so a long name costs height rather than width. -->
|
|
101
|
+
{#each matrix.labels as text, i (matrix.order[i])}
|
|
102
|
+
<text
|
|
103
|
+
data-matrix-col={matrix.order[i]}
|
|
104
|
+
data-matrix-state={selected === matrix.order[i] ? 'selected' : undefined}
|
|
105
|
+
transform="translate({labelW + i * cell + cell / 2 + 4}, {labelW - 6}) rotate(-90)"
|
|
106
|
+
>{text}</text
|
|
107
|
+
>
|
|
108
|
+
{/each}
|
|
109
|
+
|
|
110
|
+
{#each matrix.labels as text, i (matrix.order[i])}
|
|
111
|
+
{@const id = matrix.order[i]}
|
|
112
|
+
<g
|
|
113
|
+
data-matrix-row={id}
|
|
114
|
+
data-matrix-state={selected === id ? 'selected' : undefined}
|
|
115
|
+
role="button"
|
|
116
|
+
tabindex="0"
|
|
117
|
+
aria-pressed={selected === id}
|
|
118
|
+
aria-label={text}
|
|
119
|
+
onclick={() => toggle(id)}
|
|
120
|
+
onkeydown={(event) => onKey(event, id)}
|
|
121
|
+
>
|
|
122
|
+
<rect
|
|
123
|
+
x="0"
|
|
124
|
+
y={labelW + i * cell}
|
|
125
|
+
width={labelW + n * cell}
|
|
126
|
+
height={cell}
|
|
127
|
+
data-matrix-band
|
|
128
|
+
/>
|
|
129
|
+
<text
|
|
130
|
+
x={labelW - 6}
|
|
131
|
+
y={labelW + i * cell + cell / 2}
|
|
132
|
+
text-anchor="end"
|
|
133
|
+
dominant-baseline="middle">{text}</text
|
|
134
|
+
>
|
|
135
|
+
</g>
|
|
136
|
+
{/each}
|
|
137
|
+
|
|
138
|
+
{#each matrix.order as id, i (id)}
|
|
139
|
+
<rect
|
|
140
|
+
data-matrix-diagonal
|
|
141
|
+
x={labelW + i * cell}
|
|
142
|
+
y={labelW + i * cell}
|
|
143
|
+
width={cell}
|
|
144
|
+
height={cell}
|
|
145
|
+
/>
|
|
146
|
+
{/each}
|
|
147
|
+
|
|
148
|
+
{#each matrix.cells as c (`${c.row}:${c.col}`)}
|
|
149
|
+
<g
|
|
150
|
+
data-matrix-cell
|
|
151
|
+
data-matrix-from={c.source}
|
|
152
|
+
data-matrix-to={c.target}
|
|
153
|
+
data-matrix-above={c.above ? '' : undefined}
|
|
154
|
+
data-matrix-state={cellState(c.source, c.target)}
|
|
155
|
+
style:--cell-weight={c.count / matrix.maxCount}
|
|
156
|
+
>
|
|
157
|
+
<rect
|
|
158
|
+
x={labelW + c.col * cell + 1}
|
|
159
|
+
y={labelW + c.row * cell + 1}
|
|
160
|
+
width={cell}
|
|
161
|
+
height={cell}
|
|
162
|
+
/>
|
|
163
|
+
<title>{c.source} → {c.target} ({c.count})</title>
|
|
164
|
+
</g>
|
|
165
|
+
{/each}
|
|
166
|
+
|
|
167
|
+
{#each matrix.blocks as b (b.name + b.start)}
|
|
168
|
+
<rect
|
|
169
|
+
data-matrix-block={b.name}
|
|
170
|
+
x={labelW + b.start * cell}
|
|
171
|
+
y={labelW + b.start * cell}
|
|
172
|
+
width={(b.end - b.start + 1) * cell}
|
|
173
|
+
height={(b.end - b.start + 1) * cell}
|
|
174
|
+
/>
|
|
175
|
+
{/each}
|
|
176
|
+
</svg>
|
|
177
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dependency structure matrix: every node a row and a column, a cell where the row
|
|
3
|
+
* depends on the column.
|
|
4
|
+
*
|
|
5
|
+
* Providers come first, so a layered codebase is lower-triangular and a mark ABOVE the
|
|
6
|
+
* diagonal is a dependency against the grain — a cycle, or a layer reaching up. Groups sit
|
|
7
|
+
* on the diagonal as outlined blocks, so a cross-module violation is a mark outside its
|
|
8
|
+
* block. It stays readable at sizes where a node-link drawing is a hairball, which is the
|
|
9
|
+
* reason to reach for it.
|
|
10
|
+
*
|
|
11
|
+
* Share a `state` with a `DependencyDiagram` beside it and a row click selects the same
|
|
12
|
+
* node in both.
|
|
13
|
+
*/
|
|
14
|
+
import { GraphState } from '../GraphState.svelte.js';
|
|
15
|
+
import type { NodeAxis } from '../layout/types.js';
|
|
16
|
+
import type { GraphFields } from '../types.js';
|
|
17
|
+
type Props = {
|
|
18
|
+
/** Share one state with another view. The caller owns it; this component never updates it. */
|
|
19
|
+
state?: GraphState;
|
|
20
|
+
nodes?: unknown[];
|
|
21
|
+
edges?: unknown[];
|
|
22
|
+
fields?: GraphFields;
|
|
23
|
+
/** Keep each group contiguous and outline it on the diagonal. `null` for one flat order. */
|
|
24
|
+
groupBy?: NodeAxis | null;
|
|
25
|
+
/** Cell size in pixels. */
|
|
26
|
+
cell?: number;
|
|
27
|
+
value?: string | null;
|
|
28
|
+
label?: string;
|
|
29
|
+
onselect?: (id: string | null) => void;
|
|
30
|
+
class?: string;
|
|
31
|
+
};
|
|
32
|
+
declare const DependencyMatrix: import("svelte").Component<Props, {}, "value">;
|
|
33
|
+
type DependencyMatrix = ReturnType<typeof DependencyMatrix>;
|
|
34
|
+
export default DependencyMatrix;
|