@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.
- package/README.md +67 -1
- package/dist/Graph.svelte +234 -259
- package/dist/Graph.svelte.d.ts +1 -1
- package/dist/GraphLegend.svelte +81 -10
- package/dist/GraphLegend.svelte.d.ts +15 -0
- package/dist/GraphState.svelte.d.ts +168 -51
- package/dist/GraphState.svelte.js +406 -305
- package/dist/actions/canvas.d.ts +18 -0
- package/dist/actions/canvas.js +59 -0
- package/dist/actions/choices.d.ts +12 -0
- package/dist/actions/choices.js +23 -0
- package/dist/actions/interactions.d.ts +20 -0
- package/dist/actions/interactions.js +57 -0
- package/dist/actions/listen.d.ts +2 -0
- package/dist/actions/listen.js +10 -0
- package/dist/canvas/frame.d.ts +41 -0
- package/dist/canvas/frame.js +34 -0
- package/dist/controls/BundleControl.svelte +11 -10
- package/dist/controls/BundleControl.svelte.d.ts +0 -7
- package/dist/controls/DensityControl.svelte +17 -9
- package/dist/controls/DensityControl.svelte.d.ts +0 -8
- package/dist/controls/DepthControl.svelte +11 -4
- package/dist/controls/DepthControl.svelte.d.ts +0 -9
- package/dist/controls/DrillBar.svelte +63 -0
- package/dist/controls/DrillBar.svelte.d.ts +9 -0
- package/dist/controls/EdgeStyleControl.svelte +10 -10
- package/dist/controls/EdgeStyleControl.svelte.d.ts +0 -6
- package/dist/controls/HiddenControl.svelte +27 -0
- package/dist/controls/HiddenControl.svelte.d.ts +7 -0
- package/dist/controls/MeasureControl.svelte +39 -0
- package/dist/controls/MeasureControl.svelte.d.ts +12 -0
- package/dist/controls/ToggleControl.svelte +33 -0
- package/dist/controls/ToggleControl.svelte.d.ts +13 -0
- package/dist/controls/ViolationsControl.svelte +27 -0
- package/dist/controls/ViolationsControl.svelte.d.ts +7 -0
- package/dist/controls/ZoomControl.svelte +21 -18
- package/dist/controls/zoom.d.ts +3 -0
- package/dist/controls/zoom.js +6 -0
- package/dist/diagrams/ArcDiagram.svelte +116 -0
- package/dist/diagrams/ArcDiagram.svelte.d.ts +31 -0
- package/dist/diagrams/CallTree.svelte +17 -5
- package/dist/diagrams/CallTree.svelte.d.ts +5 -1
- package/dist/diagrams/DependencyDiagram.svelte +18 -6
- package/dist/diagrams/DependencyDiagram.svelte.d.ts +5 -1
- package/dist/diagrams/DependencyMatrix.svelte +156 -0
- package/dist/diagrams/DependencyMatrix.svelte.d.ts +34 -0
- package/dist/diagrams/ErDiagram.svelte +18 -6
- package/dist/diagrams/ErDiagram.svelte.d.ts +5 -1
- package/dist/diagrams/LayersDiagram.svelte +124 -0
- package/dist/diagrams/LayersDiagram.svelte.d.ts +30 -0
- package/dist/diagrams/Neighborhood.svelte +26 -6
- package/dist/diagrams/Neighborhood.svelte.d.ts +11 -1
- package/dist/diagrams/PolymetricTree.svelte +116 -0
- package/dist/diagrams/PolymetricTree.svelte.d.ts +28 -0
- package/dist/diagrams/StructureDiagram.svelte +38 -8
- package/dist/diagrams/StructureDiagram.svelte.d.ts +9 -1
- package/dist/diagrams/Sunburst.svelte +44 -8
- package/dist/diagrams/Sunburst.svelte.d.ts +12 -2
- package/dist/diagrams/Treemap.svelte +44 -8
- package/dist/diagrams/Treemap.svelte.d.ts +12 -2
- package/dist/index.d.ts +13 -3
- package/dist/index.js +11 -2
- package/dist/layout/arcs.d.ts +6 -0
- package/dist/layout/arcs.js +141 -0
- package/dist/layout/cards.js +9 -2
- package/dist/layout/edges.d.ts +13 -1
- package/dist/layout/edges.js +35 -0
- package/dist/layout/extent.d.ts +17 -0
- package/dist/layout/extent.js +27 -0
- package/dist/layout/flow.js +3 -1
- package/dist/layout/index.d.ts +4 -1
- package/dist/layout/index.js +17 -2
- package/dist/layout/layers.d.ts +6 -0
- package/dist/layout/layers.js +180 -0
- package/dist/layout/matrix.d.ts +50 -0
- package/dist/layout/matrix.js +110 -0
- package/dist/layout/neighborhood.js +16 -7
- package/dist/layout/options.d.ts +1 -1
- package/dist/layout/options.js +16 -3
- package/dist/layout/percentile.d.ts +8 -0
- package/dist/layout/percentile.js +13 -0
- package/dist/layout/polymetric.d.ts +3 -0
- package/dist/layout/polymetric.js +221 -0
- package/dist/layout/radial.js +3 -1
- package/dist/layout/structure.js +7 -1
- package/dist/layout/sunburst.js +11 -4
- package/dist/layout/types.d.ts +83 -0
- package/dist/layout/world.js +11 -4
- package/dist/messages.d.ts +14 -0
- package/dist/messages.js +17 -0
- package/dist/model/condense.d.ts +8 -0
- package/dist/model/condense.js +114 -0
- package/dist/model/entities.d.ts +12 -0
- package/dist/model/entities.js +12 -0
- package/dist/model/normalize.d.ts +2 -6
- package/dist/model/normalize.js +83 -5
- package/dist/model/relationships.d.ts +30 -0
- package/dist/model/relationships.js +51 -0
- package/dist/model/share.d.ts +9 -0
- package/dist/model/share.js +43 -0
- package/dist/model/tree.d.ts +7 -1
- package/dist/model/tree.js +11 -4
- package/dist/preset.d.ts +18 -0
- package/dist/preset.js +34 -3
- package/dist/schema/EntitiesView.svelte +8 -6
- package/dist/schema/EntityView.svelte +13 -10
- package/dist/state/GraphConfig.svelte.d.ts +124 -0
- package/dist/state/GraphConfig.svelte.js +253 -0
- package/dist/state/GraphDrill.svelte.d.ts +52 -0
- package/dist/state/GraphDrill.svelte.js +115 -0
- package/dist/state/GraphGroups.svelte.d.ts +35 -0
- package/dist/state/GraphGroups.svelte.js +73 -0
- package/dist/state/GraphSelection.svelte.d.ts +53 -0
- package/dist/state/GraphSelection.svelte.js +94 -0
- package/dist/state/intents.d.ts +33 -0
- package/dist/state/intents.js +30 -0
- package/dist/state/text.d.ts +28 -0
- package/dist/state/text.js +56 -0
- package/dist/types.d.ts +92 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
data-graph-choice="out"
|
|
37
|
+
aria-label={say('zoomOut')}
|
|
38
|
+
disabled={zoom <= ZOOM_MIN}>−</button
|
|
33
39
|
>
|
|
34
|
-
<button
|
|
35
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
data-graph-choice="in"
|
|
47
|
+
aria-label={say('zoomIn')}
|
|
48
|
+
disabled={zoom >= ZOOM_MAX}>+</button
|
|
46
49
|
>
|
|
47
50
|
</div>
|
package/dist/controls/zoom.d.ts
CHANGED
|
@@ -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;
|
package/dist/controls/zoom.js
CHANGED
|
@@ -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
|
-
<
|
|
120
|
-
|
|
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
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
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>
|