@rokkit/graph 1.8.0 → 1.8.2
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 +59 -0
- package/dist/Graph.svelte +108 -134
- 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 +161 -52
- package/dist/GraphState.svelte.js +380 -304
- 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 +11 -32
- 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 +10 -3
- package/dist/index.js +9 -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 +26 -2
- 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 +12 -0
- package/dist/layout/matrix.js +20 -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 +79 -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 +64 -2
- 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 +72 -1
- package/package.json +5 -4
package/dist/model/tree.d.ts
CHANGED
|
@@ -3,8 +3,13 @@ export type TreeNode = {
|
|
|
3
3
|
/** Path joined by `/`. The root is `''`. */
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
|
-
/** Full path to this node, outermost first. Empty at the root. */
|
|
6
|
+
/** Full path to this node, outermost first. Empty at the root. Folding splices it. */
|
|
7
7
|
path: string[];
|
|
8
|
+
/**
|
|
9
|
+
* The node's path in the DATA — never folded. What a host knows a node by, so what a drill
|
|
10
|
+
* reports and what `focusPath` names; `path` is only where the box sits in the picture.
|
|
11
|
+
*/
|
|
12
|
+
address: string[];
|
|
8
13
|
/** 0 at the root. A layout materialising N levels reads this. */
|
|
9
14
|
depth: number;
|
|
10
15
|
/** The real node behind this box. Absent only on a container nothing declared. */
|
|
@@ -22,6 +27,7 @@ export type TreeNode = {
|
|
|
22
27
|
*/
|
|
23
28
|
export declare function buildTree(model: GraphModel, options?: {
|
|
24
29
|
measure?: string;
|
|
30
|
+
keep?: string[];
|
|
25
31
|
}): TreeNode;
|
|
26
32
|
/** The node at `path`, or undefined. */
|
|
27
33
|
export declare function findNode(root: TreeNode, path: string[]): TreeNode | undefined;
|
package/dist/model/tree.js
CHANGED
|
@@ -22,6 +22,7 @@ function container(path) {
|
|
|
22
22
|
id: idOf(path),
|
|
23
23
|
label: path[path.length - 1] ?? ROOT,
|
|
24
24
|
path,
|
|
25
|
+
address: path,
|
|
25
26
|
depth: path.length,
|
|
26
27
|
children: [],
|
|
27
28
|
value: 0
|
|
@@ -95,6 +96,8 @@ function attach(root, node, measure, degree) {
|
|
|
95
96
|
id: node.id,
|
|
96
97
|
label: node.label,
|
|
97
98
|
path: [],
|
|
99
|
+
// No address either: a pathless node has no place in the containment tree to drill to.
|
|
100
|
+
address: [],
|
|
98
101
|
depth: 1,
|
|
99
102
|
node,
|
|
100
103
|
children: [],
|
|
@@ -131,9 +134,13 @@ function summarise(node, depth) {
|
|
|
131
134
|
* edges, and folding it would silently drop them. The rule is "a wrapper is not a level", and
|
|
132
135
|
* a thing the data names is not a wrapper.
|
|
133
136
|
*/
|
|
134
|
-
function fold(node) {
|
|
135
|
-
node.children = node.children.map(fold);
|
|
136
|
-
|
|
137
|
+
function fold(node, keep) {
|
|
138
|
+
node.children = node.children.map((child) => fold(child, keep));
|
|
139
|
+
// A container ON the focus chain is the scope the reader asked for, not punctuation: folding
|
|
140
|
+
// it would make the focusPath naming it stop resolving. A host loading one level at a time
|
|
141
|
+
// sends exactly such a chain — a crate's modules, with nothing declaring the crate.
|
|
142
|
+
const onFocusChain = node.address.every((segment, i) => keep[i] === segment);
|
|
143
|
+
if (node.children.length === 1 && node.node === undefined && node.depth > 0 && !onFocusChain) {
|
|
137
144
|
const only = node.children[0];
|
|
138
145
|
// The child takes this box's PLACE, so the segment this box contributed is spliced out
|
|
139
146
|
// of its path — not merely trimmed from the end, which would leave a leaf sitting at
|
|
@@ -165,7 +172,7 @@ export function buildTree(model, options = {}) {
|
|
|
165
172
|
// a descendant arriving first synthesises the box, and its owner fills in the identity.
|
|
166
173
|
for (const node of model.nodes)
|
|
167
174
|
attach(root, node, measure, degree);
|
|
168
|
-
const folded = fold(root);
|
|
175
|
+
const folded = fold(root, options.keep ?? []);
|
|
169
176
|
sort(folded);
|
|
170
177
|
summarise(folded, 0);
|
|
171
178
|
return folded;
|
package/dist/preset.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export type GraphShades = {
|
|
|
12
12
|
stroke: string;
|
|
13
13
|
label: string;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The sequential ramp a share is shaded on (#164): a palette family and the two steps it runs
|
|
17
|
+
* between. Unlike `groups` this is ORDINAL — more of the measure is more of the colour.
|
|
18
|
+
*/
|
|
19
|
+
export type GraphShadeRamp = {
|
|
20
|
+
family: string;
|
|
21
|
+
from: string;
|
|
22
|
+
to: string;
|
|
23
|
+
};
|
|
15
24
|
export type GraphPreset = {
|
|
16
25
|
/** Ordered ramp assigned to open-ended group names, wrapping when exhausted. */
|
|
17
26
|
groups: string[];
|
|
@@ -21,6 +30,8 @@ export type GraphPreset = {
|
|
|
21
30
|
};
|
|
22
31
|
patterns: string[];
|
|
23
32
|
using: GraphChannel;
|
|
33
|
+
/** The ramp a `shadeBy` share is painted on (#164). Independent of `using`. */
|
|
34
|
+
shade: GraphShadeRamp;
|
|
24
35
|
};
|
|
25
36
|
/**
|
|
26
37
|
* Mirrors `createChartPreset`'s shape on purpose — a consumer who has met one
|
|
@@ -32,6 +43,13 @@ export type GraphPreset = {
|
|
|
32
43
|
*/
|
|
33
44
|
export declare const defaultGraphPreset: GraphPreset;
|
|
34
45
|
export declare function createGraphPreset(overrides?: Partial<GraphPreset>): GraphPreset;
|
|
46
|
+
/**
|
|
47
|
+
* A share in 0..1 as custom properties, the way `resolveGroupStyles` resolves a group: the
|
|
48
|
+
* ramp step nearest the share for the fill, two steps on for the stroke, and a label MEASURED
|
|
49
|
+
* against that fill — rokkit's auto on-color (near-black or near-white by luminance), so the
|
|
50
|
+
* flip point falls out of the ramp instead of being guessed.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveShade(share: number, mode: 'light' | 'dark', preset?: GraphPreset): Record<'--group-fill' | '--group-stroke' | '--group-label', string>;
|
|
35
53
|
/**
|
|
36
54
|
* Resolves group names to CSS custom properties.
|
|
37
55
|
*
|
package/dist/preset.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { categoricalPalette } from '@rokkit/core';
|
|
1
|
+
import { categoricalPalette, pickOnColor, relativeLuminance } from '@rokkit/core';
|
|
2
2
|
/**
|
|
3
3
|
* Mirrors `createChartPreset`'s shape on purpose — a consumer who has met one
|
|
4
4
|
* has met both. Shade picks differ from chart's because a cluster is a large
|
|
@@ -17,7 +17,9 @@ export const defaultGraphPreset = {
|
|
|
17
17
|
dark: { fill: '900', stroke: '600', label: '100' }
|
|
18
18
|
},
|
|
19
19
|
patterns: ['diagonal', 'dots', 'triangles', 'hatch', 'lattice', 'swell', 'checkerboard', 'waves'],
|
|
20
|
-
using: 'color'
|
|
20
|
+
using: 'color',
|
|
21
|
+
// Neutral, so a shade never reads as a group's hue. The palette has no `slate`.
|
|
22
|
+
shade: { family: 'gray', from: '50', to: '900' }
|
|
21
23
|
};
|
|
22
24
|
export function createGraphPreset(overrides = {}) {
|
|
23
25
|
return {
|
|
@@ -26,7 +28,36 @@ export function createGraphPreset(overrides = {}) {
|
|
|
26
28
|
shades: {
|
|
27
29
|
light: { ...defaultGraphPreset.shades.light, ...overrides.shades?.light },
|
|
28
30
|
dark: { ...defaultGraphPreset.shades.dark, ...overrides.shades?.dark }
|
|
29
|
-
}
|
|
31
|
+
},
|
|
32
|
+
shade: { ...defaultGraphPreset.shade, ...overrides.shade }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The family's steps from `from` to `to`, in that order — reversed in dark mode. */
|
|
36
|
+
function rampSteps(ramp, mode) {
|
|
37
|
+
const family = categoricalPalette[ramp.family] ?? categoricalPalette[defaultGraphPreset.shade.family];
|
|
38
|
+
const [lo, hi] = [Number(ramp.from), Number(ramp.to)].sort((a, b) => a - b);
|
|
39
|
+
const steps = Object.keys(family)
|
|
40
|
+
.map(Number)
|
|
41
|
+
.filter((step) => step >= lo && step <= hi)
|
|
42
|
+
.sort((a, b) => a - b)
|
|
43
|
+
.map((step) => family[String(step)]);
|
|
44
|
+
// In dark mode the paper is dark, so "more ink" runs toward the light end.
|
|
45
|
+
return mode === 'dark' ? steps.reverse() : steps;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A share in 0..1 as custom properties, the way `resolveGroupStyles` resolves a group: the
|
|
49
|
+
* ramp step nearest the share for the fill, two steps on for the stroke, and a label MEASURED
|
|
50
|
+
* against that fill — rokkit's auto on-color (near-black or near-white by luminance), so the
|
|
51
|
+
* flip point falls out of the ramp instead of being guessed.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveShade(share, mode, preset = defaultGraphPreset) {
|
|
54
|
+
const steps = rampSteps(preset.shade, mode);
|
|
55
|
+
const at = Math.round(Math.min(1, Math.max(0, share)) * (steps.length - 1));
|
|
56
|
+
const fill = steps[at];
|
|
57
|
+
return {
|
|
58
|
+
'--group-fill': fill,
|
|
59
|
+
'--group-stroke': steps[Math.min(steps.length - 1, at + 2)],
|
|
60
|
+
'--group-label': pickOnColor(relativeLuminance(fill))
|
|
30
61
|
};
|
|
31
62
|
}
|
|
32
63
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { EntityRow, GraphStateConfig } from '../GraphState.svelte.js'
|
|
7
7
|
import NoteBlocks from './NoteBlocks.svelte'
|
|
8
8
|
import type { GraphFields } from '../types.js'
|
|
9
|
+
import { say } from '../messages.js'
|
|
9
10
|
|
|
10
11
|
let {
|
|
11
12
|
state: provided,
|
|
@@ -40,12 +41,13 @@
|
|
|
40
41
|
if (graph === own) own.update(config())
|
|
41
42
|
})
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{ name: '
|
|
46
|
-
{ name: '
|
|
47
|
-
{ name: '
|
|
48
|
-
|
|
44
|
+
// $derived, so the headings follow a locale change.
|
|
45
|
+
const columns: TableColumn[] = $derived([
|
|
46
|
+
{ name: 'label', label: say('entity'), snippet: 'entity' },
|
|
47
|
+
{ name: 'rowCount', label: say('rows'), align: 'right', snippet: 'rows' },
|
|
48
|
+
{ name: 'refCount', label: say('refs'), align: 'right', snippet: 'refs' },
|
|
49
|
+
{ name: 'note', label: say('comment'), sortable: false, snippet: 'comment' }
|
|
50
|
+
])
|
|
49
51
|
</script>
|
|
50
52
|
|
|
51
53
|
{#snippet entity(_value: unknown, _column: TableColumn, row: Record<string, unknown>)}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import NoteBlocks from './NoteBlocks.svelte'
|
|
6
6
|
import { baseType, typeSize } from './column-type.js'
|
|
7
7
|
import type { GraphFields } from '../types.js'
|
|
8
|
+
import { counted, say } from '../messages.js'
|
|
9
|
+
import { interactions } from '../actions/interactions.js'
|
|
8
10
|
|
|
9
11
|
let {
|
|
10
12
|
state: provided,
|
|
@@ -44,22 +46,22 @@
|
|
|
44
46
|
</script>
|
|
45
47
|
|
|
46
48
|
{#if entity}
|
|
47
|
-
<article data-graph-entity data-node-kind={entity.kind} class={className}>
|
|
49
|
+
<article data-graph-entity data-node-kind={entity.kind} class={className} use:interactions={{ state: graph }}>
|
|
48
50
|
<header data-graph-entity-head>
|
|
49
51
|
{#if entity.group}<span data-graph-entity-group>{entity.group}.</span>{/if}
|
|
50
52
|
<h2 data-graph-entity-title>{entity.label}</h2>
|
|
51
|
-
<span data-graph-entity-count>{entity.rows.length}
|
|
53
|
+
<span data-graph-entity-count>{counted(entity.rows.length, 'columnOne', 'columnMany')}</span>
|
|
52
54
|
</header>
|
|
53
55
|
|
|
54
56
|
{#if entity.note}
|
|
55
57
|
<section data-graph-section="note">
|
|
56
|
-
<h3 data-graph-section-title>
|
|
58
|
+
<h3 data-graph-section-title>{say('comment')}</h3>
|
|
57
59
|
<NoteBlocks note={entity.note} />
|
|
58
60
|
</section>
|
|
59
61
|
{/if}
|
|
60
62
|
|
|
61
63
|
<section data-graph-section="columns">
|
|
62
|
-
<h3 data-graph-section-title>
|
|
64
|
+
<h3 data-graph-section-title>{say('columns')}</h3>
|
|
63
65
|
{#each entity.rows as row (row.name)}
|
|
64
66
|
<div data-graph-column>
|
|
65
67
|
<span data-graph-column-name>{row.name}</span>
|
|
@@ -77,11 +79,11 @@
|
|
|
77
79
|
|
|
78
80
|
{#if indexes.length > 0}
|
|
79
81
|
<section data-graph-section="indexes">
|
|
80
|
-
<h3 data-graph-section-title>
|
|
82
|
+
<h3 data-graph-section-title>{say('indexes')}</h3>
|
|
81
83
|
{#each indexes as index, i (i)}
|
|
82
84
|
<div data-graph-index>
|
|
83
85
|
<span data-graph-index-def>{index.def}</span>
|
|
84
|
-
{#if index.unique}<span data-graph-index-unique>unique</span>{/if}
|
|
86
|
+
{#if index.unique}<span data-graph-index-unique>{say('unique')}</span>{/if}
|
|
85
87
|
{#if index.name}<span data-graph-index-name>{index.name}</span>{/if}
|
|
86
88
|
</div>
|
|
87
89
|
{/each}
|
|
@@ -89,15 +91,16 @@
|
|
|
89
91
|
{/if}
|
|
90
92
|
|
|
91
93
|
<section data-graph-section="relationships">
|
|
92
|
-
<h3 data-graph-section-title>
|
|
94
|
+
<h3 data-graph-section-title>{say('relationships')}</h3>
|
|
93
95
|
{#if graph.relationships.length === 0}
|
|
94
|
-
<p data-graph-relationships-empty>
|
|
96
|
+
<p data-graph-relationships-empty>{say('noRelationships')}</p>
|
|
95
97
|
{:else}
|
|
96
98
|
{#each graph.relationships as rel, i (rel.edge.id + i)}
|
|
97
99
|
<button
|
|
98
100
|
type="button"
|
|
99
101
|
data-graph-relationship={rel.direction}
|
|
100
|
-
|
|
102
|
+
data-graph-press="select"
|
|
103
|
+
data-graph-key={rel.id}
|
|
101
104
|
>
|
|
102
105
|
{#if rel.group}<span data-graph-relationship-group>{rel.group}.</span>{/if}
|
|
103
106
|
<span data-graph-relationship-label>{rel.label}</span>
|
|
@@ -111,6 +114,6 @@
|
|
|
111
114
|
</article>
|
|
112
115
|
{:else}
|
|
113
116
|
<article data-graph-entity data-graph-entity-empty class={className}>
|
|
114
|
-
<p>
|
|
117
|
+
<p>{say('noEntity')}</p>
|
|
115
118
|
</article>
|
|
116
119
|
{/if}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { GraphPreset } from '../preset.js';
|
|
2
|
+
import type { Arrange, Density, EdgeStyle, LayoutFn, NodeAxis, ShowEdges } from '../layout/types.js';
|
|
3
|
+
import type { GraphFields } from '../types.js';
|
|
4
|
+
import type { GraphStateConfig } from '../GraphState.svelte.js';
|
|
5
|
+
export type GraphConfigValues = {
|
|
6
|
+
nodes: unknown[];
|
|
7
|
+
edges: unknown[];
|
|
8
|
+
fields: GraphFields;
|
|
9
|
+
layout: string | LayoutFn;
|
|
10
|
+
focus: string | null;
|
|
11
|
+
density: Density;
|
|
12
|
+
arrange: Arrange;
|
|
13
|
+
groupBy: NodeAxis;
|
|
14
|
+
nestBy: NodeAxis | null;
|
|
15
|
+
groupTint: boolean;
|
|
16
|
+
sizeBy: string;
|
|
17
|
+
sizeScale: 'linear' | 'log';
|
|
18
|
+
depth: number;
|
|
19
|
+
focusPath: string[];
|
|
20
|
+
levels: number | undefined;
|
|
21
|
+
root: string | null;
|
|
22
|
+
bundleTension: number | undefined;
|
|
23
|
+
radialMode: 'tree' | 'dendrogram';
|
|
24
|
+
edgeStyle: EdgeStyle;
|
|
25
|
+
preset: GraphPreset;
|
|
26
|
+
mode: 'light' | 'dark';
|
|
27
|
+
label: string | undefined;
|
|
28
|
+
onselect: ((id: string | null) => void) | undefined;
|
|
29
|
+
ondrill: GraphStateConfig['ondrill'];
|
|
30
|
+
ondrillup: GraphStateConfig['ondrillup'];
|
|
31
|
+
onfocuspath: GraphStateConfig['onfocuspath'];
|
|
32
|
+
onexpand: GraphStateConfig['onexpand'];
|
|
33
|
+
oncollapse: GraphStateConfig['oncollapse'];
|
|
34
|
+
showEdges: ShowEdges;
|
|
35
|
+
layerLabels: string[] | undefined;
|
|
36
|
+
widthBy: string | undefined;
|
|
37
|
+
heightBy: string | undefined;
|
|
38
|
+
colorBy: string | undefined;
|
|
39
|
+
above: string | undefined;
|
|
40
|
+
shadeBy: string | undefined;
|
|
41
|
+
};
|
|
42
|
+
type Key = keyof GraphConfigValues;
|
|
43
|
+
type Field = {
|
|
44
|
+
key: Key;
|
|
45
|
+
/** Shapes the supplied-or-fallback value; sees the whole config (nestBy reads groupBy). */
|
|
46
|
+
normalise?: (value: unknown, config: GraphStateConfig) => unknown;
|
|
47
|
+
} & ({
|
|
48
|
+
fallback: () => unknown;
|
|
49
|
+
raw?: never;
|
|
50
|
+
}
|
|
51
|
+
/** Taken as given — a `null` stays `null`, an omitted key is `undefined` — no fallback. */
|
|
52
|
+
| {
|
|
53
|
+
raw: true;
|
|
54
|
+
fallback?: never;
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Every input, its default, and how it is shaped. `update()` assigns every field on every call —
|
|
58
|
+
* an omitted key reverts to its fallback, which is the contract the GraphState specs pin
|
|
59
|
+
* field by field.
|
|
60
|
+
*/
|
|
61
|
+
export declare const CONFIG_FIELDS: readonly Field[];
|
|
62
|
+
/**
|
|
63
|
+
* A graph's inputs — the one job `update()` has. The layout pipeline and the selection read
|
|
64
|
+
* from here.
|
|
65
|
+
*
|
|
66
|
+
* `update()` FULLY re-applies config rather than merging deltas, matching `SparkState.update` —
|
|
67
|
+
* so a prop reverting to undefined actually reverts. `apply()` is the merging verb, for a
|
|
68
|
+
* component writing the few options its own controls drive into a state someone else owns.
|
|
69
|
+
*/
|
|
70
|
+
export declare class GraphConfig {
|
|
71
|
+
#private;
|
|
72
|
+
constructor(config?: GraphStateConfig);
|
|
73
|
+
update(config?: GraphStateConfig): void;
|
|
74
|
+
/** Merge a partial config over the last full one, leaving every key it does not name alone. */
|
|
75
|
+
apply(config: Partial<GraphStateConfig>): GraphStateConfig;
|
|
76
|
+
/**
|
|
77
|
+
* Change the detail level from inside the component. `Graph`'s `density` PROP only reaches
|
|
78
|
+
* the state it owns; with a caller-supplied state — how all three views share one — a
|
|
79
|
+
* control that set the prop would render, click, and change nothing.
|
|
80
|
+
*/
|
|
81
|
+
setDensity(density: Density): void;
|
|
82
|
+
/** Every edge or only the climbing ones (#167) — what `ViolationsControl` drives. */
|
|
83
|
+
setShowEdges(value: ShowEdges): void;
|
|
84
|
+
/** Move the drill path from inside the state — drilling. See `GraphDrill`. */
|
|
85
|
+
setFocusPath(path: string[]): void;
|
|
86
|
+
/** Change the grouping axes from inside the component — same reason as `setDensity`. */
|
|
87
|
+
setGrouping(outer: NodeAxis, inner?: NodeAxis | null): void;
|
|
88
|
+
get nodes(): unknown[];
|
|
89
|
+
get edges(): unknown[];
|
|
90
|
+
get fields(): GraphFields;
|
|
91
|
+
get layout(): string | LayoutFn;
|
|
92
|
+
get focus(): string | null;
|
|
93
|
+
get density(): Density;
|
|
94
|
+
get arrange(): Arrange;
|
|
95
|
+
get groupBy(): NodeAxis;
|
|
96
|
+
get nestBy(): NodeAxis | null;
|
|
97
|
+
get groupTint(): boolean;
|
|
98
|
+
get sizeBy(): string;
|
|
99
|
+
get sizeScale(): "linear" | "log";
|
|
100
|
+
get depth(): number;
|
|
101
|
+
get focusPath(): string[];
|
|
102
|
+
get levels(): number | undefined;
|
|
103
|
+
get root(): string | null;
|
|
104
|
+
get bundleTension(): number | undefined;
|
|
105
|
+
get radialMode(): "tree" | "dendrogram";
|
|
106
|
+
get edgeStyle(): EdgeStyle;
|
|
107
|
+
get preset(): GraphPreset;
|
|
108
|
+
get mode(): "light" | "dark";
|
|
109
|
+
get label(): string | undefined;
|
|
110
|
+
get onselect(): ((id: string | null) => void) | undefined;
|
|
111
|
+
get ondrill(): ((path: string[], node: import("../types.js").GraphNode | null) => void | Promise<void>) | undefined;
|
|
112
|
+
get ondrillup(): ((path: string[]) => void | Promise<void>) | undefined;
|
|
113
|
+
get onfocuspath(): ((path: string[]) => void) | undefined;
|
|
114
|
+
get onexpand(): ((id: string, node: import("../types.js").GraphNode) => void) | undefined;
|
|
115
|
+
get oncollapse(): ((id: string, node: import("../types.js").GraphNode) => void) | undefined;
|
|
116
|
+
get showEdges(): ShowEdges;
|
|
117
|
+
get layerLabels(): string[] | undefined;
|
|
118
|
+
get widthBy(): string | undefined;
|
|
119
|
+
get heightBy(): string | undefined;
|
|
120
|
+
get colorBy(): string | undefined;
|
|
121
|
+
get above(): string | undefined;
|
|
122
|
+
get shadeBy(): string | undefined;
|
|
123
|
+
}
|
|
124
|
+
export {};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { defaultGraphPreset } from '../preset.js';
|
|
2
|
+
/**
|
|
3
|
+
* A level cap, or nothing.
|
|
4
|
+
*
|
|
5
|
+
* Left UNDEFINED when the caller said nothing: a state-level default of 2 is indistinguishable
|
|
6
|
+
* from an explicit 2, and `radial` needs "no cap" to mean the whole tree — inventing a number
|
|
7
|
+
* here silently pruned a codebase dendrogram to two rings.
|
|
8
|
+
*/
|
|
9
|
+
const floorLevels = (levels) => levels === undefined ? undefined : Math.max(1, Math.floor(levels));
|
|
10
|
+
/**
|
|
11
|
+
* Rejected rather than rendered: a box subdivided by its OWN axis yields exactly one child
|
|
12
|
+
* containing everything, which reads as a rendering fault, not a no-op.
|
|
13
|
+
*/
|
|
14
|
+
const distinctNest = (inner, outer) => (inner === outer ? null : inner);
|
|
15
|
+
/**
|
|
16
|
+
* Every input, its default, and how it is shaped. `update()` assigns every field on every call —
|
|
17
|
+
* an omitted key reverts to its fallback, which is the contract the GraphState specs pin
|
|
18
|
+
* field by field.
|
|
19
|
+
*/
|
|
20
|
+
export const CONFIG_FIELDS = Object.freeze([
|
|
21
|
+
{ key: 'nodes', fallback: () => [] },
|
|
22
|
+
{ key: 'edges', fallback: () => [] },
|
|
23
|
+
{ key: 'fields', fallback: () => ({}) },
|
|
24
|
+
{ key: 'layout', fallback: () => 'flow' },
|
|
25
|
+
{ key: 'focus', fallback: () => null },
|
|
26
|
+
{ key: 'density', fallback: () => 'keys' },
|
|
27
|
+
{ key: 'arrange', fallback: () => 'untangle' },
|
|
28
|
+
{ key: 'groupBy', fallback: () => 'group' },
|
|
29
|
+
{
|
|
30
|
+
key: 'nestBy',
|
|
31
|
+
fallback: () => null,
|
|
32
|
+
normalise: (v, config) => distinctNest(v, config.groupBy ?? 'group')
|
|
33
|
+
},
|
|
34
|
+
{ key: 'groupTint', fallback: () => false },
|
|
35
|
+
{ key: 'sizeBy', fallback: () => 'degree' },
|
|
36
|
+
{ key: 'sizeScale', fallback: () => 'linear' },
|
|
37
|
+
// Floored at 1: a depth of 0 or -1 is a request for nothing, and returning an empty canvas
|
|
38
|
+
// for it looks identical to a broken focus.
|
|
39
|
+
{ key: 'depth', fallback: () => 1, normalise: (v) => Math.max(1, Math.floor(v)) },
|
|
40
|
+
{ key: 'focusPath', fallback: () => [] },
|
|
41
|
+
{ key: 'levels', normalise: floorLevels, raw: true },
|
|
42
|
+
{ key: 'root', fallback: () => null },
|
|
43
|
+
{ key: 'bundleTension', raw: true },
|
|
44
|
+
{ key: 'radialMode', fallback: () => 'tree' },
|
|
45
|
+
{ key: 'edgeStyle', fallback: () => 'curved' },
|
|
46
|
+
{ key: 'preset', fallback: () => defaultGraphPreset },
|
|
47
|
+
{ key: 'mode', fallback: () => 'light' },
|
|
48
|
+
{ key: 'label', raw: true },
|
|
49
|
+
{ key: 'onselect', raw: true },
|
|
50
|
+
{ key: 'ondrill', raw: true },
|
|
51
|
+
{ key: 'ondrillup', raw: true },
|
|
52
|
+
{ key: 'onfocuspath', raw: true },
|
|
53
|
+
{ key: 'onexpand', raw: true },
|
|
54
|
+
{ key: 'oncollapse', raw: true },
|
|
55
|
+
{ key: 'showEdges', fallback: () => 'all' },
|
|
56
|
+
{ key: 'layerLabels', raw: true },
|
|
57
|
+
{ key: 'widthBy', raw: true },
|
|
58
|
+
{ key: 'heightBy', raw: true },
|
|
59
|
+
{ key: 'colorBy', raw: true },
|
|
60
|
+
{ key: 'above', raw: true },
|
|
61
|
+
{ key: 'shadeBy', raw: true }
|
|
62
|
+
]);
|
|
63
|
+
function resolve(config) {
|
|
64
|
+
const values = {};
|
|
65
|
+
for (const field of CONFIG_FIELDS) {
|
|
66
|
+
const given = config[field.key];
|
|
67
|
+
const value = field.raw ? given : (given ?? field.fallback());
|
|
68
|
+
const { key, normalise } = field;
|
|
69
|
+
values[key] = normalise ? normalise(value, config) : value;
|
|
70
|
+
}
|
|
71
|
+
return values;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A graph's inputs — the one job `update()` has. The layout pipeline and the selection read
|
|
75
|
+
* from here.
|
|
76
|
+
*
|
|
77
|
+
* `update()` FULLY re-applies config rather than merging deltas, matching `SparkState.update` —
|
|
78
|
+
* so a prop reverting to undefined actually reverts. `apply()` is the merging verb, for a
|
|
79
|
+
* component writing the few options its own controls drive into a state someone else owns.
|
|
80
|
+
*/
|
|
81
|
+
export class GraphConfig {
|
|
82
|
+
#v = $state(resolve({}));
|
|
83
|
+
/** The last FULL config, so `apply` can merge over it rather than reset around it. */
|
|
84
|
+
#last = {};
|
|
85
|
+
constructor(config = {}) {
|
|
86
|
+
this.update(config);
|
|
87
|
+
}
|
|
88
|
+
update(config = {}) {
|
|
89
|
+
const previous = this.#last;
|
|
90
|
+
this.#last = config;
|
|
91
|
+
// Field by field onto the one proxy, not a new object: each field stays its own signal,
|
|
92
|
+
// so a re-render that leaves `nodes` alone does not re-trigger what reads only `nodes`.
|
|
93
|
+
//
|
|
94
|
+
// And only the fields whose INPUT changed. Re-assigning the same array still fires its
|
|
95
|
+
// signal — the proxy wraps it afresh — and an absent `fields` resolves to a fresh `{}`, so
|
|
96
|
+
// every update would re-normalise the whole model even when only a layout option moved.
|
|
97
|
+
// A consumer's own `$state` array mutated in place is unaffected: it is tracked through
|
|
98
|
+
// its own proxy, not through this assignment.
|
|
99
|
+
const resolved = resolve(config);
|
|
100
|
+
const target = this.#v;
|
|
101
|
+
for (const key of Object.keys(resolved)) {
|
|
102
|
+
if (key in config && config[key] === previous[key])
|
|
103
|
+
continue;
|
|
104
|
+
if (!(key in config) && !(key in previous))
|
|
105
|
+
continue;
|
|
106
|
+
target[key] = resolved[key];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Merge a partial config over the last full one, leaving every key it does not name alone. */
|
|
110
|
+
apply(config) {
|
|
111
|
+
const merged = { ...this.#last, ...config };
|
|
112
|
+
this.update(merged);
|
|
113
|
+
return merged;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Change the detail level from inside the component. `Graph`'s `density` PROP only reaches
|
|
117
|
+
* the state it owns; with a caller-supplied state — how all three views share one — a
|
|
118
|
+
* control that set the prop would render, click, and change nothing.
|
|
119
|
+
*/
|
|
120
|
+
setDensity(density) {
|
|
121
|
+
this.#v.density = density;
|
|
122
|
+
this.#remember({ density });
|
|
123
|
+
}
|
|
124
|
+
/** Every edge or only the climbing ones (#167) — what `ViolationsControl` drives. */
|
|
125
|
+
setShowEdges(value) {
|
|
126
|
+
this.#v.showEdges = value;
|
|
127
|
+
this.#remember({ showEdges: value });
|
|
128
|
+
}
|
|
129
|
+
/** Move the drill path from inside the state — drilling. See `GraphDrill`. */
|
|
130
|
+
setFocusPath(path) {
|
|
131
|
+
this.#v.focusPath = path;
|
|
132
|
+
this.#remember({ focusPath: path });
|
|
133
|
+
}
|
|
134
|
+
/** Change the grouping axes from inside the component — same reason as `setDensity`. */
|
|
135
|
+
setGrouping(outer, inner = null) {
|
|
136
|
+
this.#v.groupBy = outer;
|
|
137
|
+
this.#v.nestBy = distinctNest(inner, outer);
|
|
138
|
+
this.#remember({ groupBy: outer, nestBy: this.#v.nestBy });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Fold a change made from INSIDE into the last full config. `apply()` merges over that, so a
|
|
142
|
+
* setter that changed only the live value was quietly undone by the next merge — any
|
|
143
|
+
* component writing its own options put the old drill path, density or grouping back.
|
|
144
|
+
*/
|
|
145
|
+
#remember(change) {
|
|
146
|
+
this.#last = { ...this.#last, ...change };
|
|
147
|
+
}
|
|
148
|
+
get nodes() {
|
|
149
|
+
return this.#v.nodes;
|
|
150
|
+
}
|
|
151
|
+
get edges() {
|
|
152
|
+
return this.#v.edges;
|
|
153
|
+
}
|
|
154
|
+
get fields() {
|
|
155
|
+
return this.#v.fields;
|
|
156
|
+
}
|
|
157
|
+
get layout() {
|
|
158
|
+
return this.#v.layout;
|
|
159
|
+
}
|
|
160
|
+
get focus() {
|
|
161
|
+
return this.#v.focus;
|
|
162
|
+
}
|
|
163
|
+
get density() {
|
|
164
|
+
return this.#v.density;
|
|
165
|
+
}
|
|
166
|
+
get arrange() {
|
|
167
|
+
return this.#v.arrange;
|
|
168
|
+
}
|
|
169
|
+
get groupBy() {
|
|
170
|
+
return this.#v.groupBy;
|
|
171
|
+
}
|
|
172
|
+
get nestBy() {
|
|
173
|
+
return this.#v.nestBy;
|
|
174
|
+
}
|
|
175
|
+
get groupTint() {
|
|
176
|
+
return this.#v.groupTint;
|
|
177
|
+
}
|
|
178
|
+
get sizeBy() {
|
|
179
|
+
return this.#v.sizeBy;
|
|
180
|
+
}
|
|
181
|
+
get sizeScale() {
|
|
182
|
+
return this.#v.sizeScale;
|
|
183
|
+
}
|
|
184
|
+
get depth() {
|
|
185
|
+
return this.#v.depth;
|
|
186
|
+
}
|
|
187
|
+
get focusPath() {
|
|
188
|
+
return this.#v.focusPath;
|
|
189
|
+
}
|
|
190
|
+
get levels() {
|
|
191
|
+
return this.#v.levels;
|
|
192
|
+
}
|
|
193
|
+
get root() {
|
|
194
|
+
return this.#v.root;
|
|
195
|
+
}
|
|
196
|
+
get bundleTension() {
|
|
197
|
+
return this.#v.bundleTension;
|
|
198
|
+
}
|
|
199
|
+
get radialMode() {
|
|
200
|
+
return this.#v.radialMode;
|
|
201
|
+
}
|
|
202
|
+
get edgeStyle() {
|
|
203
|
+
return this.#v.edgeStyle;
|
|
204
|
+
}
|
|
205
|
+
get preset() {
|
|
206
|
+
return this.#v.preset;
|
|
207
|
+
}
|
|
208
|
+
get mode() {
|
|
209
|
+
return this.#v.mode;
|
|
210
|
+
}
|
|
211
|
+
get label() {
|
|
212
|
+
return this.#v.label;
|
|
213
|
+
}
|
|
214
|
+
get onselect() {
|
|
215
|
+
return this.#v.onselect;
|
|
216
|
+
}
|
|
217
|
+
get ondrill() {
|
|
218
|
+
return this.#v.ondrill;
|
|
219
|
+
}
|
|
220
|
+
get ondrillup() {
|
|
221
|
+
return this.#v.ondrillup;
|
|
222
|
+
}
|
|
223
|
+
get onfocuspath() {
|
|
224
|
+
return this.#v.onfocuspath;
|
|
225
|
+
}
|
|
226
|
+
get onexpand() {
|
|
227
|
+
return this.#v.onexpand;
|
|
228
|
+
}
|
|
229
|
+
get oncollapse() {
|
|
230
|
+
return this.#v.oncollapse;
|
|
231
|
+
}
|
|
232
|
+
get showEdges() {
|
|
233
|
+
return this.#v.showEdges;
|
|
234
|
+
}
|
|
235
|
+
get layerLabels() {
|
|
236
|
+
return this.#v.layerLabels;
|
|
237
|
+
}
|
|
238
|
+
get widthBy() {
|
|
239
|
+
return this.#v.widthBy;
|
|
240
|
+
}
|
|
241
|
+
get heightBy() {
|
|
242
|
+
return this.#v.heightBy;
|
|
243
|
+
}
|
|
244
|
+
get colorBy() {
|
|
245
|
+
return this.#v.colorBy;
|
|
246
|
+
}
|
|
247
|
+
get above() {
|
|
248
|
+
return this.#v.above;
|
|
249
|
+
}
|
|
250
|
+
get shadeBy() {
|
|
251
|
+
return this.#v.shadeBy;
|
|
252
|
+
}
|
|
253
|
+
}
|