@rokkit/graph 1.7.0 → 1.8.1

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 (120) hide show
  1. package/README.md +67 -1
  2. package/dist/Graph.svelte +234 -259
  3. package/dist/Graph.svelte.d.ts +1 -1
  4. package/dist/GraphLegend.svelte +81 -10
  5. package/dist/GraphLegend.svelte.d.ts +15 -0
  6. package/dist/GraphState.svelte.d.ts +168 -51
  7. package/dist/GraphState.svelte.js +406 -305
  8. package/dist/actions/canvas.d.ts +18 -0
  9. package/dist/actions/canvas.js +59 -0
  10. package/dist/actions/choices.d.ts +12 -0
  11. package/dist/actions/choices.js +23 -0
  12. package/dist/actions/interactions.d.ts +20 -0
  13. package/dist/actions/interactions.js +57 -0
  14. package/dist/actions/listen.d.ts +2 -0
  15. package/dist/actions/listen.js +10 -0
  16. package/dist/canvas/frame.d.ts +41 -0
  17. package/dist/canvas/frame.js +34 -0
  18. package/dist/controls/BundleControl.svelte +11 -10
  19. package/dist/controls/BundleControl.svelte.d.ts +0 -7
  20. package/dist/controls/DensityControl.svelte +17 -9
  21. package/dist/controls/DensityControl.svelte.d.ts +0 -8
  22. package/dist/controls/DepthControl.svelte +11 -4
  23. package/dist/controls/DepthControl.svelte.d.ts +0 -9
  24. package/dist/controls/DrillBar.svelte +63 -0
  25. package/dist/controls/DrillBar.svelte.d.ts +9 -0
  26. package/dist/controls/EdgeStyleControl.svelte +10 -10
  27. package/dist/controls/EdgeStyleControl.svelte.d.ts +0 -6
  28. package/dist/controls/HiddenControl.svelte +27 -0
  29. package/dist/controls/HiddenControl.svelte.d.ts +7 -0
  30. package/dist/controls/MeasureControl.svelte +39 -0
  31. package/dist/controls/MeasureControl.svelte.d.ts +12 -0
  32. package/dist/controls/ToggleControl.svelte +33 -0
  33. package/dist/controls/ToggleControl.svelte.d.ts +13 -0
  34. package/dist/controls/ViolationsControl.svelte +27 -0
  35. package/dist/controls/ViolationsControl.svelte.d.ts +7 -0
  36. package/dist/controls/ZoomControl.svelte +21 -18
  37. package/dist/controls/zoom.d.ts +3 -0
  38. package/dist/controls/zoom.js +6 -0
  39. package/dist/diagrams/ArcDiagram.svelte +116 -0
  40. package/dist/diagrams/ArcDiagram.svelte.d.ts +31 -0
  41. package/dist/diagrams/CallTree.svelte +17 -5
  42. package/dist/diagrams/CallTree.svelte.d.ts +5 -1
  43. package/dist/diagrams/DependencyDiagram.svelte +18 -6
  44. package/dist/diagrams/DependencyDiagram.svelte.d.ts +5 -1
  45. package/dist/diagrams/DependencyMatrix.svelte +156 -0
  46. package/dist/diagrams/DependencyMatrix.svelte.d.ts +34 -0
  47. package/dist/diagrams/ErDiagram.svelte +18 -6
  48. package/dist/diagrams/ErDiagram.svelte.d.ts +5 -1
  49. package/dist/diagrams/LayersDiagram.svelte +124 -0
  50. package/dist/diagrams/LayersDiagram.svelte.d.ts +30 -0
  51. package/dist/diagrams/Neighborhood.svelte +26 -6
  52. package/dist/diagrams/Neighborhood.svelte.d.ts +11 -1
  53. package/dist/diagrams/PolymetricTree.svelte +116 -0
  54. package/dist/diagrams/PolymetricTree.svelte.d.ts +28 -0
  55. package/dist/diagrams/StructureDiagram.svelte +38 -8
  56. package/dist/diagrams/StructureDiagram.svelte.d.ts +9 -1
  57. package/dist/diagrams/Sunburst.svelte +44 -8
  58. package/dist/diagrams/Sunburst.svelte.d.ts +12 -2
  59. package/dist/diagrams/Treemap.svelte +44 -8
  60. package/dist/diagrams/Treemap.svelte.d.ts +12 -2
  61. package/dist/index.d.ts +13 -3
  62. package/dist/index.js +11 -2
  63. package/dist/layout/arcs.d.ts +6 -0
  64. package/dist/layout/arcs.js +141 -0
  65. package/dist/layout/cards.js +9 -2
  66. package/dist/layout/edges.d.ts +13 -1
  67. package/dist/layout/edges.js +35 -0
  68. package/dist/layout/extent.d.ts +17 -0
  69. package/dist/layout/extent.js +27 -0
  70. package/dist/layout/flow.js +3 -1
  71. package/dist/layout/index.d.ts +4 -1
  72. package/dist/layout/index.js +17 -2
  73. package/dist/layout/layers.d.ts +6 -0
  74. package/dist/layout/layers.js +180 -0
  75. package/dist/layout/matrix.d.ts +50 -0
  76. package/dist/layout/matrix.js +110 -0
  77. package/dist/layout/neighborhood.js +16 -7
  78. package/dist/layout/options.d.ts +1 -1
  79. package/dist/layout/options.js +16 -3
  80. package/dist/layout/percentile.d.ts +8 -0
  81. package/dist/layout/percentile.js +13 -0
  82. package/dist/layout/polymetric.d.ts +3 -0
  83. package/dist/layout/polymetric.js +221 -0
  84. package/dist/layout/radial.js +3 -1
  85. package/dist/layout/structure.js +7 -1
  86. package/dist/layout/sunburst.js +11 -4
  87. package/dist/layout/types.d.ts +83 -0
  88. package/dist/layout/world.js +11 -4
  89. package/dist/messages.d.ts +14 -0
  90. package/dist/messages.js +17 -0
  91. package/dist/model/condense.d.ts +8 -0
  92. package/dist/model/condense.js +114 -0
  93. package/dist/model/entities.d.ts +12 -0
  94. package/dist/model/entities.js +12 -0
  95. package/dist/model/normalize.d.ts +2 -6
  96. package/dist/model/normalize.js +83 -5
  97. package/dist/model/relationships.d.ts +30 -0
  98. package/dist/model/relationships.js +51 -0
  99. package/dist/model/share.d.ts +9 -0
  100. package/dist/model/share.js +43 -0
  101. package/dist/model/tree.d.ts +7 -1
  102. package/dist/model/tree.js +11 -4
  103. package/dist/preset.d.ts +18 -0
  104. package/dist/preset.js +34 -3
  105. package/dist/schema/EntitiesView.svelte +8 -6
  106. package/dist/schema/EntityView.svelte +13 -10
  107. package/dist/state/GraphConfig.svelte.d.ts +124 -0
  108. package/dist/state/GraphConfig.svelte.js +253 -0
  109. package/dist/state/GraphDrill.svelte.d.ts +52 -0
  110. package/dist/state/GraphDrill.svelte.js +115 -0
  111. package/dist/state/GraphGroups.svelte.d.ts +35 -0
  112. package/dist/state/GraphGroups.svelte.js +73 -0
  113. package/dist/state/GraphSelection.svelte.d.ts +53 -0
  114. package/dist/state/GraphSelection.svelte.js +94 -0
  115. package/dist/state/intents.d.ts +33 -0
  116. package/dist/state/intents.js +30 -0
  117. package/dist/state/text.d.ts +28 -0
  118. package/dist/state/text.js +56 -0
  119. package/dist/types.d.ts +92 -1
  120. package/package.json +5 -4
