@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
|
@@ -1,67 +1,22 @@
|
|
|
1
1
|
import { SvelteSet } from 'svelte/reactivity';
|
|
2
2
|
import { normalizeGraph } from './model/normalize.js';
|
|
3
3
|
import { layouts } from './layout/index.js';
|
|
4
|
-
import { edgePath } from './layout/edges.js';
|
|
5
|
-
import { nodeShapeOf } from './layout/options.js';
|
|
4
|
+
import { buildEdges, edgePath } from './layout/edges.js';
|
|
5
|
+
import { appliesTo, nodeShapeOf } from './layout/options.js';
|
|
6
6
|
import { arcPath } from './layout/arc.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* One edge's contribution to the selected node's relationship list.
|
|
23
|
-
*
|
|
24
|
-
* An UNPLACED edge is reported too, with the raw name as the label — the far end is not a
|
|
25
|
-
* node, so there is nothing to look up. Hiding it would be worse: "this references something
|
|
26
|
-
* we have not indexed" is exactly what a reader of a partial graph needs to see.
|
|
27
|
-
*/
|
|
28
|
-
function describeEdge(edge, id, describe) {
|
|
29
|
-
const ends = endpointsFor(edge, id);
|
|
30
|
-
if (!ends)
|
|
31
|
-
return [];
|
|
32
|
-
if (ends.self)
|
|
33
|
-
return [describe(id, 'out', edge)];
|
|
34
|
-
const { other, direction } = ends;
|
|
35
|
-
// An unplaced far end is not a node, so there is nothing to look up: the raw name IS
|
|
36
|
-
// the label. Reported rather than hidden — "references something not indexed" is what a
|
|
37
|
-
// reader of a partial graph most needs to see.
|
|
38
|
-
return edge.unplaced
|
|
39
|
-
? [{ direction, id: other, label: other, edge }]
|
|
40
|
-
: [describe(other, direction, edge)];
|
|
41
|
-
}
|
|
42
|
-
/** Bottom-right extent of a set of boxes, or {0,0} when there are none. */
|
|
43
|
-
function extentOf(boxes) {
|
|
44
|
-
let w = 0;
|
|
45
|
-
let h = 0;
|
|
46
|
-
for (const box of boxes) {
|
|
47
|
-
w = Math.max(w, box.x + box.w);
|
|
48
|
-
h = Math.max(h, box.y + box.h);
|
|
49
|
-
}
|
|
50
|
-
return { w, h };
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The store. Turns raw `nodes`/`edges`/`fields` into the reactive shape the visuals
|
|
54
|
-
* render, and owns every transition.
|
|
55
|
-
*
|
|
56
|
-
* Components read from this and call its methods — they never compute. That is what
|
|
57
|
-
* lets the geometry, badge derivation and selection logic be covered exhaustively
|
|
58
|
-
* with no DOM, and lets the component specs assert only attributes.
|
|
59
|
-
*
|
|
60
|
-
* `update()` is re-callable and FULLY re-applies config rather than merging deltas,
|
|
61
|
-
* matching `SparkState.update` — so a prop reverting to undefined actually reverts.
|
|
62
|
-
* Deliberately NOT `PlotState.update`, which guards each field with
|
|
63
|
-
* `if (config.X !== undefined)` and therefore merges.
|
|
64
|
-
*/
|
|
7
|
+
import { resolveGroupStyles, resolveShade } from './preset.js';
|
|
8
|
+
import { entityRows } from './model/entities.js';
|
|
9
|
+
import { contentExtent } from './layout/extent.js';
|
|
10
|
+
import { GraphConfig } from './state/GraphConfig.svelte.js';
|
|
11
|
+
import { GraphSelection } from './state/GraphSelection.svelte.js';
|
|
12
|
+
import { GraphDrill } from './state/GraphDrill.svelte.js';
|
|
13
|
+
import { GraphGroups } from './state/GraphGroups.svelte.js';
|
|
14
|
+
import { condense } from './model/condense.js';
|
|
15
|
+
import { channelRows, diagramLabel, drillErrorText, moreRowsLabel, sideNames } from './state/text.js';
|
|
16
|
+
import { INTENTS, isIntent } from './state/intents.js';
|
|
17
|
+
import { say } from './messages.js';
|
|
18
|
+
/** A presence attribute: `''` when on, absent when off. */
|
|
19
|
+
const flag = (on) => (on ? '' : undefined);
|
|
65
20
|
/**
|
|
66
21
|
* Sorted unique strings, dropping empties.
|
|
67
22
|
*
|
|
@@ -69,126 +24,90 @@ function extentOf(boxes) {
|
|
|
69
24
|
* sorted anyway, so the Set would be an extra structure for nothing — and a plain Set inside a
|
|
70
25
|
* reactive class is exactly what `svelte/prefer-svelte-reactivity` is right to flag.
|
|
71
26
|
*/
|
|
72
|
-
/**
|
|
73
|
-
* A level cap, or nothing.
|
|
74
|
-
*
|
|
75
|
-
* Left UNDEFINED when the caller said nothing: a state-level default of 2 is indistinguishable
|
|
76
|
-
* from an explicit 2, and `radial` needs "no cap" to mean the whole tree — inventing a number
|
|
77
|
-
* here silently pruned a codebase dendrogram to two rings.
|
|
78
|
-
*/
|
|
79
|
-
function floorLevels(levels) {
|
|
80
|
-
return levels === undefined ? undefined : Math.max(1, Math.floor(levels));
|
|
81
|
-
}
|
|
82
27
|
function unique(values) {
|
|
83
28
|
return values
|
|
84
29
|
.filter((v) => Boolean(v))
|
|
85
30
|
.sort()
|
|
86
31
|
.filter((v, i, all) => i === 0 || v !== all[i - 1]);
|
|
87
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The store. Turns raw `nodes`/`edges`/`fields` into the reactive shape the visuals
|
|
35
|
+
* render, and owns every transition.
|
|
36
|
+
*
|
|
37
|
+
* Components read from this and call its methods — they never compute. That is what
|
|
38
|
+
* lets the geometry, badge derivation and selection logic be covered exhaustively
|
|
39
|
+
* with no DOM, and lets the component specs assert only attributes.
|
|
40
|
+
*
|
|
41
|
+
* A composition: `config` (every input, `state/GraphConfig`) feeds the layout pipeline
|
|
42
|
+
* derived here; the selection and the view queries read both. `update()` FULLY re-applies
|
|
43
|
+
* config rather than merging deltas — so a prop reverting to undefined actually reverts;
|
|
44
|
+
* `apply()` merges.
|
|
45
|
+
*/
|
|
88
46
|
export class GraphState {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#mode = $state('light');
|
|
114
|
-
#label = $state(undefined);
|
|
115
|
-
#onselect = $state(undefined);
|
|
116
|
-
#model = $derived(normalizeGraph(this.#nodes, this.#edges, this.#fields));
|
|
117
|
-
#layoutFn = $derived(typeof this.#layout === 'function' ? this.#layout : (layouts[this.#layout] ?? layouts.cluster));
|
|
47
|
+
/** Every input — see `CONFIG_FIELDS` for what each omitted key falls back to. */
|
|
48
|
+
config = new GraphConfig();
|
|
49
|
+
/** Which subtree is the canvas, and the host events for moving it (#165). */
|
|
50
|
+
drill = new GraphDrill({
|
|
51
|
+
config: this.config,
|
|
52
|
+
model: () => this.#model,
|
|
53
|
+
layoutName: () => this.layoutName
|
|
54
|
+
});
|
|
55
|
+
/** What the reader picked and opened. */
|
|
56
|
+
selection = new GraphSelection({
|
|
57
|
+
model: () => this.#model,
|
|
58
|
+
routedEdges: () => this.#result.edges,
|
|
59
|
+
onselect: () => this.config.onselect
|
|
60
|
+
});
|
|
61
|
+
/** The model as normalised — every group AND every member. */
|
|
62
|
+
#canonical = $derived(normalizeGraph(this.config.nodes, this.config.edges, this.config.fields));
|
|
63
|
+
/** Which groups are collapsed (#166). */
|
|
64
|
+
groups = new GraphGroups({
|
|
65
|
+
model: () => this.#canonical,
|
|
66
|
+
config: this.config
|
|
67
|
+
});
|
|
68
|
+
/** What is drawn: the canonical model with each collapsed group standing in for its members. */
|
|
69
|
+
#model = $derived(condense(this.#canonical, this.groups.collapsed));
|
|
70
|
+
#layoutFn = $derived(typeof this.config.layout === 'function' ? this.config.layout : (layouts[this.config.layout] ?? layouts.cluster));
|
|
118
71
|
#result = $derived(this.#layoutFn(this.#model, {
|
|
119
|
-
density: this
|
|
120
|
-
arrange: this
|
|
121
|
-
groupBy: this
|
|
122
|
-
nestBy: this
|
|
123
|
-
sizeBy: this
|
|
124
|
-
sizeScale: this
|
|
125
|
-
depth: this
|
|
126
|
-
focusPath: this
|
|
127
|
-
levels: this
|
|
128
|
-
radialMode: this
|
|
129
|
-
root: this
|
|
130
|
-
bundleTension: this
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
72
|
+
density: this.config.density,
|
|
73
|
+
arrange: this.config.arrange,
|
|
74
|
+
groupBy: this.config.groupBy,
|
|
75
|
+
nestBy: this.config.nestBy ?? undefined,
|
|
76
|
+
sizeBy: this.config.sizeBy,
|
|
77
|
+
sizeScale: this.config.sizeScale,
|
|
78
|
+
depth: this.config.depth,
|
|
79
|
+
focusPath: this.config.focusPath,
|
|
80
|
+
levels: this.config.levels,
|
|
81
|
+
radialMode: this.config.radialMode,
|
|
82
|
+
root: this.config.root,
|
|
83
|
+
bundleTension: this.config.bundleTension,
|
|
84
|
+
showEdges: this.config.showEdges,
|
|
85
|
+
layerLabels: this.config.layerLabels,
|
|
86
|
+
widthBy: this.config.widthBy,
|
|
87
|
+
heightBy: this.config.heightBy,
|
|
88
|
+
colorBy: this.config.colorBy,
|
|
89
|
+
above: this.config.above,
|
|
90
|
+
shadeBy: this.config.shadeBy,
|
|
91
|
+
edgeStyle: this.config.edgeStyle,
|
|
92
|
+
focus: this.config.focus ?? this.selection.value,
|
|
93
|
+
expanded: this.selection.expanded
|
|
134
94
|
}));
|
|
95
|
+
/**
|
|
96
|
+
* Overlay edges routed over the finished layout. The layout never saw them — that is the
|
|
97
|
+
* point — so they are routed here, against whatever cards it placed. A layout with no cards
|
|
98
|
+
* (the treemap, the sunburst) simply has nothing to route them between.
|
|
99
|
+
*/
|
|
100
|
+
#overlayRouted = $derived(buildEdges(this.#model.overlays, this.#result.cards));
|
|
101
|
+
#routed = $derived(this.#overlayRouted.length > 0
|
|
102
|
+
? [...this.#result.edges, ...this.#overlayRouted]
|
|
103
|
+
: this.#result.edges);
|
|
104
|
+
/** Heaviest weight across every edge that carries one — the 1 that `edgeWeight` scales to. */
|
|
105
|
+
#maxEdgeWeight = $derived(Math.max(0, ...[...this.#model.edges, ...this.#model.overlays].map((e) => e.weight ?? 0)));
|
|
135
106
|
// Duplicates are left in on purpose: `resolveGroupStyles` de-duplicates and sorts, because
|
|
136
107
|
// that is where ramp assignment lives. A Set here would be a second, redundant de-dup.
|
|
137
108
|
#groups = $derived(this.#model.nodes.map((n) => n.group).filter((g) => Boolean(g)));
|
|
138
|
-
#groupStyles = $derived(resolveGroupStyles(this.#groups, this
|
|
139
|
-
#
|
|
140
|
-
? new SvelteSet(this.#model.neighbors.get(this.#value) ?? [])
|
|
141
|
-
: new SvelteSet());
|
|
142
|
-
#entities = $derived(this.#model.nodes.map((node) => ({
|
|
143
|
-
id: node.id,
|
|
144
|
-
label: node.label,
|
|
145
|
-
group: node.group,
|
|
146
|
-
kind: node.kind,
|
|
147
|
-
rowCount: node.rows.length,
|
|
148
|
-
refCount: this.#model.edges.filter((e) => e.source === node.id || e.target === node.id)
|
|
149
|
-
.length,
|
|
150
|
-
note: node.note
|
|
151
|
-
})));
|
|
152
|
-
#entity = $derived(this.#value ? (this.#model.byId.get(this.#value) ?? null) : null);
|
|
153
|
-
/**
|
|
154
|
-
* Derived from `#model.edges` — the canonical, unfiltered model — NOT from `#result.edges`.
|
|
155
|
-
*
|
|
156
|
-
* A layout filters: `cluster` drops edges whose endpoints were not laid out, and
|
|
157
|
-
* `neighborhood` lays out only the focus node's 1-hop neighbourhood. Since `focus` and
|
|
158
|
-
* `value` are independent config fields, reading the layout's edges lets the state
|
|
159
|
-
* contradict itself: with `focus: 'audit.log'` (no neighbours) and `value: 'public.orders'`,
|
|
160
|
-
* `relationships` would be `[]` while `entities`' `refCount` for the same node is 1.
|
|
161
|
-
*
|
|
162
|
-
* Routed geometry is attached opportunistically — it exists only for edges the active layout
|
|
163
|
-
* actually placed, so `routed` is optional on `Relationship`. A relationship is a fact about
|
|
164
|
-
* the model; its geometry is a fact about the current view.
|
|
165
|
-
*/
|
|
166
|
-
#relationships = $derived.by(() => {
|
|
167
|
-
const id = this.#value;
|
|
168
|
-
if (!id)
|
|
169
|
-
return [];
|
|
170
|
-
// A plain Map on purpose: a call-local lookup built once from an already-derived array
|
|
171
|
-
// and discarded when this derivation re-runs. Nothing outside can observe it, so
|
|
172
|
-
// SvelteMap would add proxying for no reactivity.
|
|
173
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
174
|
-
const routed = new Map(this.#result.edges.map((e) => [e.id, e]));
|
|
175
|
-
const describe = (other, direction, edge) => {
|
|
176
|
-
// normalizeGraph drops any edge whose endpoints do not resolve, so both ends of
|
|
177
|
-
// every edge in #model.edges are in byId. No `?? other` fallback here: an
|
|
178
|
-
// unreachable branch cannot be tested, and the package's 100% statement bar
|
|
179
|
-
// exists to keep that honest.
|
|
180
|
-
const node = this.#model.byId.get(other);
|
|
181
|
-
return {
|
|
182
|
-
direction,
|
|
183
|
-
id: other,
|
|
184
|
-
label: node.label,
|
|
185
|
-
group: node.group,
|
|
186
|
-
edge,
|
|
187
|
-
routed: routed.get(edge.id)
|
|
188
|
-
};
|
|
189
|
-
};
|
|
190
|
-
return this.#model.edges.flatMap((edge) => describeEdge(edge, id, describe));
|
|
191
|
-
});
|
|
109
|
+
#groupStyles = $derived(resolveGroupStyles(this.#groups, this.config.mode, this.config.preset));
|
|
110
|
+
#entities = $derived(entityRows(this.#model));
|
|
192
111
|
constructor(config = {}) {
|
|
193
112
|
this.update(config);
|
|
194
113
|
}
|
|
@@ -200,13 +119,16 @@ export class GraphState {
|
|
|
200
119
|
* field by field.
|
|
201
120
|
*/
|
|
202
121
|
update(config = {}) {
|
|
203
|
-
this
|
|
204
|
-
this.#
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
122
|
+
this.config.update(config);
|
|
123
|
+
this.#adoptValue(config);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `value` is input AND output, so it is only adopted when the caller supplies one —
|
|
127
|
+
* otherwise a re-render would wipe a selection the user just made.
|
|
128
|
+
*/
|
|
129
|
+
#adoptValue(config) {
|
|
208
130
|
if (config.value !== undefined)
|
|
209
|
-
this
|
|
131
|
+
this.selection.adopt(config.value);
|
|
210
132
|
}
|
|
211
133
|
/**
|
|
212
134
|
* Merge a partial config, leaving every key it does not name alone.
|
|
@@ -217,85 +139,102 @@ export class GraphState {
|
|
|
217
139
|
* owner's data on every keystroke. This merges over the last full config instead.
|
|
218
140
|
*/
|
|
219
141
|
apply(config) {
|
|
220
|
-
this
|
|
221
|
-
}
|
|
222
|
-
#applyData(config) {
|
|
223
|
-
this.#nodes = config.nodes ?? [];
|
|
224
|
-
this.#edges = config.edges ?? [];
|
|
225
|
-
this.#fields = config.fields ?? {};
|
|
226
|
-
this.#layout = config.layout ?? 'flow';
|
|
227
|
-
this.#focus = config.focus ?? null;
|
|
228
|
-
}
|
|
229
|
-
/** How the boxes are organised — one axis, or one subdivided by a second. */
|
|
230
|
-
#applyGrouping(config) {
|
|
231
|
-
const outer = config.groupBy ?? 'group';
|
|
232
|
-
this.#groupBy = outer;
|
|
233
|
-
// Rejected rather than rendered: a box subdivided by its OWN axis yields exactly one
|
|
234
|
-
// child containing everything, which reads as a rendering fault, not a no-op.
|
|
235
|
-
this.#nestBy = config.nestBy === outer ? null : (config.nestBy ?? null);
|
|
236
|
-
this.#groupTint = config.groupTint ?? false;
|
|
237
|
-
}
|
|
238
|
-
/** What a node's size encodes, and how far a neighbourhood reaches. */
|
|
239
|
-
#applyMeasure(config) {
|
|
240
|
-
this.#sizeBy = config.sizeBy ?? 'degree';
|
|
241
|
-
this.#sizeScale = config.sizeScale ?? 'linear';
|
|
242
|
-
// Floored at 1: a depth of 0 or -1 is a request for nothing, and returning an empty
|
|
243
|
-
// canvas for it looks identical to a broken focus.
|
|
244
|
-
this.#depth = Math.max(1, Math.floor(config.depth ?? 1));
|
|
245
|
-
this.#focusPath = config.focusPath ?? [];
|
|
246
|
-
this.#levels = floorLevels(config.levels);
|
|
247
|
-
this.#root = config.root ?? null;
|
|
248
|
-
this.#bundleTension = config.bundleTension;
|
|
249
|
-
this.#radialMode = config.radialMode ?? 'tree';
|
|
250
|
-
}
|
|
251
|
-
#applyView(config) {
|
|
252
|
-
this.#density = config.density ?? 'keys';
|
|
253
|
-
this.#arrange = config.arrange ?? 'untangle';
|
|
254
|
-
this.#applyGrouping(config);
|
|
255
|
-
this.#applyMeasure(config);
|
|
256
|
-
this.#edgeStyle = config.edgeStyle ?? 'curved';
|
|
257
|
-
this.#preset = config.preset ?? defaultGraphPreset;
|
|
258
|
-
this.#mode = config.mode ?? 'light';
|
|
259
|
-
this.#label = config.label;
|
|
260
|
-
this.#onselect = config.onselect;
|
|
142
|
+
this.#adoptValue(this.config.apply(config));
|
|
261
143
|
}
|
|
262
144
|
// ─── transitions ───────────────────────────────────────────────────────────
|
|
263
145
|
select(id) {
|
|
264
|
-
this
|
|
265
|
-
|
|
146
|
+
this.selection.select(id);
|
|
147
|
+
}
|
|
148
|
+
/** Drop the selection and report `null` — see `GraphSelection.clear`. */
|
|
149
|
+
clear() {
|
|
150
|
+
this.selection.clear();
|
|
151
|
+
}
|
|
152
|
+
/** Show one node's rows in full, whatever the density says — see `GraphSelection`. */
|
|
153
|
+
toggleExpanded(id) {
|
|
154
|
+
this.selection.toggleExpanded(id);
|
|
155
|
+
}
|
|
156
|
+
isExpanded(id) {
|
|
157
|
+
return this.selection.isExpanded(id);
|
|
158
|
+
}
|
|
159
|
+
// ─── groups (#166) — see GraphGroups ────────────────────────────────────────
|
|
160
|
+
isGroup(id) {
|
|
161
|
+
return this.groups.isGroup(id);
|
|
162
|
+
}
|
|
163
|
+
isCollapsed(id) {
|
|
164
|
+
return this.groups.isCollapsed(id);
|
|
165
|
+
}
|
|
166
|
+
memberCount(id) {
|
|
167
|
+
return this.groups.memberCount(id);
|
|
168
|
+
}
|
|
169
|
+
groupOf(id) {
|
|
170
|
+
return this.groups.groupOf(id);
|
|
266
171
|
}
|
|
267
172
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* to show the entity panel, so a silent clear left that panel open over nothing. A
|
|
272
|
-
* controlled consumer cannot observe an internal `#value` — the callback is the only
|
|
273
|
-
* channel, which is why it carries `null` rather than being skipped.
|
|
274
|
-
*
|
|
275
|
-
* Guarded on an actual change, or a background click on an already-empty canvas
|
|
276
|
-
* round-trips through the consumer's setter on every stray click.
|
|
173
|
+
* Expand or collapse a group. Collapsing one whose member is selected moves the selection to
|
|
174
|
+
* the group: the thing being looked at is now inside it, and a selection naming a hidden node
|
|
175
|
+
* would leave the entity panel empty for no visible reason.
|
|
277
176
|
*/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this
|
|
177
|
+
toggleGroup(id) {
|
|
178
|
+
const collapsing = this.groups.isGroup(id) && !this.groups.isCollapsed(id);
|
|
179
|
+
const selected = this.value;
|
|
180
|
+
const holdsSelection = collapsing && selected !== null && this.groups.groupOf(selected) === id;
|
|
181
|
+
if (!this.groups.toggle(id))
|
|
182
|
+
return false;
|
|
183
|
+
if (holdsSelection)
|
|
184
|
+
this.select(id);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/** The selection's group action — what a drill bar offers. */
|
|
188
|
+
get groupAction() {
|
|
189
|
+
return this.groups.actionFor(this.value);
|
|
190
|
+
}
|
|
191
|
+
// ─── drilling (#165) — see GraphDrill ───────────────────────────────────────
|
|
192
|
+
get drillPath() {
|
|
193
|
+
return this.drill.path;
|
|
194
|
+
}
|
|
195
|
+
get breadcrumbs() {
|
|
196
|
+
return this.drill.breadcrumbs;
|
|
197
|
+
}
|
|
198
|
+
/** A drill's handler returned a promise that has not settled. */
|
|
199
|
+
get pending() {
|
|
200
|
+
return this.drill.pending;
|
|
201
|
+
}
|
|
202
|
+
get drillError() {
|
|
203
|
+
return this.drill.error;
|
|
204
|
+
}
|
|
205
|
+
canDrill(box) {
|
|
206
|
+
return this.drill.canDrill(box);
|
|
207
|
+
}
|
|
208
|
+
drillInto(box) {
|
|
209
|
+
return this.drill.drillInto(box);
|
|
210
|
+
}
|
|
211
|
+
drillOut(levels = 1) {
|
|
212
|
+
return this.drill.drillOut(levels);
|
|
213
|
+
}
|
|
214
|
+
drillTo(path) {
|
|
215
|
+
return this.drill.drillTo(path);
|
|
283
216
|
}
|
|
284
217
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* Per NODE rather than a global density change: a reader who clicks "+3 more" on one
|
|
288
|
-
* card is asking about that card, and expanding all of them answers a question they did
|
|
289
|
-
* not ask — on a large diagram it also relayouts everything under them.
|
|
218
|
+
* The selected box, when it can be opened — what a drill bar's "Open" acts on. The keyboard
|
|
219
|
+
* route into a leaf: Enter selects it, and this is the next step.
|
|
290
220
|
*/
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
else
|
|
295
|
-
this.#expanded.add(id);
|
|
221
|
+
/** The failed drill, worded for the reader; null when the last drill did not fail. */
|
|
222
|
+
get drillErrorText() {
|
|
223
|
+
return drillErrorText(this.drillError);
|
|
296
224
|
}
|
|
297
|
-
|
|
298
|
-
|
|
225
|
+
get drillTarget() {
|
|
226
|
+
const value = this.value;
|
|
227
|
+
if (!value)
|
|
228
|
+
return null;
|
|
229
|
+
return this.clusters.find((c) => c.nodeId === value && this.canDrill(c)) ?? null;
|
|
230
|
+
}
|
|
231
|
+
/** Whether a drill bar has anything to show: a trail, a load, a failure, or a box to open. */
|
|
232
|
+
get showsDrillBar() {
|
|
233
|
+
return (this.drillPath.length > 0 ||
|
|
234
|
+
this.pending ||
|
|
235
|
+
this.drillError !== null ||
|
|
236
|
+
this.drillTarget !== null ||
|
|
237
|
+
this.groupAction !== null);
|
|
299
238
|
}
|
|
300
239
|
/**
|
|
301
240
|
* The more-row's text, or null when the card is hiding nothing and needs no control.
|
|
@@ -310,14 +249,7 @@ export class GraphState {
|
|
|
310
249
|
* answer to what was asked, not a filter that matched nothing.
|
|
311
250
|
*/
|
|
312
251
|
moreLabel(id) {
|
|
313
|
-
|
|
314
|
-
return 'show less';
|
|
315
|
-
const card = this.cards[id];
|
|
316
|
-
if (!card || card.more <= 0)
|
|
317
|
-
return null;
|
|
318
|
-
if (this.#density !== 'keys' || card.vis.length > 0)
|
|
319
|
-
return `+ ${card.more} more`;
|
|
320
|
-
return `no keys · ${card.more} ${card.more === 1 ? 'row' : 'rows'}`;
|
|
252
|
+
return moreRowsLabel(this.selection.isExpanded(id), this.cards[id], this.config.density);
|
|
321
253
|
}
|
|
322
254
|
/**
|
|
323
255
|
* Change the detail level from inside the component.
|
|
@@ -327,25 +259,40 @@ export class GraphState {
|
|
|
327
259
|
* prop would render, click, and change nothing.
|
|
328
260
|
*/
|
|
329
261
|
setDensity(density) {
|
|
330
|
-
this
|
|
262
|
+
this.config.setDensity(density);
|
|
263
|
+
}
|
|
264
|
+
/** Every edge or only the climbing ones — what `ViolationsControl` drives (#167). */
|
|
265
|
+
setShowEdges(value) {
|
|
266
|
+
this.config.setShowEdges(value);
|
|
267
|
+
}
|
|
268
|
+
get showEdges() {
|
|
269
|
+
return this.config.showEdges;
|
|
331
270
|
}
|
|
332
271
|
// ─── per-item lookups the templates need ───────────────────────────────────
|
|
272
|
+
/**
|
|
273
|
+
* In `neighborhood`, the selection IS the focus (it defaults to it), and the canvas is built
|
|
274
|
+
* from adjacency to the focus — so every card is "related" and every edge "highlight" by
|
|
275
|
+
* construction, and ring 2 is "dim" for being the ring the reader asked for (#172). Those
|
|
276
|
+
* states would say nothing, so they are withheld; a selected NEIGHBOUR still lights its own.
|
|
277
|
+
*/
|
|
278
|
+
get #selectingFocus() {
|
|
279
|
+
const value = this.value;
|
|
280
|
+
return this.layoutName === 'neighborhood' && value !== null && (this.config.focus ?? value) === value;
|
|
281
|
+
}
|
|
333
282
|
nodeState(id) {
|
|
334
|
-
if (
|
|
335
|
-
return null;
|
|
336
|
-
|
|
337
|
-
return 'selected';
|
|
338
|
-
return this.#related.has(id) ? 'related' : 'dim';
|
|
283
|
+
if (this.#selectingFocus)
|
|
284
|
+
return id === this.value ? 'selected' : null;
|
|
285
|
+
return this.selection.nodeState(id);
|
|
339
286
|
}
|
|
340
287
|
edgeState(edge) {
|
|
341
|
-
if (
|
|
288
|
+
if (this.#selectingFocus)
|
|
342
289
|
return null;
|
|
343
|
-
return
|
|
290
|
+
return this.selection.edgeState(edge);
|
|
344
291
|
}
|
|
345
292
|
edgePath(edge) {
|
|
346
293
|
// A layout that knows more than the endpoints builds its own — a bundled edge follows
|
|
347
294
|
// the whole chain of ancestors between them, which `edgePath` never sees.
|
|
348
|
-
return edge.path ?? edgePath(edge, this
|
|
295
|
+
return edge.path ?? edgePath(edge, this.config.edgeStyle);
|
|
349
296
|
}
|
|
350
297
|
groupStyle(group) {
|
|
351
298
|
return (group && this.#groupStyles.get(group)) || {};
|
|
@@ -357,6 +304,35 @@ export class GraphState {
|
|
|
357
304
|
* Doing that inline put the same map/join in two places in `Graph.svelte` — a derivation
|
|
358
305
|
* living in a component, which is the one thing this layer exists to prevent.
|
|
359
306
|
*/
|
|
307
|
+
/** Whether shading is on: a measure is named, and the active layout reads it (#164). */
|
|
308
|
+
get #shading() {
|
|
309
|
+
return Boolean(this.config.shadeBy) && appliesTo(this.layoutName, 'shadeBy');
|
|
310
|
+
}
|
|
311
|
+
#shaded(cluster) {
|
|
312
|
+
return this.#shading && cluster.shade !== undefined;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* A box's paint as a style attribute: the shade ramp's step for its share when shading
|
|
316
|
+
* (#164), else its group's colour. Custom properties, so a theme still has the last word.
|
|
317
|
+
*/
|
|
318
|
+
boxStyleAttr(cluster) {
|
|
319
|
+
if (!this.#shaded(cluster))
|
|
320
|
+
return this.groupStyleAttr(cluster.ramp ?? cluster.name);
|
|
321
|
+
return Object.entries(resolveShade(cluster.shade, this.config.mode, this.config.preset))
|
|
322
|
+
.map(([property, value]) => `${property}:${value}`)
|
|
323
|
+
.join(';');
|
|
324
|
+
}
|
|
325
|
+
/** What a legend says about the shade: the measure and the ramp's two ends, or null. */
|
|
326
|
+
get shadeLegend() {
|
|
327
|
+
if (!this.#shading)
|
|
328
|
+
return null;
|
|
329
|
+
const { mode, preset } = this.config;
|
|
330
|
+
return {
|
|
331
|
+
measure: this.config.shadeBy,
|
|
332
|
+
low: resolveShade(0, mode, preset)['--group-fill'],
|
|
333
|
+
high: resolveShade(1, mode, preset)['--group-fill']
|
|
334
|
+
};
|
|
335
|
+
}
|
|
360
336
|
groupStyleAttr(group) {
|
|
361
337
|
return Object.entries(this.groupStyle(group))
|
|
362
338
|
.map(([property, value]) => `${property}:${value}`)
|
|
@@ -385,8 +361,26 @@ export class GraphState {
|
|
|
385
361
|
get cards() {
|
|
386
362
|
return this.#result.cards;
|
|
387
363
|
}
|
|
364
|
+
/** Structural edges as the layout routed them, then any overlay edges on top. */
|
|
388
365
|
get routedEdges() {
|
|
389
|
-
return this.#
|
|
366
|
+
return this.#routed;
|
|
367
|
+
}
|
|
368
|
+
/** Every overlay edge in the model, placed or not. */
|
|
369
|
+
get overlayEdges() {
|
|
370
|
+
return this.#model.overlays;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* An edge's weight as 0..1 of the heaviest weighted edge, for stroke width — or undefined
|
|
374
|
+
* for an edge with no weight, which draws at the theme's normal width. A layout's own
|
|
375
|
+
* per-set `strength` wins.
|
|
376
|
+
*/
|
|
377
|
+
edgeWeight(edge) {
|
|
378
|
+
// A layout that scales each set to its own heaviest (`arcs`) has already said.
|
|
379
|
+
if (edge.strength !== undefined)
|
|
380
|
+
return edge.strength;
|
|
381
|
+
if (edge.weight === undefined || this.#maxEdgeWeight <= 0)
|
|
382
|
+
return undefined;
|
|
383
|
+
return edge.weight / this.#maxEdgeWeight;
|
|
390
384
|
}
|
|
391
385
|
get size() {
|
|
392
386
|
return this.#result.size;
|
|
@@ -402,32 +396,19 @@ export class GraphState {
|
|
|
402
396
|
* stay in the component. Keeping this here is what lets it be tested without a renderer.
|
|
403
397
|
*/
|
|
404
398
|
get contentSize() {
|
|
405
|
-
|
|
406
|
-
x: c.x,
|
|
407
|
-
y: c.y,
|
|
408
|
-
w: c.w ?? 0,
|
|
409
|
-
h: c.h ?? 0
|
|
410
|
-
}));
|
|
411
|
-
let { w, h } = extentOf(clusters);
|
|
412
|
-
// An ungrouped layout (neighborhood) reports no clusters, so fall back to the cards.
|
|
413
|
-
if (!w || !h) {
|
|
414
|
-
const cards = extentOf(Object.values(this.#result.cards));
|
|
415
|
-
w = Math.max(w, cards.w);
|
|
416
|
-
h = Math.max(h, cards.h);
|
|
417
|
-
}
|
|
418
|
-
return { w: w || this.#result.size.w, h: h || this.#result.size.h };
|
|
399
|
+
return contentExtent(this.#result.clusters, this.#result.cards, this.#result.size);
|
|
419
400
|
}
|
|
420
401
|
get related() {
|
|
421
|
-
return this
|
|
402
|
+
return this.selection.related;
|
|
422
403
|
}
|
|
423
404
|
get entities() {
|
|
424
405
|
return this.#entities;
|
|
425
406
|
}
|
|
426
407
|
get entity() {
|
|
427
|
-
return this
|
|
408
|
+
return this.selection.entity;
|
|
428
409
|
}
|
|
429
410
|
get relationships() {
|
|
430
|
-
return this
|
|
411
|
+
return this.selection.relationships;
|
|
431
412
|
}
|
|
432
413
|
/**
|
|
433
414
|
* Edges the field map could not place at one or both ends.
|
|
@@ -441,19 +422,11 @@ export class GraphState {
|
|
|
441
422
|
return this.#model.edges.filter((e) => e.unplaced !== undefined);
|
|
442
423
|
}
|
|
443
424
|
get density() {
|
|
444
|
-
return this
|
|
425
|
+
return this.config.density;
|
|
445
426
|
}
|
|
446
427
|
get arrange() {
|
|
447
|
-
return this
|
|
428
|
+
return this.config.arrange;
|
|
448
429
|
}
|
|
449
|
-
/**
|
|
450
|
-
* A cluster's unique render key.
|
|
451
|
-
*
|
|
452
|
-
* NOT the name: nesting puts a `table` box under `public` AND under `billing`, and keying
|
|
453
|
-
* an `{#each}` by name alone is a duplicate key — Svelte throws `each_key_duplicate`, the
|
|
454
|
-
* render aborts, and no inner box appears at all, which reads as nesting silently not
|
|
455
|
-
* working rather than as an error.
|
|
456
|
-
*/
|
|
457
430
|
/**
|
|
458
431
|
* The SVG path for a wedge-shaped box, centred on the canvas.
|
|
459
432
|
*
|
|
@@ -473,18 +446,148 @@ export class GraphState {
|
|
|
473
446
|
*/
|
|
474
447
|
boxAttrs(cluster) {
|
|
475
448
|
return {
|
|
449
|
+
...this.#boxIntent(cluster),
|
|
450
|
+
// #164: painted from the shade ramp rather than its group's colour.
|
|
451
|
+
'data-graph-shaded': flag(this.#shaded(cluster)),
|
|
476
452
|
'data-cluster-depth': String(cluster.depth ?? 0),
|
|
453
|
+
// polymetric (#168): the channels a box could not show — missing data, or past the cap.
|
|
454
|
+
'data-graph-missing': cluster.missing?.join(' '),
|
|
455
|
+
'data-graph-clamped': cluster.clamped?.join(' '),
|
|
477
456
|
'data-node-group': cluster.name,
|
|
478
457
|
'data-graph-node-id': cluster.nodeId,
|
|
479
458
|
'data-node-kind': cluster.kind,
|
|
480
459
|
'data-node-state': cluster.nodeId ? this.nodeState(cluster.nodeId) : undefined
|
|
481
460
|
};
|
|
482
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* What a press and a double-click on a box mean (see `state/intents`): a leaf selects, and
|
|
464
|
+
* opens into its level; a container that can be drilled opens on press; a region does
|
|
465
|
+
* neither. The label says so for assistive tech.
|
|
466
|
+
*/
|
|
467
|
+
#boxIntent(cluster) {
|
|
468
|
+
if (cluster.nodeId) {
|
|
469
|
+
const key = cluster.nodeId;
|
|
470
|
+
return { 'data-graph-press': 'select', 'data-graph-open': 'drill', 'data-graph-key': key, 'aria-label': this.caption(cluster) };
|
|
471
|
+
}
|
|
472
|
+
if (!this.canDrill(cluster))
|
|
473
|
+
return {};
|
|
474
|
+
return {
|
|
475
|
+
'data-graph-drillable': '',
|
|
476
|
+
'data-graph-press': 'drill',
|
|
477
|
+
'data-graph-key': this.clusterKey(cluster),
|
|
478
|
+
'aria-label': say('open', { name: this.caption(cluster) })
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/** Whether a box takes the pointer and the keyboard: it has an intent. */
|
|
482
|
+
interactive(cluster) {
|
|
483
|
+
return Boolean(cluster.nodeId) || this.canDrill(cluster);
|
|
484
|
+
}
|
|
485
|
+
/** A box's visible caption: its name, and the layout's caption or its count. */
|
|
486
|
+
caption(cluster) {
|
|
487
|
+
return `${cluster.name} · ${cluster.caption ?? cluster.count}`;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* A node card's hooks and intents: it selects on press, and folds or opens its group on open
|
|
491
|
+
* (#166). A card with nothing below its head — no rows shown, none to reveal, not a group —
|
|
492
|
+
* is marked head-only so the theme can drop the empty body.
|
|
493
|
+
*/
|
|
494
|
+
cardAttrs(key) {
|
|
495
|
+
return {
|
|
496
|
+
...this.#cardNodeAttrs(key),
|
|
497
|
+
...this.#cardGroupAttrs(key),
|
|
498
|
+
'data-graph-press': 'select',
|
|
499
|
+
'data-graph-open': 'group',
|
|
500
|
+
'data-graph-key': key
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
#cardNodeAttrs(key) {
|
|
504
|
+
const card = this.cards[key];
|
|
505
|
+
const headOnly = card !== undefined && card.vis.length === 0 && card.more <= 0 && !this.isGroup(key);
|
|
506
|
+
return {
|
|
507
|
+
'data-node-kind': card?.node.kind,
|
|
508
|
+
'data-node-group': card?.node.group,
|
|
509
|
+
'data-node-state': this.nodeState(key) ?? undefined,
|
|
510
|
+
'data-node-headonly': flag(headOnly)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
#cardGroupAttrs(key) {
|
|
514
|
+
return {
|
|
515
|
+
'data-graph-group': flag(this.isGroup(key)),
|
|
516
|
+
'data-graph-collapsed': flag(this.isCollapsed(key)),
|
|
517
|
+
'data-graph-member-of': this.groupOf(key) ?? undefined
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/** What a card's head counts: a group, the members it stands for; anything else, its rows. */
|
|
521
|
+
cardCount(key) {
|
|
522
|
+
return this.isGroup(key) ? this.memberCount(key) : (this.cards[key]?.node.rows.length ?? 0);
|
|
523
|
+
}
|
|
524
|
+
/** The "more rows" control's hooks: it expands the card, and says when a filter emptied it. */
|
|
525
|
+
moreAttrs(key) {
|
|
526
|
+
const expanded = this.isExpanded(key);
|
|
527
|
+
return {
|
|
528
|
+
'data-graph-press': 'expand',
|
|
529
|
+
'data-graph-key': key,
|
|
530
|
+
'data-graph-more-empty': flag(this.cards[key]?.vis.length === 0 && !expanded),
|
|
531
|
+
'data-expanded': flag(expanded)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The box a `data-graph-key` names, if it is on the canvas: a container by its render key,
|
|
536
|
+
* a leaf by its node id — the one key a leaf carries for both its press and its open.
|
|
537
|
+
*/
|
|
538
|
+
boxFor(key) {
|
|
539
|
+
return this.clusters.find((cluster) => cluster.nodeId === key || this.clusterKey(cluster) === key);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Perform an intent read off the DOM (see `state/intents`). An unknown intent, or a key
|
|
543
|
+
* that names nothing, does nothing — the DOM is not a trusted source.
|
|
544
|
+
*/
|
|
545
|
+
act(intent, key) {
|
|
546
|
+
if (!isIntent(intent))
|
|
547
|
+
return;
|
|
548
|
+
INTENTS[intent](this, key ?? '');
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* A cluster's unique render key.
|
|
552
|
+
*
|
|
553
|
+
* NOT the name: nesting puts a `table` box under `public` AND under `billing`, and keying
|
|
554
|
+
* an `{#each}` by name alone is a duplicate key — Svelte throws `each_key_duplicate`, the
|
|
555
|
+
* render aborts, and no inner box appears at all, which reads as nesting silently not
|
|
556
|
+
* working rather than as an error.
|
|
557
|
+
*/
|
|
483
558
|
clusterKey(cluster) {
|
|
559
|
+
// A containment box's PATH is its address, unique by construction; the parent's label is
|
|
560
|
+
// not — `lib/marks` under `geoms` and under `src` share depth, parent label and name.
|
|
561
|
+
// A pathless box (an orphan node at the root, or a group box) keeps the label key.
|
|
562
|
+
if (cluster.path && cluster.path.length > 0)
|
|
563
|
+
return `path:${cluster.path.join('/')}`;
|
|
484
564
|
return `${cluster.depth ?? 0}:${cluster.parent ?? ''}:${cluster.name}`;
|
|
485
565
|
}
|
|
566
|
+
/** `polymetric` — which measure is on which channel, and each one's cap; for the legend. */
|
|
567
|
+
get channels() {
|
|
568
|
+
return this.#result.channels;
|
|
569
|
+
}
|
|
570
|
+
/** The polymetric legend's rows, labelled from the locale (#168). */
|
|
571
|
+
get channelRows() {
|
|
572
|
+
return channelRows(this.#result.channels);
|
|
573
|
+
}
|
|
574
|
+
/** What each side of an arc diagram shows, named from the data or the locale (#169). */
|
|
575
|
+
get sideNames() {
|
|
576
|
+
return sideNames([...this.#model.edges, ...this.#model.overlays], this.config.above);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Every measure a channel could bind: the keys in the nodes' `measures`, plus `weight` and
|
|
580
|
+
* `degree`, which every node has an answer for. Sorted, so a picker does not reshuffle.
|
|
581
|
+
*/
|
|
582
|
+
get measureKeys() {
|
|
583
|
+
return unique([
|
|
584
|
+
'degree',
|
|
585
|
+
'weight',
|
|
586
|
+
...this.#canonical.nodes.flatMap((node) => Object.keys(node.measures ?? {}))
|
|
587
|
+
]);
|
|
588
|
+
}
|
|
486
589
|
get groupBy() {
|
|
487
|
-
return this
|
|
590
|
+
return this.config.groupBy;
|
|
488
591
|
}
|
|
489
592
|
/**
|
|
490
593
|
* The node kinds actually present, sorted.
|
|
@@ -509,32 +612,32 @@ export class GraphState {
|
|
|
509
612
|
get groupsPresent() {
|
|
510
613
|
return unique(this.#model.nodes.map((n) => n.group));
|
|
511
614
|
}
|
|
512
|
-
/** Whether cards carry the group ramp themselves. See `GraphStateConfig.groupTint`. */
|
|
513
615
|
/** `dot` or `card` — what the active layout draws a node as. */
|
|
514
616
|
get nodeShape() {
|
|
515
617
|
return nodeShapeOf(this.layoutName);
|
|
516
618
|
}
|
|
619
|
+
/** Whether cards carry the group ramp themselves. See `GraphStateConfig.groupTint`. */
|
|
517
620
|
get groupTint() {
|
|
518
|
-
return this
|
|
621
|
+
return this.config.groupTint;
|
|
519
622
|
}
|
|
520
623
|
get nestBy() {
|
|
521
|
-
return this
|
|
624
|
+
return this.config.nestBy;
|
|
522
625
|
}
|
|
523
626
|
get sizeBy() {
|
|
524
|
-
return this
|
|
627
|
+
return this.config.sizeBy;
|
|
525
628
|
}
|
|
526
629
|
get sizeScale() {
|
|
527
|
-
return this
|
|
630
|
+
return this.config.sizeScale;
|
|
528
631
|
}
|
|
529
632
|
get depth() {
|
|
530
|
-
return this
|
|
633
|
+
return this.config.depth;
|
|
531
634
|
}
|
|
532
635
|
get focusPath() {
|
|
533
|
-
return this
|
|
636
|
+
return this.config.focusPath;
|
|
534
637
|
}
|
|
535
638
|
/** The configured cap, or the package default of 2 when none was set. */
|
|
536
639
|
get levels() {
|
|
537
|
-
return this
|
|
640
|
+
return this.config.levels ?? 2;
|
|
538
641
|
}
|
|
539
642
|
/**
|
|
540
643
|
* Column headings for a multi-column layout, empty for the grid ones.
|
|
@@ -558,24 +661,22 @@ export class GraphState {
|
|
|
558
661
|
* a rendering fault rather than a no-op.
|
|
559
662
|
*/
|
|
560
663
|
setGrouping(outer, inner = null) {
|
|
561
|
-
this
|
|
562
|
-
this.#nestBy = inner === outer ? null : inner;
|
|
664
|
+
this.config.setGrouping(outer, inner);
|
|
563
665
|
}
|
|
564
666
|
get edgeStyle() {
|
|
565
|
-
return this
|
|
667
|
+
return this.config.edgeStyle;
|
|
566
668
|
}
|
|
567
669
|
get mode() {
|
|
568
|
-
return this
|
|
670
|
+
return this.config.mode;
|
|
569
671
|
}
|
|
570
672
|
/** The layout's registry key, or 'custom' when a LayoutFn was passed directly. */
|
|
571
673
|
get layoutName() {
|
|
572
|
-
return typeof this
|
|
674
|
+
return typeof this.config.layout === 'string' ? this.config.layout : 'custom';
|
|
573
675
|
}
|
|
574
676
|
get value() {
|
|
575
|
-
return this
|
|
677
|
+
return this.selection.value;
|
|
576
678
|
}
|
|
577
679
|
get label() {
|
|
578
|
-
return (this.#
|
|
579
|
-
`Diagram of ${plural(this.#model.nodes.length, 'node')} and ${plural(this.#model.edges.length, 'relationship')}`);
|
|
680
|
+
return this.config.label ?? diagramLabel(this.#model.nodes.length, this.#model.edges.length);
|
|
580
681
|
}
|
|
581
682
|
}
|