@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,32 @@
|
|
|
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
|
+
/** What area encodes. A `measures` key, or `weight`, or `degree`. */
|
|
15
|
+
sizeBy?: string;
|
|
16
|
+
/** How many levels below the focus to materialise. */
|
|
17
|
+
levels?: number;
|
|
18
|
+
/** The subtree to render as the whole canvas. `[]` is the root. Drilling, not zooming. */
|
|
19
|
+
focusPath?: string[];
|
|
20
|
+
controls?: boolean;
|
|
21
|
+
maxLevels?: number;
|
|
22
|
+
legend?: boolean;
|
|
23
|
+
value?: string | null;
|
|
24
|
+
preset?: GraphPreset;
|
|
25
|
+
mode?: 'light' | 'dark';
|
|
26
|
+
label?: string;
|
|
27
|
+
onselect?: (id: string | null) => void;
|
|
28
|
+
class?: string;
|
|
29
|
+
};
|
|
30
|
+
declare const Sunburst: import("svelte").Component<Props, {}, "value" | "focusPath" | "levels">;
|
|
31
|
+
type Sunburst = ReturnType<typeof Sunburst>;
|
|
32
|
+
export default Sunburst;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Containment as nested rectangles, area proportional to a measure.
|
|
4
|
+
*
|
|
5
|
+
* Answers "where is the mass" — a box's area is what it CONTAINS, so the tree sums
|
|
6
|
+
* subtrees rather than counting nodes. Every box is the same shape at every depth,
|
|
7
|
+
* including a leaf: a treemap nests one thing.
|
|
8
|
+
*
|
|
9
|
+
* No edges. Containment IS the relationship here, and at this scale an adjacency graph
|
|
10
|
+
* cannot be drawn.
|
|
11
|
+
*/
|
|
12
|
+
import Graph from '../Graph.svelte'
|
|
13
|
+
import GraphLegend from '../GraphLegend.svelte'
|
|
14
|
+
import DiagramFrame from './DiagramFrame.svelte'
|
|
15
|
+
import DepthControl from '../controls/DepthControl.svelte'
|
|
16
|
+
import ZoomControl from '../controls/ZoomControl.svelte'
|
|
17
|
+
import { GraphState } from '../GraphState.svelte.js'
|
|
18
|
+
import type { GraphFields } from '../types.js'
|
|
19
|
+
import type { GraphPreset } from '../preset.js'
|
|
20
|
+
|
|
21
|
+
type Props = {
|
|
22
|
+
/**
|
|
23
|
+
* Share one state across this diagram and, say, an entity table beside it. Given one,
|
|
24
|
+
* the caller owns it: this component reads it and never calls `update`, exactly as
|
|
25
|
+
* `Graph` does — two owners writing one state fight on every render.
|
|
26
|
+
*/
|
|
27
|
+
state?: GraphState
|
|
28
|
+
nodes?: unknown[]
|
|
29
|
+
edges?: unknown[]
|
|
30
|
+
fields?: GraphFields
|
|
31
|
+
/** What area encodes. A `measures` key, or `weight`, or `degree`. */
|
|
32
|
+
sizeBy?: string
|
|
33
|
+
/** How many levels below the focus to materialise. */
|
|
34
|
+
levels?: number
|
|
35
|
+
/** The subtree to render as the whole canvas. `[]` is the root. Drilling, not zooming. */
|
|
36
|
+
focusPath?: string[]
|
|
37
|
+
controls?: boolean
|
|
38
|
+
maxLevels?: number
|
|
39
|
+
legend?: boolean
|
|
40
|
+
value?: string | null
|
|
41
|
+
preset?: GraphPreset
|
|
42
|
+
mode?: 'light' | 'dark'
|
|
43
|
+
label?: string
|
|
44
|
+
onselect?: (id: string | null) => void
|
|
45
|
+
class?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let {
|
|
49
|
+
state: provided,
|
|
50
|
+
nodes = [],
|
|
51
|
+
edges = [],
|
|
52
|
+
fields = {},
|
|
53
|
+
sizeBy = 'weight',
|
|
54
|
+
levels = $bindable(2),
|
|
55
|
+
focusPath = $bindable([]),
|
|
56
|
+
controls = false,
|
|
57
|
+
maxLevels = 4,
|
|
58
|
+
legend = false,
|
|
59
|
+
value = $bindable(undefined),
|
|
60
|
+
preset = undefined,
|
|
61
|
+
mode = 'light',
|
|
62
|
+
label = undefined,
|
|
63
|
+
onselect = undefined,
|
|
64
|
+
class: className = ''
|
|
65
|
+
}: Props = $props()
|
|
66
|
+
|
|
67
|
+
let zoom = $state(1)
|
|
68
|
+
|
|
69
|
+
const config = () => ({
|
|
70
|
+
nodes,
|
|
71
|
+
edges,
|
|
72
|
+
fields,
|
|
73
|
+
layout: 'world',
|
|
74
|
+
sizeBy,
|
|
75
|
+
levels,
|
|
76
|
+
focusPath,
|
|
77
|
+
value,
|
|
78
|
+
preset,
|
|
79
|
+
mode,
|
|
80
|
+
label,
|
|
81
|
+
onselect
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// svelte-ignore state_referenced_locally
|
|
85
|
+
const graph = provided ?? new GraphState(config())
|
|
86
|
+
|
|
87
|
+
$effect(() => {
|
|
88
|
+
if (!provided) {
|
|
89
|
+
graph.update(config())
|
|
90
|
+
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A supplied state is the caller's: their data, preset, mode and selection stay theirs
|
|
95
|
+
// and are never reset here. What this component DOES own is the options its own
|
|
96
|
+
// controls drive — the layout above all, since choosing this component is choosing it.
|
|
97
|
+
// `apply` merges; `update` would revert everything it does not name.
|
|
98
|
+
const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
|
|
99
|
+
config()
|
|
100
|
+
graph.apply(mine)
|
|
101
|
+
})
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
|
|
105
|
+
inside is always defined, so the frame would render an empty control bar over
|
|
106
|
+
every diagram that asked for none. -->
|
|
107
|
+
{#snippet controlBar()}
|
|
108
|
+
<DepthControl {levels} max={maxLevels} onchange={(v) => (levels = v)} />
|
|
109
|
+
<ZoomControl {zoom} onchange={(v) => (zoom = v)} />
|
|
110
|
+
{/snippet}
|
|
111
|
+
|
|
112
|
+
{#snippet legendBar()}
|
|
113
|
+
<GraphLegend state={graph} groups />
|
|
114
|
+
{/snippet}
|
|
115
|
+
|
|
116
|
+
<DiagramFrame
|
|
117
|
+
class={className}
|
|
118
|
+
overlay={controls ? controlBar : undefined}
|
|
119
|
+
footer={legend ? legendBar : undefined}
|
|
120
|
+
>
|
|
121
|
+
{#snippet canvas()}
|
|
122
|
+
<Graph state={graph} bind:zoom arrows={false} />
|
|
123
|
+
{/snippet}
|
|
124
|
+
</DiagramFrame>
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/** What area encodes. A `measures` key, or `weight`, or `degree`. */
|
|
15
|
+
sizeBy?: string;
|
|
16
|
+
/** How many levels below the focus to materialise. */
|
|
17
|
+
levels?: number;
|
|
18
|
+
/** The subtree to render as the whole canvas. `[]` is the root. Drilling, not zooming. */
|
|
19
|
+
focusPath?: string[];
|
|
20
|
+
controls?: boolean;
|
|
21
|
+
maxLevels?: number;
|
|
22
|
+
legend?: boolean;
|
|
23
|
+
value?: string | null;
|
|
24
|
+
preset?: GraphPreset;
|
|
25
|
+
mode?: 'light' | 'dark';
|
|
26
|
+
label?: string;
|
|
27
|
+
onselect?: (id: string | null) => void;
|
|
28
|
+
class?: string;
|
|
29
|
+
};
|
|
30
|
+
declare const Treemap: import("svelte").Component<Props, {}, "value" | "focusPath" | "levels">;
|
|
31
|
+
type Treemap = ReturnType<typeof Treemap>;
|
|
32
|
+
export default Treemap;
|
package/dist/icons.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default glyph per node kind and row badge.
|
|
3
3
|
*
|
|
4
|
+
* Reachable as `@rokkit/graph/icons` — a subpath with NO Svelte imports, because the one
|
|
5
|
+
* place a consumer is REQUIRED to use it is a UnoCSS config, which Node loads directly and
|
|
6
|
+
* which therefore cannot import the main barrel (that re-exports `Graph.svelte`, and Node
|
|
7
|
+
* cannot parse `.svelte`). Found by dbd wiring its safelist.
|
|
8
|
+
*
|
|
4
9
|
* `i-glyph:<name>` is the vocabulary this repo actually ships — `packages/icons/lib/glyph.json`,
|
|
5
10
|
* 697 names, safelisted wholesale by the app's UnoCSS config. An earlier version of this map
|
|
6
11
|
* used `i-graph-<kind>`, which is defined nowhere: UnoCSS emitted no rule, so every kind
|
package/dist/icons.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default glyph per node kind and row badge.
|
|
3
3
|
*
|
|
4
|
+
* Reachable as `@rokkit/graph/icons` — a subpath with NO Svelte imports, because the one
|
|
5
|
+
* place a consumer is REQUIRED to use it is a UnoCSS config, which Node loads directly and
|
|
6
|
+
* which therefore cannot import the main barrel (that re-exports `Graph.svelte`, and Node
|
|
7
|
+
* cannot parse `.svelte`). Found by dbd wiring its safelist.
|
|
8
|
+
*
|
|
4
9
|
* `i-glyph:<name>` is the vocabulary this repo actually ships — `packages/icons/lib/glyph.json`,
|
|
5
10
|
* 697 names, safelisted wholesale by the app's UnoCSS config. An earlier version of this map
|
|
6
11
|
* used `i-graph-<kind>`, which is defined nowhere: UnoCSS emitted no rule, so every kind
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
export { default as Graph } from './Graph.svelte';
|
|
2
|
+
export { default as GraphLegend } from './GraphLegend.svelte';
|
|
3
|
+
export { default as ErDiagram } from './diagrams/ErDiagram.svelte';
|
|
4
|
+
export { default as DependencyDiagram } from './diagrams/DependencyDiagram.svelte';
|
|
5
|
+
export { default as CallTree } from './diagrams/CallTree.svelte';
|
|
6
|
+
export { default as Treemap } from './diagrams/Treemap.svelte';
|
|
7
|
+
export { default as Sunburst } from './diagrams/Sunburst.svelte';
|
|
8
|
+
export { default as StructureDiagram } from './diagrams/StructureDiagram.svelte';
|
|
9
|
+
export { default as Neighborhood } from './diagrams/Neighborhood.svelte';
|
|
10
|
+
export { default as DependencyMatrix } from './diagrams/DependencyMatrix.svelte';
|
|
11
|
+
export { default as DensityControl } from './controls/DensityControl.svelte';
|
|
12
|
+
export { default as EdgeStyleControl } from './controls/EdgeStyleControl.svelte';
|
|
13
|
+
export { default as ZoomControl } from './controls/ZoomControl.svelte';
|
|
14
|
+
export { default as DepthControl } from './controls/DepthControl.svelte';
|
|
15
|
+
export { default as BundleControl } from './controls/BundleControl.svelte';
|
|
16
|
+
export { ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, clampZoom } from './controls/zoom.js';
|
|
2
17
|
export { GraphState } from './GraphState.svelte.js';
|
|
3
18
|
export { normalizeGraph } from './model/normalize.js';
|
|
19
|
+
export { buildTree, findNode } from './model/tree.js';
|
|
20
|
+
export type { TreeNode } from './model/tree.js';
|
|
4
21
|
export { readPath } from './model/path.js';
|
|
5
22
|
export { createGraphPreset, defaultGraphPreset, resolveGroupStyles } from './preset.js';
|
|
6
23
|
export { DEFAULT_ICONS } from './icons.js';
|
|
7
|
-
export { cluster, neighborhood, points, layouts } from './layout/index.js';
|
|
24
|
+
export { cluster, flow, neighborhood, points, radial, structure, sunburst, world, layouts } from './layout/index.js';
|
|
25
|
+
export { LAYOUT_OPTIONS, appliesTo } from './layout/options.js';
|
|
26
|
+
export { buildMatrix } from './layout/matrix.js';
|
|
27
|
+
export type { Matrix, MatrixCell, MatrixBlock, MatrixOptions } from './layout/matrix.js';
|
|
8
28
|
export type { EntityRow, GraphStateConfig, Relationship } from './GraphState.svelte.js';
|
|
9
29
|
export type { GraphChannel, GraphPreset, GraphShades } from './preset.js';
|
|
10
30
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
// Public surface for `@rokkit/graph`.
|
|
2
|
+
// The canvas. It draws what a layout hands it and ships no chrome — pair it with the
|
|
3
|
+
// controls below, or reach for a named diagram, which is the composition already made.
|
|
2
4
|
export { default as Graph } from './Graph.svelte';
|
|
5
|
+
export { default as GraphLegend } from './GraphLegend.svelte';
|
|
6
|
+
// Named diagrams: one layout, the controls that mean something for that picture, an opt-in
|
|
7
|
+
// legend. `@rokkit/chart` has `BarChart` and `LineChart` over one `Plot`; these are the same
|
|
8
|
+
// idea over one `Graph`, and each is usable on its own.
|
|
9
|
+
export { default as ErDiagram } from './diagrams/ErDiagram.svelte';
|
|
10
|
+
export { default as DependencyDiagram } from './diagrams/DependencyDiagram.svelte';
|
|
11
|
+
export { default as CallTree } from './diagrams/CallTree.svelte';
|
|
12
|
+
export { default as Treemap } from './diagrams/Treemap.svelte';
|
|
13
|
+
export { default as Sunburst } from './diagrams/Sunburst.svelte';
|
|
14
|
+
export { default as StructureDiagram } from './diagrams/StructureDiagram.svelte';
|
|
15
|
+
export { default as Neighborhood } from './diagrams/Neighborhood.svelte';
|
|
16
|
+
export { default as DependencyMatrix } from './diagrams/DependencyMatrix.svelte';
|
|
17
|
+
// Controls, for composing your own arrangement over the bare canvas.
|
|
18
|
+
export { default as DensityControl } from './controls/DensityControl.svelte';
|
|
19
|
+
export { default as EdgeStyleControl } from './controls/EdgeStyleControl.svelte';
|
|
20
|
+
export { default as ZoomControl } from './controls/ZoomControl.svelte';
|
|
21
|
+
export { default as DepthControl } from './controls/DepthControl.svelte';
|
|
22
|
+
export { default as BundleControl } from './controls/BundleControl.svelte';
|
|
23
|
+
export { ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, clampZoom } from './controls/zoom.js';
|
|
3
24
|
export { GraphState } from './GraphState.svelte.js';
|
|
4
25
|
export { normalizeGraph } from './model/normalize.js';
|
|
26
|
+
export { buildTree, findNode } from './model/tree.js';
|
|
5
27
|
export { readPath } from './model/path.js';
|
|
6
28
|
export { createGraphPreset, defaultGraphPreset, resolveGroupStyles } from './preset.js';
|
|
7
29
|
export { DEFAULT_ICONS } from './icons.js';
|
|
8
|
-
export { cluster, neighborhood, points, layouts } from './layout/index.js';
|
|
30
|
+
export { cluster, flow, neighborhood, points, radial, structure, sunburst, world, layouts } from './layout/index.js';
|
|
31
|
+
export { LAYOUT_OPTIONS, appliesTo } from './layout/options.js';
|
|
32
|
+
export { buildMatrix } from './layout/matrix.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Wedge = {
|
|
2
|
+
r0: number;
|
|
3
|
+
r1: number;
|
|
4
|
+
a0: number;
|
|
5
|
+
a1: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The wedge as a path, centred on `(cx, cy)`.
|
|
9
|
+
*
|
|
10
|
+
* An innermost wedge has `r0 === 0`, where the two inner corners are the same point and an
|
|
11
|
+
* inner arc is undefined — that one is a pie slice, closing through the centre instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare function arcPath(cx: number, cy: number, wedge: Wedge): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* An annulus sector as an SVG path.
|
|
2
|
+
*
|
|
3
|
+
* Kept out of the component because it is arithmetic, not rendering: the layout reports a
|
|
4
|
+
* wedge as four numbers, and turning those into a path is a pure function that can be tested
|
|
5
|
+
* to exact coordinates. The same split `edgePath` already makes.
|
|
6
|
+
*/
|
|
7
|
+
const TAU = Math.PI * 2;
|
|
8
|
+
/** Point on a circle of radius `r` at angle `a`, relative to a centre at the origin. */
|
|
9
|
+
function at(cx, cy, r, a) {
|
|
10
|
+
return [cx + Math.cos(a) * r, cy + Math.sin(a) * r];
|
|
11
|
+
}
|
|
12
|
+
const fmt = (n) => Number(n.toFixed(3));
|
|
13
|
+
/**
|
|
14
|
+
* A full turn cannot be one arc.
|
|
15
|
+
*
|
|
16
|
+
* SVG's `A` command draws the arc BETWEEN two points, and for a complete circle those two
|
|
17
|
+
* points coincide — so the command is a no-op and the wedge vanishes. A single child covering
|
|
18
|
+
* its parent's whole span is the common case at the root, not an edge case, so the span is
|
|
19
|
+
* halved and drawn as two arcs.
|
|
20
|
+
*/
|
|
21
|
+
function isFullTurn(wedge) {
|
|
22
|
+
return wedge.a1 - wedge.a0 >= TAU - 1e-9;
|
|
23
|
+
}
|
|
24
|
+
/** Ring path for a span that covers the whole circle: two half-arcs, outer then inner. */
|
|
25
|
+
function fullRing(cx, cy, wedge) {
|
|
26
|
+
const { r0, r1 } = wedge;
|
|
27
|
+
const ring = (r, sweep) => {
|
|
28
|
+
const [ax, ay] = at(cx, cy, r, 0);
|
|
29
|
+
const [bx, by] = at(cx, cy, r, Math.PI);
|
|
30
|
+
return (`M ${fmt(ax)} ${fmt(ay)} ` +
|
|
31
|
+
`A ${fmt(r)} ${fmt(r)} 0 0 ${sweep} ${fmt(bx)} ${fmt(by)} ` +
|
|
32
|
+
`A ${fmt(r)} ${fmt(r)} 0 0 ${sweep} ${fmt(ax)} ${fmt(ay)} Z`);
|
|
33
|
+
};
|
|
34
|
+
// The inner circle is drawn the opposite way round, so the even-odd/nonzero fill punches
|
|
35
|
+
// it out and the result is a ring rather than a disc.
|
|
36
|
+
return r0 > 0 ? `${ring(r1, 1)} ${ring(r0, 0)}` : ring(r1, 1);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The wedge as a path, centred on `(cx, cy)`.
|
|
40
|
+
*
|
|
41
|
+
* An innermost wedge has `r0 === 0`, where the two inner corners are the same point and an
|
|
42
|
+
* inner arc is undefined — that one is a pie slice, closing through the centre instead.
|
|
43
|
+
*/
|
|
44
|
+
export function arcPath(cx, cy, wedge) {
|
|
45
|
+
if (isFullTurn(wedge))
|
|
46
|
+
return fullRing(cx, cy, wedge);
|
|
47
|
+
const { r0, r1, a0, a1 } = wedge;
|
|
48
|
+
const large = a1 - a0 > Math.PI ? 1 : 0;
|
|
49
|
+
const [ox0, oy0] = at(cx, cy, r1, a0);
|
|
50
|
+
const [ox1, oy1] = at(cx, cy, r1, a1);
|
|
51
|
+
const outer = `M ${fmt(ox0)} ${fmt(oy0)} A ${fmt(r1)} ${fmt(r1)} 0 ${large} 1 ${fmt(ox1)} ${fmt(oy1)}`;
|
|
52
|
+
if (r0 <= 0)
|
|
53
|
+
return `${outer} L ${fmt(cx)} ${fmt(cy)} Z`;
|
|
54
|
+
const [ix1, iy1] = at(cx, cy, r0, a1);
|
|
55
|
+
const [ix0, iy0] = at(cx, cy, r0, a0);
|
|
56
|
+
return (`${outer} L ${fmt(ix1)} ${fmt(iy1)} ` +
|
|
57
|
+
`A ${fmt(r0)} ${fmt(r0)} 0 ${large} 0 ${fmt(ix0)} ${fmt(iy0)} Z`);
|
|
58
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type Point = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Where two paths stop agreeing.
|
|
7
|
+
*
|
|
8
|
+
* The tree's lowest common ancestor, by prefix — `TreeNode.path` is the full chain from the
|
|
9
|
+
* root, so no parent pointers are needed and nothing has to be walked twice.
|
|
10
|
+
*/
|
|
11
|
+
export declare function commonDepth(a: string[], b: string[]): number;
|
|
12
|
+
/**
|
|
13
|
+
* Pull each control point toward the straight line between the endpoints.
|
|
14
|
+
*
|
|
15
|
+
* `beta` is the tension: 1 keeps the full route through the tree, 0 collapses it onto the
|
|
16
|
+
* chord. Endpoints are unmoved at any beta — an edge has to start and finish where its nodes
|
|
17
|
+
* are, whatever the middle does.
|
|
18
|
+
*/
|
|
19
|
+
export declare function relax(points: Point[], beta: number): Point[];
|
|
20
|
+
/**
|
|
21
|
+
* A uniform cubic B-spline through `points`, as an SVG path.
|
|
22
|
+
*
|
|
23
|
+
* B-spline rather than a curve THROUGH the points: a spline that interpolates every control
|
|
24
|
+
* point reproduces the corner at each tree level, and the whole reason to route through the
|
|
25
|
+
* tree is to get a smooth flow. The ends are duplicated so the curve actually reaches the
|
|
26
|
+
* first and last point instead of stopping short of them.
|
|
27
|
+
*/
|
|
28
|
+
export declare function splinePath(points: Point[]): string;
|
|
29
|
+
/** The bundled route between two leaves, relaxed by `beta` and drawn as a spline. */
|
|
30
|
+
export declare function bundlePath(points: Point[], beta: number): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* Hierarchical edge bundling — Holten (2006).
|
|
2
|
+
*
|
|
3
|
+
* A radial dendrogram of a codebase has every module on one rim, and the calls between them
|
|
4
|
+
* are chords across the middle. Drawn straight, a few hundred of those are a solid disc of
|
|
5
|
+
* ink. Bundled, edges that travel the same way through the TREE are pulled together, and the
|
|
6
|
+
* picture turns into visible flows between parts of the structure — which is the thing the
|
|
7
|
+
* reader came for.
|
|
8
|
+
*
|
|
9
|
+
* The bundle is a B-spline through the path each edge takes up to the lowest common ancestor
|
|
10
|
+
* and back down, relaxed toward the straight line by `beta`. At `beta = 0` it IS the straight
|
|
11
|
+
* line, which is why that is the "Straight" setting rather than a separate code path.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Where two paths stop agreeing.
|
|
15
|
+
*
|
|
16
|
+
* The tree's lowest common ancestor, by prefix — `TreeNode.path` is the full chain from the
|
|
17
|
+
* root, so no parent pointers are needed and nothing has to be walked twice.
|
|
18
|
+
*/
|
|
19
|
+
export function commonDepth(a, b) {
|
|
20
|
+
let i = 0;
|
|
21
|
+
while (i < a.length && i < b.length && a[i] === b[i])
|
|
22
|
+
i++;
|
|
23
|
+
return i;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pull each control point toward the straight line between the endpoints.
|
|
27
|
+
*
|
|
28
|
+
* `beta` is the tension: 1 keeps the full route through the tree, 0 collapses it onto the
|
|
29
|
+
* chord. Endpoints are unmoved at any beta — an edge has to start and finish where its nodes
|
|
30
|
+
* are, whatever the middle does.
|
|
31
|
+
*/
|
|
32
|
+
export function relax(points, beta) {
|
|
33
|
+
if (points.length < 2)
|
|
34
|
+
return points;
|
|
35
|
+
const first = points[0];
|
|
36
|
+
const last = points[points.length - 1];
|
|
37
|
+
const span = points.length - 1;
|
|
38
|
+
return points.map((p, i) => {
|
|
39
|
+
const t = i / span;
|
|
40
|
+
const straightX = first.x + (last.x - first.x) * t;
|
|
41
|
+
const straightY = first.y + (last.y - first.y) * t;
|
|
42
|
+
return {
|
|
43
|
+
x: beta * p.x + (1 - beta) * straightX,
|
|
44
|
+
y: beta * p.y + (1 - beta) * straightY
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const fmt = (n) => Number(n.toFixed(2));
|
|
49
|
+
/**
|
|
50
|
+
* A uniform cubic B-spline through `points`, as an SVG path.
|
|
51
|
+
*
|
|
52
|
+
* B-spline rather than a curve THROUGH the points: a spline that interpolates every control
|
|
53
|
+
* point reproduces the corner at each tree level, and the whole reason to route through the
|
|
54
|
+
* tree is to get a smooth flow. The ends are duplicated so the curve actually reaches the
|
|
55
|
+
* first and last point instead of stopping short of them.
|
|
56
|
+
*/
|
|
57
|
+
export function splinePath(points) {
|
|
58
|
+
if (points.length === 0)
|
|
59
|
+
return '';
|
|
60
|
+
if (points.length === 1)
|
|
61
|
+
return `M ${fmt(points[0].x)} ${fmt(points[0].y)}`;
|
|
62
|
+
if (points.length === 2) {
|
|
63
|
+
return `M ${fmt(points[0].x)} ${fmt(points[0].y)} L ${fmt(points[1].x)} ${fmt(points[1].y)}`;
|
|
64
|
+
}
|
|
65
|
+
// Clamped: repeating the endpoints pins the curve to them, which a plain B-spline does not do.
|
|
66
|
+
const p = [points[0], points[0], ...points, points[points.length - 1], points[points.length - 1]];
|
|
67
|
+
let d = `M ${fmt(p[1].x)} ${fmt(p[1].y)}`;
|
|
68
|
+
for (let i = 1; i < p.length - 2; i++) {
|
|
69
|
+
const [a, b, c, e] = [p[i - 1], p[i], p[i + 1], p[i + 2]];
|
|
70
|
+
// Standard uniform cubic B-spline in Bezier form.
|
|
71
|
+
const c1 = { x: b.x + (c.x - a.x) / 6, y: b.y + (c.y - a.y) / 6 };
|
|
72
|
+
const c2 = { x: c.x - (e.x - b.x) / 6, y: c.y - (e.y - b.y) / 6 };
|
|
73
|
+
d += ` C ${fmt(c1.x)} ${fmt(c1.y)}, ${fmt(c2.x)} ${fmt(c2.y)}, ${fmt(c.x)} ${fmt(c.y)}`;
|
|
74
|
+
}
|
|
75
|
+
return d;
|
|
76
|
+
}
|
|
77
|
+
/** The bundled route between two leaves, relaxed by `beta` and drawn as a spline. */
|
|
78
|
+
export function bundlePath(points, beta) {
|
|
79
|
+
return splinePath(relax(points, beta));
|
|
80
|
+
}
|
package/dist/layout/cluster.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildCards } from './cards.js';
|
|
2
2
|
import { barycenterPasses, buildAdjacency, buildClusters, flow, groupByGroup, orderClusters, pack } from './clusters.js';
|
|
3
3
|
import { buildEdges } from './edges.js';
|
|
4
|
+
import { warnUnknownOptions } from './options.js';
|
|
4
5
|
import { nestedClusters } from './nested.js';
|
|
5
6
|
/**
|
|
6
7
|
* Deterministic cluster layout: groups become clusters, clusters are ordered to
|
|
@@ -12,6 +13,7 @@ import { nestedClusters } from './nested.js';
|
|
|
12
13
|
* area-descending order with alphabetical lists.
|
|
13
14
|
*/
|
|
14
15
|
export const cluster = (model, options) => {
|
|
16
|
+
warnUnknownOptions(options, 'cluster');
|
|
15
17
|
const density = options.density ?? 'keys';
|
|
16
18
|
const arrange = options.arrange ?? 'untangle';
|
|
17
19
|
const cards = buildCards(model.nodes, density, { expanded: options.expanded });
|
package/dist/layout/edges.js
CHANGED
|
@@ -26,6 +26,7 @@ function selfLoop(edge, i, card, { y1, y2 }) {
|
|
|
26
26
|
toKey: edge.target,
|
|
27
27
|
kind: edge.kind,
|
|
28
28
|
relation: edge.relation,
|
|
29
|
+
...carried(edge),
|
|
29
30
|
self: true,
|
|
30
31
|
x1: card.x + card.w,
|
|
31
32
|
y1,
|
|
@@ -35,6 +36,15 @@ function selfLoop(edge, i, card, { y1, y2 }) {
|
|
|
35
36
|
s2: 1
|
|
36
37
|
};
|
|
37
38
|
}
|
|
39
|
+
/** Overlay flag and weight, copied only when set so existing routed edges stay key-identical. */
|
|
40
|
+
function carried(edge) {
|
|
41
|
+
const out = {};
|
|
42
|
+
if (edge.overlay)
|
|
43
|
+
out.overlay = true;
|
|
44
|
+
if (edge.weight !== undefined)
|
|
45
|
+
out.weight = edge.weight;
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
38
48
|
/** Which side of each card the connector leaves from. 1 = right, -1 = left. */
|
|
39
49
|
function sides(a, b) {
|
|
40
50
|
if (a.x + a.w + 50 <= b.x)
|
|
@@ -61,6 +71,7 @@ function buildEdge(edge, i, cards) {
|
|
|
61
71
|
toKey: edge.target,
|
|
62
72
|
kind: edge.kind,
|
|
63
73
|
relation: edge.relation,
|
|
74
|
+
...carried(edge),
|
|
64
75
|
self: false,
|
|
65
76
|
x1: s1 === 1 ? a.x + a.w : a.x,
|
|
66
77
|
y1,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LayoutFn } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Columns by reference direction, ordered to reduce crossings, with fixed ports.
|
|
4
|
+
*
|
|
5
|
+
* No clusters: ranking and schema grouping compete for the same axis, and a box cannot be both
|
|
6
|
+
* "in the `public` box" and "in column 3". `cluster` remains the schema view.
|
|
7
|
+
*/
|
|
8
|
+
export declare const flow: LayoutFn;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* The `flow` layout: columns by reference direction, fixed ports, crossings reduced.
|
|
2
|
+
*
|
|
3
|
+
* Raised from the ER diagram — incoming should always enter on the LEFT and outgoing always
|
|
4
|
+
* leave from the RIGHT, and the boxes should be arranged so links are not buried behind
|
|
5
|
+
* entities. Those are ONE feature. `edges.ts` picks a connector's side by relative POSITION,
|
|
6
|
+
* so an edge's exit side tells a reader where the other box happens to sit rather than which
|
|
7
|
+
* way the reference points; and fixing the ports without ranking makes burial worse, because a
|
|
8
|
+
* right-exit connector reaching a left-hand table sweeps back across everything between.
|
|
9
|
+
*
|
|
10
|
+
* Ranking is what makes x-position mean direction, and only then are fixed ports honest.
|
|
11
|
+
*
|
|
12
|
+
* See docs/design/25-flow-layout.md — including what this deliberately does NOT do: a long
|
|
13
|
+
* edge is still drawn straight over any box between its endpoints, because routing it around
|
|
14
|
+
* needs dummy nodes on the intervening ranks and that is its own slice.
|
|
15
|
+
*/
|
|
16
|
+
import { buildCards } from './cards.js';
|
|
17
|
+
import { rank } from './rank.js';
|
|
18
|
+
import { order } from './order.js';
|
|
19
|
+
import { warnUnknownOptions } from './options.js';
|
|
20
|
+
import { CARD_W, GAP_Y, HEAD_H, ROW_H } from './constants.js';
|
|
21
|
+
/**
|
|
22
|
+
* Horizontal gap between columns.
|
|
23
|
+
*
|
|
24
|
+
* Wider than `cluster`'s `GAP_X` on purpose: this gap is where every connector between two
|
|
25
|
+
* columns is drawn, so it is working space rather than breathing room. Too tight and the
|
|
26
|
+
* curves of a fan-out overlap each other before they reach their targets.
|
|
27
|
+
*/
|
|
28
|
+
const COL_GAP = 120;
|
|
29
|
+
/** Vertical anchor for a row on a card, matching `edges.ts` so the two agree. */
|
|
30
|
+
function anchorY(card, rowName) {
|
|
31
|
+
const idx = card.vis.findIndex((r) => r.name === rowName);
|
|
32
|
+
if (idx >= 0)
|
|
33
|
+
return card.y + HEAD_H + idx * ROW_H + ROW_H / 2;
|
|
34
|
+
return card.y + HEAD_H / 2;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Place each column left to right, centring every column on a shared axis.
|
|
38
|
+
*
|
|
39
|
+
* Every id in `layers` has a card without checking: both come from `model.nodes`, and
|
|
40
|
+
* `buildCards` keys one entry per node. A guard here would be a branch no input can reach.
|
|
41
|
+
*/
|
|
42
|
+
function place(layers, cards) {
|
|
43
|
+
const heights = layers.map((layer) => layer.reduce((total, id) => total + cards[id].h + GAP_Y, -GAP_Y));
|
|
44
|
+
const tallest = Math.max(0, ...heights);
|
|
45
|
+
let x = 0;
|
|
46
|
+
layers.forEach((layer, i) => {
|
|
47
|
+
// Centred rather than top-aligned: a short column hanging off the top reads as a
|
|
48
|
+
// hierarchy that is not there, and centring keeps most connectors near-horizontal.
|
|
49
|
+
let y = (tallest - heights[i]) / 2;
|
|
50
|
+
for (const id of layer) {
|
|
51
|
+
const card = cards[id];
|
|
52
|
+
card.x = x;
|
|
53
|
+
card.y = y;
|
|
54
|
+
y += card.h + GAP_Y;
|
|
55
|
+
}
|
|
56
|
+
x += CARD_W + COL_GAP;
|
|
57
|
+
});
|
|
58
|
+
return { w: Math.max(0, x - COL_GAP), h: tallest };
|
|
59
|
+
}
|
|
60
|
+
/** The identity half of a routed edge — everything that is not geometry. */
|
|
61
|
+
function identity(edge, i) {
|
|
62
|
+
return {
|
|
63
|
+
i,
|
|
64
|
+
id: edge.id,
|
|
65
|
+
fromKey: edge.source,
|
|
66
|
+
toKey: edge.target,
|
|
67
|
+
kind: edge.kind,
|
|
68
|
+
relation: edge.relation
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A self-reference bows off the right edge — the same shape `edges.ts` gives it, including the
|
|
73
|
+
* nudge that stops a single-row loop collapsing to a point.
|
|
74
|
+
*/
|
|
75
|
+
function selfLoop(edge, i, at) {
|
|
76
|
+
return {
|
|
77
|
+
...identity(edge, i),
|
|
78
|
+
self: true,
|
|
79
|
+
x1: at.x,
|
|
80
|
+
y1: at.y1,
|
|
81
|
+
x2: at.x,
|
|
82
|
+
y2: at.y2 === at.y1 ? at.y1 + 14 : at.y2,
|
|
83
|
+
s1: 1,
|
|
84
|
+
s2: 1
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One connector, always leaving the right and entering the left.
|
|
89
|
+
*
|
|
90
|
+
* That is the whole point of the layout, so there is no side calculation here at all — unlike
|
|
91
|
+
* `edges.ts`, which derives sides from geometry. A back edge gets the same treatment and pays
|
|
92
|
+
* a visible sweep for it; that is the minority case ranking exists to isolate.
|
|
93
|
+
*/
|
|
94
|
+
function route(edge, i, cards, back) {
|
|
95
|
+
const a = cards[edge.source];
|
|
96
|
+
const b = cards[edge.target];
|
|
97
|
+
if (!a || !b)
|
|
98
|
+
return null;
|
|
99
|
+
const y1 = anchorY(a, edge.sourceRow);
|
|
100
|
+
const y2 = anchorY(b, edge.targetRow);
|
|
101
|
+
if (a === b)
|
|
102
|
+
return selfLoop(edge, i, { x: a.x + a.w, y1, y2 });
|
|
103
|
+
return {
|
|
104
|
+
...identity(edge, i),
|
|
105
|
+
back: back.has(edge.id) || undefined,
|
|
106
|
+
self: false,
|
|
107
|
+
x1: a.x + a.w,
|
|
108
|
+
y1,
|
|
109
|
+
x2: b.x,
|
|
110
|
+
y2,
|
|
111
|
+
s1: 1,
|
|
112
|
+
s2: -1
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function routeAll(model, cards, back) {
|
|
116
|
+
const routed = [];
|
|
117
|
+
model.edges.forEach((edge, i) => {
|
|
118
|
+
const e = route(edge, i, cards, back);
|
|
119
|
+
if (e)
|
|
120
|
+
routed.push(e);
|
|
121
|
+
});
|
|
122
|
+
return routed;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Columns by reference direction, ordered to reduce crossings, with fixed ports.
|
|
126
|
+
*
|
|
127
|
+
* No clusters: ranking and schema grouping compete for the same axis, and a box cannot be both
|
|
128
|
+
* "in the `public` box" and "in column 3". `cluster` remains the schema view.
|
|
129
|
+
*/
|
|
130
|
+
export const flow = (model, options) => {
|
|
131
|
+
warnUnknownOptions(options, 'flow');
|
|
132
|
+
const cards = buildCards(model.nodes, options.density ?? 'keys', {
|
|
133
|
+
expanded: options.expanded
|
|
134
|
+
});
|
|
135
|
+
if (model.nodes.length === 0) {
|
|
136
|
+
return { clusters: [], cards: {}, edges: [], size: { w: 0, h: 0 } };
|
|
137
|
+
}
|
|
138
|
+
const { ranks, back } = rank(model);
|
|
139
|
+
const size = place(order(model, ranks, back), cards);
|
|
140
|
+
return { clusters: [], cards, edges: routeAll(model, cards, back), size };
|
|
141
|
+
};
|