@@ -0,0 +1,12 @@
1
+ type Props = {
2
+ channel: 'width' | 'height' | 'color';
3
+ value?: string;
4
+ /** The measures to offer — `GraphState.measureKeys`, what the data has. */
5
+ options: string[];
6
+ /** Offer "none" — a host with two metrics yet can leave shade off. */
7
+ allowNone?: boolean;
8
+ onchange?: (value: string | undefined) => void;
9
+ };
10
+ declare const MeasureControl: import("svelte").Component<Props, {}, "">;
11
+ type MeasureControl = ReturnType<typeof MeasureControl>;
12
+ export default MeasureControl;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ /**
3
+ * An on/off control — the shape of every graph toggle (violations, hidden pairs, bundling,
4
+ * edge style). It reports the state the reader asked for; each public control maps that
5
+ * back to its own value and keeps its own `data-*` hook. Internal: not exported.
6
+ */
7
+ import { choices } from '../actions/choices.js'
8
+
9
+ type Props = {
10
+ pressed: boolean
11
+ label: string
12
+ title: string
13
+ /** A glyph beside the label, decorative. */
14
+ glyph?: string
15
+ /** The control's own style hooks, e.g. `{ 'data-graph-bundle': 'bundled' }`. */
16
+ hooks?: Record<string, string>
17
+ onchange?: (pressed: boolean) => void
18
+ }
19
+
20
+ let { pressed, label, title, glyph = undefined, hooks = {}, onchange }: Props = $props()
21
+ </script>
22
+
23
+ <button
24
+ type="button"
25
+ {...hooks}
26
+ aria-pressed={pressed}
27
+ {title}
28
+ data-graph-choice={pressed ? 'off' : 'on'}
29
+ use:choices={{ onchoose: (next) => onchange?.(next === 'on') }}
30
+ >
31
+ {#if glyph}<span data-graph-control-glyph aria-hidden="true">{glyph}</span>{/if}
32
+ <span data-graph-control-label>{label}</span>
33
+ </button>
@@ -0,0 +1,13 @@
1
+ type Props = {
2
+ pressed: boolean;
3
+ label: string;
4
+ title: string;
5
+ /** A glyph beside the label, decorative. */
6
+ glyph?: string;
7
+ /** The control's own style hooks, e.g. `{ 'data-graph-bundle': 'bundled' }`. */
8
+ hooks?: Record<string, string>;
9
+ onchange?: (pressed: boolean) => void;
10
+ };
11
+ declare const ToggleControl: import("svelte").Component<Props, {}, "">;
12
+ type ToggleControl = ReturnType<typeof ToggleControl>;
13
+ export default ToggleControl;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Every edge, or only the ones that climb (#167).
4
+ *
5
+ * A toggle: on a real codebase the conformant edges are the vast majority and drown the
6
+ * signal, so "only violations" is the view a reviewer actually reads.
7
+ */
8
+ import ToggleControl from './ToggleControl.svelte'
9
+ import { say } from '../messages.js'
10
+
11
+ type Props = {
12
+ showEdges?: 'all' | 'violations'
13
+ onchange?: (value: 'all' | 'violations') => void
14
+ }
15
+
16
+ let { showEdges = 'all', onchange }: Props = $props()
17
+
18
+ const only = $derived(showEdges === 'violations')
19
+ </script>
20
+
21
+ <ToggleControl
22
+ pressed={only}
23
+ hooks={{ 'data-graph-violations': showEdges }}
24
+ label={say(only ? 'violationsOnly' : 'allEdges')}
25
+ title={say(only ? 'violationsOnlyTitle' : 'allEdgesTitle')}
26
+ onchange={(on) => onchange?.(on ? 'violations' : 'all')}
27
+ />
@@ -0,0 +1,7 @@
1
+ type Props = {
2
+ showEdges?: 'all' | 'violations';
3
+ onchange?: (value: 'all' | 'violations') => void;
4
+ };
5
+ declare const ViolationsControl: import("svelte").Component<Props, {}, "">;
6
+ type ViolationsControl = ReturnType<typeof ViolationsControl>;
7
+ export default ViolationsControl;
@@ -6,7 +6,10 @@
6
6
  * navigable without a trackpad. The canvas handles ctrl+wheel and pinch itself — this is
7
7
  * the discoverable path to the same value, not the only one.
8
8
  */
9
- import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP, clampZoom } from './zoom.js'
9
+ import { choices } from '../actions/choices.js'
10
+ import { say } from '../messages.js'
11
+ import { ZOOM_MAX, ZOOM_MIN, nextZoom } from './zoom.js'
12
+ import type { ZoomMove } from './zoom.js'
10
13
 
11
14
  type Props = {
12
15
  zoom?: number
@@ -16,32 +19,32 @@
16
19
  let { zoom = 1, onchange }: Props = $props()
17
20
 
18
21
  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
22
  </script>
25
23
 
26
- <div data-graph-zoom-controls role="group" aria-label="Zoom">
24
+ <!-- Clamped by `nextZoom` as well as in the canvas: the buttons disable at the ends, but a
25
+ caller can hand this control a value from anywhere, and reporting one outside the range
26
+ would push the canvas past a limit it has already agreed to. -->
27
+ <div
28
+ data-graph-zoom-controls
29
+ role="group"
30
+ aria-label={say('zoom')}
31
+ use:choices={{ onchoose: (move) => onchange?.(nextZoom(zoom, move as ZoomMove)) }}
32
+ >
27
33
  <button
28
34
  type="button"
29
35
  data-graph-zoom="out"
30
- aria-label="Zoom out"
31
- disabled={zoom <= ZOOM_MIN}
32
- onclick={() => step(1 / ZOOM_STEP)}>−</button
36
+ data-graph-choice="out"
37
+ aria-label={say('zoomOut')}
38
+ disabled={zoom <= ZOOM_MIN}>−</button
33
39
  >
34
- <button
35
- type="button"
36
- data-graph-zoom="reset"
37
- aria-label="Reset zoom to fit"
38
- onclick={() => onchange?.(1)}>{percent}%</button
40
+ <button type="button" data-graph-zoom="reset" data-graph-choice="reset" aria-label={say('zoomReset')}
41
+ >{percent}%</button
39
42
  >
40
43
  <button
41
44
  type="button"
42
45
  data-graph-zoom="in"
43
- aria-label="Zoom in"
44
- disabled={zoom >= ZOOM_MAX}
45
- onclick={() => step(ZOOM_STEP)}>+</button
46
+ data-graph-choice="in"
47
+ aria-label={say('zoomIn')}
48
+ disabled={zoom >= ZOOM_MAX}>+</button
46
49
  >
47
50
  </div>
@@ -4,3 +4,6 @@ export declare const ZOOM_MAX = 4;
4
4
  export declare const ZOOM_STEP = 1.25;
5
5
  /** `value` brought inside the range. */
6
6
  export declare const clampZoom: (value: number) => number;
7
+ export type ZoomMove = 'in' | 'out' | 'reset';
8
+ /** The zoom after one move: a notch in or out, kept inside the range, or back to fit. */
9
+ export declare function nextZoom(zoom: number, move: ZoomMove): number;
@@ -10,3 +10,9 @@ export const ZOOM_MAX = 4;
10
10
  export const ZOOM_STEP = 1.25;
11
11
  /** `value` brought inside the range. */
12
12
  export const clampZoom = (value) => Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, value));
13
+ /** The zoom after one move: a notch in or out, kept inside the range, or back to fit. */
14
+ export function nextZoom(zoom, move) {
15
+ if (move === 'reset')
16
+ return 1;
17
+ return clampZoom(zoom * (move === 'in' ? ZOOM_STEP : 1 / ZOOM_STEP));
18
+ }
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Two relations over one shared, ordered axis (#169): the coupling the code declares on the
4
+ * left, the coupling the history reveals on the right.
5
+ *
6
+ * The finding is a pair with an arc on one side and none on the other — files that change
7
+ * together with nothing in the code tying them. The host flags those `hidden` (rokkit
8
+ * computes no history); `HiddenControl` shows only them. The legend is on by default: which
9
+ * side is which cannot be read off the arcs.
10
+ */
11
+ import Graph from '../Graph.svelte'
12
+ import GraphLegend from '../GraphLegend.svelte'
13
+ import DiagramFrame from './DiagramFrame.svelte'
14
+ import DrillBar from '../controls/DrillBar.svelte'
15
+ import HiddenControl from '../controls/HiddenControl.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
+ /** Share one state with another view — see `DependencyDiagram`. */
23
+ state?: GraphState
24
+ nodes?: unknown[]
25
+ edges?: unknown[]
26
+ fields?: GraphFields
27
+ /** The relation drawn on the right. Omitted, overlays go right and the rest left. */
28
+ above?: string
29
+ /** Every pair, or only the hidden ones. Bindable — the control moves it. */
30
+ showEdges?: 'all' | 'hidden'
31
+ /**
32
+ * The two sides' names in the legend, left then right. Omitted, they come from the data:
33
+ * the relations drawn on each side, else Declared / Observed from the locale.
34
+ */
35
+ sideLabels?: [string, string]
36
+ controls?: boolean
37
+ /** On by default: the two sides are unreadable without a key. */
38
+ legend?: boolean
39
+ value?: string | null
40
+ preset?: GraphPreset
41
+ mode?: 'light' | 'dark'
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
+ above = undefined,
53
+ showEdges = $bindable('all'),
54
+ sideLabels = undefined,
55
+ controls = false,
56
+ legend = true,
57
+ value = $bindable(undefined),
58
+ preset = undefined,
59
+ mode = 'light',
60
+ label = undefined,
61
+ onselect = undefined,
62
+ class: className = ''
63
+ }: Props = $props()
64
+
65
+ let zoom = $state(1)
66
+
67
+ const config = () => ({
68
+ nodes,
69
+ edges,
70
+ fields,
71
+ layout: 'arcs',
72
+ above,
73
+ showEdges,
74
+ value,
75
+ preset,
76
+ mode,
77
+ label,
78
+ onselect
79
+ })
80
+
81
+ // svelte-ignore state_referenced_locally
82
+ const graph = provided ?? new GraphState(config())
83
+
84
+ $effect(() => {
85
+ if (!provided) {
86
+ graph.update(config())
87
+ return
88
+ }
89
+ // A supplied state is the caller's — see DependencyDiagram.
90
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
91
+ config()
92
+ graph.apply(mine)
93
+ })
94
+ </script>
95
+
96
+ {#snippet controlBar()}
97
+ <DrillBar state={graph} />
98
+ {#if controls}
99
+ <HiddenControl {showEdges} onchange={(v) => (showEdges = v)} />
100
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
101
+ {/if}
102
+ {/snippet}
103
+
104
+ {#snippet legendBar()}
105
+ <GraphLegend state={graph} sides={sideLabels ?? true} hidden />
106
+ {/snippet}
107
+
108
+ <DiagramFrame
109
+ class={className}
110
+ overlay={controls || graph.showsDrillBar ? controlBar : undefined}
111
+ footer={legend ? legendBar : undefined}
112
+ >
113
+ {#snippet canvas()}
114
+ <Graph state={graph} bind:zoom arrows={false} />
115
+ {/snippet}
116
+ </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 another view — see `DependencyDiagram`. */
6
+ state?: GraphState;
7
+ nodes?: unknown[];
8
+ edges?: unknown[];
9
+ fields?: GraphFields;
10
+ /** The relation drawn on the right. Omitted, overlays go right and the rest left. */
11
+ above?: string;
12
+ /** Every pair, or only the hidden ones. Bindable — the control moves it. */
13
+ showEdges?: 'all' | 'hidden';
14
+ /**
15
+ * The two sides' names in the legend, left then right. Omitted, they come from the data:
16
+ * the relations drawn on each side, else Declared / Observed from the locale.
17
+ */
18
+ sideLabels?: [string, string];
19
+ controls?: boolean;
20
+ /** On by default: the two sides are unreadable without a key. */
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 ArcDiagram: import("svelte").Component<Props, {}, "value" | "showEdges">;
30
+ type ArcDiagram = ReturnType<typeof ArcDiagram>;
31
+ export default ArcDiagram;
@@ -14,9 +14,10 @@
14
14
  import GraphLegend from '../GraphLegend.svelte'
15
15
  import DiagramFrame from './DiagramFrame.svelte'
16
16
  import DepthControl from '../controls/DepthControl.svelte'
17
+ import DrillBar from '../controls/DrillBar.svelte'
17
18
  import ZoomControl from '../controls/ZoomControl.svelte'
18
19
  import { GraphState } from '../GraphState.svelte.js'
19
- import type { GraphFields } from '../types.js'
20
+ import type { GraphFields, GraphNode } from '../types.js'
20
21
  import type { GraphPreset } from '../preset.js'
21
22
 
22
23
  type Props = {
@@ -48,6 +49,10 @@
48
49
  arrows?: boolean
49
50
  label?: string
50
51
  onselect?: (id: string | null) => void
52
+ /** The reader expanded a group node (#166) — with its members in `nodes`, they appear at once. */
53
+ onexpand?: (id: string, node: GraphNode) => void
54
+ /** The reader collapsed a group back into one node. */
55
+ oncollapse?: (id: string, node: GraphNode) => void
51
56
  class?: string
52
57
  }
53
58
 
@@ -70,6 +75,8 @@
70
75
  arrows = true,
71
76
  label = undefined,
72
77
  onselect = undefined,
78
+ onexpand = undefined,
79
+ oncollapse = undefined,
73
80
  class: className = ''
74
81
  }: Props = $props()
75
82
 
@@ -89,7 +96,9 @@
89
96
  preset,
90
97
  mode,
91
98
  label,
92
- onselect
99
+ onselect,
100
+ onexpand,
101
+ oncollapse
93
102
  })
94
103
 
95
104
  // svelte-ignore state_referenced_locally
@@ -116,8 +125,11 @@
116
125
  inside is always defined, so the frame would render an empty control bar over
117
126
  every diagram that asked for none. -->
118
127
  {#snippet controlBar()}
119
- <DepthControl levels={levels ?? maxLevels} max={maxLevels} onchange={(v) => (levels = v)} />
120
- <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
128
+ <DrillBar state={graph} />
129
+ {#if controls}
130
+ <DepthControl levels={levels ?? maxLevels} max={maxLevels} onchange={(v) => (levels = v)} />
131
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
132
+ {/if}
121
133
  {/snippet}
122
134
 
123
135
  {#snippet legendBar()}
@@ -126,7 +138,7 @@
126
138
 
127
139
  <DiagramFrame
128
140
  class={className}
129
- overlay={controls ? controlBar : undefined}
141
+ overlay={controls || graph.showsDrillBar ? controlBar : undefined}
130
142
  footer={legend ? legendBar : undefined}
131
143
  >
132
144
  {#snippet canvas()}
@@ -1,5 +1,5 @@
1
1
  import { GraphState } from '../GraphState.svelte.js';
2
- import type { GraphFields } from '../types.js';
2
+ import type { GraphFields, GraphNode } from '../types.js';
3
3
  import type { GraphPreset } from '../preset.js';
4
4
  type Props = {
5
5
  /**
@@ -30,6 +30,10 @@ type Props = {
30
30
  arrows?: boolean;
31
31
  label?: string;
32
32
  onselect?: (id: string | null) => void;
33
+ /** The reader expanded a group node (#166) — with its members in `nodes`, they appear at once. */
34
+ onexpand?: (id: string, node: GraphNode) => void;
35
+ /** The reader collapsed a group back into one node. */
36
+ oncollapse?: (id: string, node: GraphNode) => void;
33
37
  class?: string;
34
38
  };
35
39
  declare const CallTree: import("svelte").Component<Props, {}, "value" | "levels" | "root">;
@@ -13,11 +13,12 @@
13
13
  import GraphLegend from '../GraphLegend.svelte'
14
14
  import DiagramFrame from './DiagramFrame.svelte'
15
15
  import DensityControl from '../controls/DensityControl.svelte'
16
+ import DrillBar from '../controls/DrillBar.svelte'
16
17
  import EdgeStyleControl from '../controls/EdgeStyleControl.svelte'
17
18
  import ZoomControl from '../controls/ZoomControl.svelte'
18
19
  import { GraphState } from '../GraphState.svelte.js'
19
20
  import type { Density, EdgeStyle } from '../layout/types.js'
20
- import type { GraphFields } from '../types.js'
21
+ import type { GraphFields, GraphNode } from '../types.js'
21
22
  import type { GraphPreset } from '../preset.js'
22
23
 
23
24
  type Props = {
@@ -41,6 +42,10 @@
41
42
  arrows?: boolean
42
43
  label?: string
43
44
  onselect?: (id: string | null) => void
45
+ /** The reader expanded a group node (#166) — with its members in `nodes`, they appear at once. */
46
+ onexpand?: (id: string, node: GraphNode) => void
47
+ /** The reader collapsed a group back into one node. */
48
+ oncollapse?: (id: string, node: GraphNode) => void
44
49
  class?: string
45
50
  }
46
51
 
@@ -60,6 +65,8 @@
60
65
  arrows = true,
61
66
  label = undefined,
62
67
  onselect = undefined,
68
+ onexpand = undefined,
69
+ oncollapse = undefined,
63
70
  class: className = ''
64
71
  }: Props = $props()
65
72
 
@@ -77,7 +84,9 @@
77
84
  preset,
78
85
  mode,
79
86
  label,
80
- onselect
87
+ onselect,
88
+ onexpand,
89
+ oncollapse
81
90
  })
82
91
 
83
92
  // svelte-ignore state_referenced_locally
@@ -104,9 +113,12 @@
104
113
  inside is always defined, so the frame would render an empty control bar over
105
114
  every diagram that asked for none. -->
106
115
  {#snippet controlBar()}
107
- <DensityControl {density} onchange={(v) => (density = v)} />
108
- <EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
109
- <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
116
+ <DrillBar state={graph} />
117
+ {#if controls}
118
+ <DensityControl {density} onchange={(v) => (density = v)} />
119
+ <EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
120
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
121
+ {/if}
110
122
  {/snippet}
111
123
 
112
124
  {#snippet legendBar()}
@@ -115,7 +127,7 @@
115
127
 
116
128
  <DiagramFrame
117
129
  class={className}
118
- overlay={controls ? controlBar : undefined}
130
+ overlay={controls || graph.showsDrillBar ? controlBar : undefined}
119
131
  footer={legend ? legendBar : undefined}
120
132
  >
121
133
  {#snippet canvas()}
@@ -1,6 +1,6 @@
1
1
  import { GraphState } from '../GraphState.svelte.js';
2
2
  import type { Density, EdgeStyle } from '../layout/types.js';
3
- import type { GraphFields } from '../types.js';
3
+ import type { GraphFields, GraphNode } from '../types.js';
4
4
  import type { GraphPreset } from '../preset.js';
5
5
  type Props = {
6
6
  /**
@@ -23,6 +23,10 @@ type Props = {
23
23
  arrows?: boolean;
24
24
  label?: string;
25
25
  onselect?: (id: string | null) => void;
26
+ /** The reader expanded a group node (#166) — with its members in `nodes`, they appear at once. */
27
+ onexpand?: (id: string, node: GraphNode) => void;
28
+ /** The reader collapsed a group back into one node. */
29
+ oncollapse?: (id: string, node: GraphNode) => void;
26
30
  class?: string;
27
31
  };
28
32
  declare const DependencyDiagram: import("svelte").Component<Props, {}, "value" | "density" | "edgeStyle">;
@@ -0,0 +1,156 @@
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, cellState, matrixFrame, matrixLabel } from '../layout/matrix.js'
17
+ import { interactions } from '../actions/interactions.js'
18
+ import type { NodeAxis } from '../layout/types.js'
19
+ import type { GraphFields } from '../types.js'
20
+
21
+ type Props = {
22
+ /** Share one state with another view. The caller owns it; this component never updates it. */
23
+ state?: GraphState
24
+ nodes?: unknown[]
25
+ edges?: unknown[]
26
+ fields?: GraphFields
27
+ /** Keep each group contiguous and outline it on the diagonal. `null` for one flat order. */
28
+ groupBy?: NodeAxis | null
29
+ /** Cell size in pixels. */
30
+ cell?: number
31
+ value?: string | null
32
+ label?: string
33
+ onselect?: (id: string | null) => void
34
+ class?: string
35
+ }
36
+
37
+ let {
38
+ state: provided,
39
+ nodes = [],
40
+ edges = [],
41
+ fields = {},
42
+ groupBy = 'group',
43
+ cell = 16,
44
+ value = $bindable(undefined),
45
+ label = undefined,
46
+ onselect = undefined,
47
+ class: className = ''
48
+ }: Props = $props()
49
+
50
+ const config = () => ({
51
+ nodes,
52
+ edges,
53
+ fields,
54
+ value,
55
+ onselect: (id: string | null) => {
56
+ value = id
57
+ onselect?.(id)
58
+ }
59
+ })
60
+
61
+ // svelte-ignore state_referenced_locally
62
+ const graph = provided ?? new GraphState(config())
63
+
64
+ $effect(() => {
65
+ if (!provided) graph.update(config())
66
+ })
67
+
68
+ const matrix = $derived(buildMatrix(graph.model, { groupBy: groupBy ?? undefined }))
69
+
70
+ const frame = $derived(matrixFrame(matrix, cell))
71
+ const labelW = $derived(frame.labelW)
72
+ const n = $derived(frame.n)
73
+ const selected = $derived(graph.value)
74
+ const name = $derived(label ?? matrixLabel(n, matrix.cells.length, matrix.above))
75
+ </script>
76
+
77
+ <div data-graph-matrix class={className} role="group" aria-label={name} use:interactions={{ state: graph }}>
78
+ <svg width={frame.width} height={frame.height} viewBox="0 0 {frame.width} {frame.height}">
79
+ <!-- Column headings, rotated so a long name costs height rather than width. -->
80
+ {#each matrix.labels as text, i (matrix.order[i])}
81
+ <text
82
+ data-matrix-col={matrix.order[i]}
83
+ data-matrix-state={selected === matrix.order[i] ? 'selected' : undefined}
84
+ transform="translate({labelW + i * cell + cell / 2 + 4}, {labelW - 6}) rotate(-90)"
85
+ >{text}</text
86
+ >
87
+ {/each}
88
+
89
+ {#each matrix.labels as text, i (matrix.order[i])}
90
+ {@const id = matrix.order[i]}
91
+ <g
92
+ data-matrix-row={id}
93
+ data-matrix-state={selected === id ? 'selected' : undefined}
94
+ role="button"
95
+ tabindex="0"
96
+ aria-pressed={selected === id}
97
+ aria-label={text}
98
+ data-graph-press="toggle"
99
+ data-graph-key={id}
100
+ >
101
+ <rect
102
+ x="0"
103
+ y={labelW + i * cell}
104
+ width={labelW + n * cell}
105
+ height={cell}
106
+ data-matrix-band
107
+ />
108
+ <text
109
+ x={labelW - 6}
110
+ y={labelW + i * cell + cell / 2}
111
+ text-anchor="end"
112
+ dominant-baseline="middle">{text}</text
113
+ >
114
+ </g>
115
+ {/each}
116
+
117
+ {#each matrix.order as id, i (id)}
118
+ <rect
119
+ data-matrix-diagonal
120
+ x={labelW + i * cell}
121
+ y={labelW + i * cell}
122
+ width={cell}
123
+ height={cell}
124
+ />
125
+ {/each}
126
+
127
+ {#each matrix.cells as c (`${c.row}:${c.col}`)}
128
+ <g
129
+ data-matrix-cell
130
+ data-matrix-from={c.source}
131
+ data-matrix-to={c.target}
132
+ data-matrix-above={c.above ? '' : undefined}
133
+ data-matrix-state={cellState(selected, c.source, c.target)}
134
+ style:--cell-weight={c.count / matrix.maxCount}
135
+ >
136
+ <rect
137
+ x={labelW + c.col * cell + 1}
138
+ y={labelW + c.row * cell + 1}
139
+ width={cell}
140
+ height={cell}
141
+ />
142
+ <title>{c.source} → {c.target} ({c.count})</title>
143
+ </g>
144
+ {/each}
145
+
146
+ {#each matrix.blocks as b (b.name + b.start)}
147
+ <rect
148
+ data-matrix-block={b.name}
149
+ x={labelW + b.start * cell}
150
+ y={labelW + b.start * cell}
151
+ width={(b.end - b.start + 1) * cell}
152
+ height={(b.end - b.start + 1) * cell}
153
+ />
154
+ {/each}
155
+ </svg>
156
+ </div>