@jmbarzee/force-graph 0.0.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/LICENSE +21 -0
- package/README.md +424 -0
- package/dist-lib/components/FilterBar.d.ts +90 -0
- package/dist-lib/components/GraphView.d.ts +213 -0
- package/dist-lib/components/GroupLegend.d.ts +25 -0
- package/dist-lib/components/graph-view/focus.d.ts +16 -0
- package/dist-lib/components/graph-view/useFocusRequest.d.ts +9 -0
- package/dist-lib/components/graph-view/useLinkedViews.d.ts +85 -0
- package/dist-lib/components/graph-view/useRecentlyChanged.d.ts +5 -0
- package/dist-lib/components/graph-view/useTaxonomy.d.ts +11 -0
- package/dist-lib/components/graph-view/useTheme.d.ts +26 -0
- package/dist-lib/filter/linked.d.ts +26 -0
- package/dist-lib/filter/reconcile.d.ts +38 -0
- package/dist-lib/filter/toggle.d.ts +12 -0
- package/dist-lib/filter/types.d.ts +162 -0
- package/dist-lib/graph/axis.d.ts +290 -0
- package/dist-lib/graph/groups.d.ts +31 -0
- package/dist-lib/graph/layout.d.ts +104 -0
- package/dist-lib/graph/model.d.ts +149 -0
- package/dist-lib/graph/scalar.d.ts +88 -0
- package/dist-lib/graph/scheme.d.ts +68 -0
- package/dist-lib/graph/shapes.d.ts +227 -0
- package/dist-lib/graph/taxonomy.d.ts +207 -0
- package/dist-lib/graph/validate.d.ts +47 -0
- package/dist-lib/index.d.ts +35 -0
- package/dist-lib/index.js +4895 -0
- package/dist-lib/index.js.map +1 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/** A named axis of node identity, e.g. the type axis or the source-file axis. */
|
|
2
|
+
export type AxisId = string;
|
|
3
|
+
/**
|
|
4
|
+
* One value on an axis. A stable interned string: two nodes on the same value
|
|
5
|
+
* must produce keys that are `===` equal, not merely equivalent.
|
|
6
|
+
*/
|
|
7
|
+
export type AxisValue = string;
|
|
8
|
+
/** Where a node sits on every axis it has a value for. */
|
|
9
|
+
export type AxisValues = Readonly<Record<AxisId, AxisValue>>;
|
|
10
|
+
/**
|
|
11
|
+
* Fold anything into a stable value key.
|
|
12
|
+
*
|
|
13
|
+
* Composite selections are the trap this exists for. Passing an array or an
|
|
14
|
+
* object gives a deterministic string rather than a new object each call, so a
|
|
15
|
+
* `Set` of these deduplicates the way the caller expects.
|
|
16
|
+
*/
|
|
17
|
+
export declare function internValue(parts: string | readonly string[]): AxisValue;
|
|
18
|
+
/** One button in a filter bar: a label, an optional glyph, and the values it toggles. */
|
|
19
|
+
export interface FilterChip {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
/** A style value whose colour this chip wears, in the pane's theme; none means neutral. */
|
|
24
|
+
tint?: AxisValue;
|
|
25
|
+
/**
|
|
26
|
+
* Hover text, when the label alone is not enough to identify the chip.
|
|
27
|
+
*
|
|
28
|
+
* A chip for a path shows its last segment and needs the whole path here:
|
|
29
|
+
* two paths with the same last segment are otherwise indistinguishable.
|
|
30
|
+
* Omitted means "Show/Hide <label>", which is right for a chip whose label is
|
|
31
|
+
* already the whole identity.
|
|
32
|
+
*/
|
|
33
|
+
tooltip?: string;
|
|
34
|
+
/** Toggled as a unit — "any on" turns the whole group off. */
|
|
35
|
+
values: readonly AxisValue[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* How an axis names itself and its values — the part of a descriptor a control
|
|
39
|
+
* surface needs, split out so the style axis (which is not a filter and has no
|
|
40
|
+
* descriptor) can supply one too and share the identity-mapping builder.
|
|
41
|
+
*/
|
|
42
|
+
export interface AxisNamer {
|
|
43
|
+
id: AxisId;
|
|
44
|
+
/** Human label for the axis itself, e.g. for a dropdown or a chip group. */
|
|
45
|
+
label: string;
|
|
46
|
+
labelFor(value: AxisValue): string;
|
|
47
|
+
/** One or two characters for a token. */
|
|
48
|
+
abbreviationFor(value: AxisValue): string;
|
|
49
|
+
}
|
|
50
|
+
/** What a filter change asks of the running simulation. */
|
|
51
|
+
export interface ReheatPolicy {
|
|
52
|
+
/** Alpha to reheat to. */
|
|
53
|
+
alpha: number;
|
|
54
|
+
/** Seed newly-revealed nodes at their nearest visible ancestor first. */
|
|
55
|
+
seedRevealed: boolean;
|
|
56
|
+
/** Clear the initial-fit guard, so the camera refits to the new extent. */
|
|
57
|
+
refit: boolean;
|
|
58
|
+
/** Resume ticking. */
|
|
59
|
+
resume: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One filter axis, as a host declares it: its id, its label, and what an
|
|
63
|
+
* empty selection on it means. Everything else is policy with a default that
|
|
64
|
+
* follows from `emptyMeans`, because the two kinds of axis want opposite
|
|
65
|
+
* answers to each question:
|
|
66
|
+
*
|
|
67
|
+
* - an **allow-list** (`emptyMeans: 'none'`): a node with no value is
|
|
68
|
+
* hidden, a focus always expands the selection, a change reseeds the
|
|
69
|
+
* revealed nodes at their ancestors, refits the camera and reheats to
|
|
70
|
+
* 0.5, and the initial selection is every value;
|
|
71
|
+
* - a **narrowing** axis (`emptyMeans: 'all'`): a node with no value stays
|
|
72
|
+
* visible, a focus expands only an already-active selection (adding the
|
|
73
|
+
* first value would narrow everything to one thing — the opposite of
|
|
74
|
+
* focus), a change reheats to 0.3 in place and keeps the user's pan and
|
|
75
|
+
* zoom, and the initial selection is empty.
|
|
76
|
+
*
|
|
77
|
+
* **It does not carry the values.** Those are derived, not declared: the style
|
|
78
|
+
* axis's values come from the taxonomy, and an origin axis's from scanning the
|
|
79
|
+
* graph. Declaring them here too would be the same fact in two places with
|
|
80
|
+
* nothing enforcing agreement, free to drift apart unnoticed.
|
|
81
|
+
*/
|
|
82
|
+
export interface AxisDescriptor {
|
|
83
|
+
id: AxisId;
|
|
84
|
+
/** Human label for the axis itself, e.g. for a dropdown or a chip group. Default: the id. */
|
|
85
|
+
label?: string;
|
|
86
|
+
/**
|
|
87
|
+
* What an EMPTY selection on this axis means: `none` hides everything (the
|
|
88
|
+
* chips are an allow-list), `all` shows everything (the chips narrow).
|
|
89
|
+
*/
|
|
90
|
+
emptyMeans: 'all' | 'none';
|
|
91
|
+
/** What happens to a node carrying NO value on this axis while a selection is active. */
|
|
92
|
+
absentMeans?: 'visible' | 'hidden';
|
|
93
|
+
/**
|
|
94
|
+
* How a focus widens this axis to reveal its target: `always` adds the
|
|
95
|
+
* target's value; `whenActive` adds it only while a selection is active
|
|
96
|
+
* (on a narrowing axis, adding the first value would narrow everything to
|
|
97
|
+
* that one value — the opposite of a reveal). Default: `always` on an
|
|
98
|
+
* allow-list, `whenActive` on a narrowing axis.
|
|
99
|
+
*/
|
|
100
|
+
widens?: 'always' | 'whenActive';
|
|
101
|
+
/**
|
|
102
|
+
* Whether a change on this axis keeps the user's pan and zoom. Default: an
|
|
103
|
+
* allow-list refits the camera to the new extent (and seeds revealed nodes
|
|
104
|
+
* at their ancestors); a narrowing axis keeps the camera and reheats in
|
|
105
|
+
* place.
|
|
106
|
+
*/
|
|
107
|
+
keepCamera?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* How this axis's available values group into chips. Not one chip per
|
|
110
|
+
* value: a chip may cover several values. Takes the available values
|
|
111
|
+
* rather than reading a declared list, because an axis's values are
|
|
112
|
+
* derived from the graph. Absent, one chip per value, labelled by
|
|
113
|
+
* `labelFor` and, on the style axis, wearing the value's icon and tint.
|
|
114
|
+
*/
|
|
115
|
+
chipsFor?(values: readonly AxisValue[]): readonly FilterChip[];
|
|
116
|
+
/** A value's display name. Absent: the style's label on the style axis, else the value itself. */
|
|
117
|
+
labelFor?(value: AxisValue): string;
|
|
118
|
+
/** One or two characters for a token. Absent: the style's abbreviation on the style axis, else the first two characters of `labelFor`'s answer. */
|
|
119
|
+
abbreviationFor?(value: AxisValue): string;
|
|
120
|
+
/** A chip's hover text, when the label alone does not identify the value (a file's full path under its basename). Absent: none. */
|
|
121
|
+
tooltipFor?(value: AxisValue): string;
|
|
122
|
+
/** A value's glyph, drawn on its chip. Absent: the style's icon on the style axis, none elsewhere. */
|
|
123
|
+
iconFor?(value: AxisValue): string;
|
|
124
|
+
/**
|
|
125
|
+
* The values selected when the view first opens, given the values the
|
|
126
|
+
* graph presents on this axis. Absent: every value on an allow-list,
|
|
127
|
+
* none on a narrowing axis. `initialFilter` reads it; a host that passes the
|
|
128
|
+
* `defaultFilter` prop has chosen its own start, and this is not consulted.
|
|
129
|
+
*/
|
|
130
|
+
initialSelection?(values: readonly AxisValue[]): readonly AxisValue[];
|
|
131
|
+
/**
|
|
132
|
+
* How the bar draws this axis's chips. `chip` (the default) is a tinted,
|
|
133
|
+
* glyph-led button that reads as a category; `tag` is a quieter, neutral
|
|
134
|
+
* label that reads as an origin — a file, a source, a place — and shows an
|
|
135
|
+
* "everything included" state when the selection is empty on an
|
|
136
|
+
* `emptyMeans: 'all'` axis.
|
|
137
|
+
*/
|
|
138
|
+
presentation?: 'chip' | 'tag';
|
|
139
|
+
}
|
|
140
|
+
/** A filter axis with every policy filled in — what `Taxonomy.filterAxes` carries. */
|
|
141
|
+
export interface ResolvedAxisDescriptor extends AxisNamer {
|
|
142
|
+
emptyMeans: 'all' | 'none';
|
|
143
|
+
absentMeans: 'visible' | 'hidden';
|
|
144
|
+
/** How a focus widens this axis to reveal its target. */
|
|
145
|
+
widens: 'always' | 'whenActive';
|
|
146
|
+
/** Whether a change on this axis keeps the user's pan and zoom (a narrowing axis does; an allow-list refits). */
|
|
147
|
+
keepCamera: boolean;
|
|
148
|
+
tooltipFor?(value: AxisValue): string;
|
|
149
|
+
/** A value's glyph, or '' for none. */
|
|
150
|
+
iconFor(value: AxisValue): string;
|
|
151
|
+
chipsFor(values: readonly AxisValue[]): readonly FilterChip[];
|
|
152
|
+
initialSelection(values: readonly AxisValue[]): readonly AxisValue[];
|
|
153
|
+
presentation: 'chip' | 'tag';
|
|
154
|
+
}
|
|
155
|
+
/** How the style axis names and adorns its values, for a descriptor's defaults. */
|
|
156
|
+
export interface StyleNamer {
|
|
157
|
+
axis: AxisId;
|
|
158
|
+
labelFor(value: AxisValue): string;
|
|
159
|
+
abbreviationFor(value: AxisValue): string;
|
|
160
|
+
iconFor(value: AxisValue): string;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A declared axis with every default filled in. The policies follow from
|
|
164
|
+
* `emptyMeans`; the names follow from the style axis when this is it, and
|
|
165
|
+
* from the value itself otherwise.
|
|
166
|
+
*/
|
|
167
|
+
export declare function resolveDescriptor(d: AxisDescriptor, style: StyleNamer): ResolvedAxisDescriptor;
|
|
168
|
+
/** Where a node sits on one axis, or undefined when it carries no value there. */
|
|
169
|
+
export declare function valueOf(values: AxisValues, axis: AxisId): AxisValue | undefined;
|
|
170
|
+
/** One bucket of a mapping: a named group of source values. */
|
|
171
|
+
export interface MappingBucket {
|
|
172
|
+
id: string;
|
|
173
|
+
/** Display name — a tooltip, a column header. */
|
|
174
|
+
label: string;
|
|
175
|
+
/**
|
|
176
|
+
* One or two characters for a token. Required rather than derived: a control
|
|
177
|
+
* surface has no room for a name, and truncating one is a guess the host can
|
|
178
|
+
* answer better.
|
|
179
|
+
*/
|
|
180
|
+
abbreviation: string;
|
|
181
|
+
values: readonly AxisValue[];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A function from an axis's values to an equal-or-smaller set.
|
|
185
|
+
*
|
|
186
|
+
* The point is to bound a control surface. Push/pull renders one draggable
|
|
187
|
+
* token per mapped pair, so an axis with fifty values has no usable control
|
|
188
|
+
* surface until something collapses it — and a mapping is that something. The
|
|
189
|
+
* existing example is three separate kinds folded into one filter chip.
|
|
190
|
+
*/
|
|
191
|
+
export interface AxisMapping {
|
|
192
|
+
id: string;
|
|
193
|
+
label: string;
|
|
194
|
+
axis: AxisId;
|
|
195
|
+
buckets: readonly MappingBucket[];
|
|
196
|
+
/** The bucket a value belongs to, or undefined if the mapping omits it. */
|
|
197
|
+
bucketFor(value: AxisValue | undefined): MappingBucket | undefined;
|
|
198
|
+
}
|
|
199
|
+
/** What `createAxisMapping` is given: the mapping's id, label, axis and buckets, before the buckets are checked for overlap. */
|
|
200
|
+
export interface AxisMappingSpec {
|
|
201
|
+
id: string;
|
|
202
|
+
label: string;
|
|
203
|
+
axis: AxisId;
|
|
204
|
+
buckets: readonly MappingBucket[];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Build a mapping, refusing to build an incoherent one.
|
|
208
|
+
*
|
|
209
|
+
* **Buckets must not intersect.** A value in two buckets has no answer to "which
|
|
210
|
+
* token tunes this node", and the failure would otherwise surface as a layout
|
|
211
|
+
* that depends on bucket declaration order — the kind of bug that is found
|
|
212
|
+
* months later by someone reordering a list for readability. So it throws here,
|
|
213
|
+
* loudly, at construction, rather than resolving arbitrarily per lookup.
|
|
214
|
+
*/
|
|
215
|
+
export declare function createAxisMapping(spec: AxisMappingSpec): AxisMapping;
|
|
216
|
+
/**
|
|
217
|
+
* The identity mapping for an axis — one bucket per value.
|
|
218
|
+
*
|
|
219
|
+
* Useful as the default for an axis that is already small enough to control
|
|
220
|
+
* directly, so a consumer never has to special-case "no mapping".
|
|
221
|
+
*/
|
|
222
|
+
export declare function identityMapping(axis: AxisNamer, values: readonly AxisValue[]): AxisMapping;
|
|
223
|
+
/**
|
|
224
|
+
* True when every bucket is one value named after itself — the shape under
|
|
225
|
+
* which bucket ids ARE the axis's values, so a rule written over values applies
|
|
226
|
+
* to buckets unchanged. Structural, not a flag: a flag would be a second fact
|
|
227
|
+
* about the buckets with nothing holding it to the first.
|
|
228
|
+
*/
|
|
229
|
+
export declare function isIdentityMapping(mapping: AxisMapping): boolean;
|
|
230
|
+
/**
|
|
231
|
+
* The distinct values a set of subjects carries on one axis, in first-encounter
|
|
232
|
+
* order. The domain of an axis is whatever the graph presents on it — the
|
|
233
|
+
* engine declares no value vocabulary of its own — so this is how a control
|
|
234
|
+
* surface or a filter learns what an axis can take.
|
|
235
|
+
*/
|
|
236
|
+
export declare function valuesOn(subjects: Iterable<{
|
|
237
|
+
values: AxisValues;
|
|
238
|
+
}>, axis: AxisId): AxisValue[];
|
|
239
|
+
/**
|
|
240
|
+
* How many entities carry each value on each of the given axes — the chip
|
|
241
|
+
* badges' numbers when a host does not supply its own reading. The copies of
|
|
242
|
+
* one entity (`copyKey`) count once; a subject with neither key nor id counts
|
|
243
|
+
* as itself. An axis no subject has a value on gets an empty map, never a
|
|
244
|
+
* missing one.
|
|
245
|
+
*/
|
|
246
|
+
export declare function countsOn(subjects: Iterable<{
|
|
247
|
+
values: AxisValues;
|
|
248
|
+
id?: string;
|
|
249
|
+
copyKey?: string;
|
|
250
|
+
}>, axes: readonly AxisId[]): ReadonlyMap<AxisId, ReadonlyMap<AxisValue, number>>;
|
|
251
|
+
/**
|
|
252
|
+
* Does this table declare `key` itself, rather than inheriting it?
|
|
253
|
+
*
|
|
254
|
+
* Every keyed lookup on an axis value must go through this. Values are
|
|
255
|
+
* host-supplied strings, so a plain `table[key]` read walks the prototype chain
|
|
256
|
+
* and a value named `constructor`, `toString` or `__proto__` resolves to an
|
|
257
|
+
* inherited member. That member is not `undefined`, which means every
|
|
258
|
+
* `?? default` and `!== undefined` guard downstream silently accepts it.
|
|
259
|
+
*
|
|
260
|
+
* Spelled with `Object.prototype.hasOwnProperty.call` rather than the tidier
|
|
261
|
+
* `Object.hasOwn` so it needs no lib bump, and called on the prototype rather
|
|
262
|
+
* than the table so a table that itself declares a key named `hasOwnProperty`
|
|
263
|
+
* cannot shadow the check.
|
|
264
|
+
*/
|
|
265
|
+
export declare function hasOwn(table: object, key: string): boolean;
|
|
266
|
+
/**
|
|
267
|
+
* What a change on one axis asks of the simulation, from its two declared
|
|
268
|
+
* policies: an allow-list reheats harder; an axis that keeps the camera
|
|
269
|
+
* neither seeds revealed nodes at their ancestors nor refits.
|
|
270
|
+
*/
|
|
271
|
+
export declare function reheatOf(axis: Pick<ResolvedAxisDescriptor, 'emptyMeans' | 'keepCamera'>): ReheatPolicy;
|
|
272
|
+
/** What a filter change asks of the running simulation, folded across axes. */
|
|
273
|
+
export interface ReheatPlan extends ReheatPolicy {
|
|
274
|
+
/** The axes that actually changed — empty means "do nothing". */
|
|
275
|
+
changed: readonly AxisId[];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Fold the per-axis reheat policies of every axis that changed. Several axes
|
|
279
|
+
* can move at once (a view switch does exactly that), so the policies combine
|
|
280
|
+
* rather than the last one winning: the strongest reheat, and any axis that
|
|
281
|
+
* wants seeding, refitting or resuming gets it.
|
|
282
|
+
*/
|
|
283
|
+
export declare function foldReheatPolicy(changedAxes: readonly ResolvedAxisDescriptor[]): ReheatPlan;
|
|
284
|
+
/**
|
|
285
|
+
* The structural meaning of an edge — the one fact about an edge the engine
|
|
286
|
+
* reads for itself (graduation, highlighting and spring resolution branch on
|
|
287
|
+
* it). It is a field of the edge, not an axis: every axis, on nodes and on
|
|
288
|
+
* edges, is the host's own.
|
|
289
|
+
*/
|
|
290
|
+
export type Relation = 'containment' | 'dependency';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Theme } from './scheme';
|
|
2
|
+
/** The cycling glow palette per theme; a group with no `color` of its own takes the slot for its place in the list. */
|
|
3
|
+
export declare const GROUP_PALETTE: Readonly<Record<Theme, readonly string[]>>;
|
|
4
|
+
/** The glow strength of the group holding the hovered or selected node. */
|
|
5
|
+
export declare const NODE_FOCUS_STRENGTH = 2.2;
|
|
6
|
+
/** A group as the host asks for it: over the whole graph, visible or not. */
|
|
7
|
+
export interface NodeGroup {
|
|
8
|
+
id: string;
|
|
9
|
+
nodeIds: ReadonlySet<string>;
|
|
10
|
+
/** Whether the host emphasises this group; the view glows it stronger. A host's legend inside the pane uses `groupsControl`'s `emphasize` instead and leaves this out. */
|
|
11
|
+
emphasized?: boolean;
|
|
12
|
+
/** The glow's colour, hex or rgb(); absent, the palette's slot for this group's place in the list. */
|
|
13
|
+
color?: string;
|
|
14
|
+
}
|
|
15
|
+
/** A group as drawn: the visible node ids it covers and its palette colour. */
|
|
16
|
+
export interface ActiveGroup {
|
|
17
|
+
id: string;
|
|
18
|
+
color: string;
|
|
19
|
+
/** The glow's intensity: 1, or more for a group that is emphasised or holds the hovered or selected node. */
|
|
20
|
+
strength: number;
|
|
21
|
+
nodeIds: ReadonlySet<string>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The groups as drawn: each restricted to the visible set
|
|
25
|
+
* (filter-as-source-of-truth), the empty dropped, each coloured by its place
|
|
26
|
+
* in the host's list in the theme's palette — so hiding one group recolours
|
|
27
|
+
* none of the others.
|
|
28
|
+
*/
|
|
29
|
+
export declare function activeGroupsOf(groups: readonly NodeGroup[], visibleIds: ReadonlySet<string>, theme?: Theme, emphasized?: string | null): ActiveGroup[];
|
|
30
|
+
/** The active groups with any that holds one of the given nodes glowing at `NODE_FOCUS_STRENGTH`; the same array when none does. */
|
|
31
|
+
export declare function boostGroupsForNodes(groups: ActiveGroup[], focusIds: Array<string | null>): ActiveGroup[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AxisId } from './axis';
|
|
2
|
+
import { ScalarSpan } from './scalar';
|
|
3
|
+
import { SpringPhysics } from './shapes';
|
|
4
|
+
import { Taxonomy } from './taxonomy';
|
|
5
|
+
declare const LAYOUT_STATE: unique symbol;
|
|
6
|
+
/**
|
|
7
|
+
* The layout as the user tuned it: the forces (which axis push and pull key
|
|
8
|
+
* on and the per-bucket overrides; the two layout axes with theirs; the
|
|
9
|
+
* multipliers, exponents, gravities and dynamics) and the node scale. Opaque
|
|
10
|
+
* to a host: keep it, hand it back as `layout` or `defaultLayout`, and carry
|
|
11
|
+
* it to a store through `layoutToJSON` and `layoutFromJSON`. The view retunes
|
|
12
|
+
* it for the graph and taxonomy it faces — a tuning on an axis the graph no
|
|
13
|
+
* longer carries returns to the style axis — and reports the result.
|
|
14
|
+
*/
|
|
15
|
+
export interface LayoutState {
|
|
16
|
+
readonly [LAYOUT_STATE]: true;
|
|
17
|
+
}
|
|
18
|
+
/** One layout axis as plain data: a node axis with the bands the user changed, or a computed scalar with the span they dragged. */
|
|
19
|
+
export type LayoutTuningJSON = {
|
|
20
|
+
axis: AxisId;
|
|
21
|
+
overrides?: Record<string, {
|
|
22
|
+
min: number;
|
|
23
|
+
max: number;
|
|
24
|
+
}>;
|
|
25
|
+
} | {
|
|
26
|
+
scalar: string;
|
|
27
|
+
span?: ScalarSpan;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A layout as plain data, for a store, a URL or a host's own hand: what
|
|
31
|
+
* `layoutToJSON` writes and `layoutFromJSON` reads. Every part is optional
|
|
32
|
+
* and defaults to the taxonomy's, so a host that wants the view to open on a
|
|
33
|
+
* scalar writes `{ forces: { bandY: { scalar: 'credits' } } }` and nothing
|
|
34
|
+
* else.
|
|
35
|
+
*/
|
|
36
|
+
export interface LayoutJSON {
|
|
37
|
+
forces?: {
|
|
38
|
+
/** The axis push and pull key on, with the buckets the user changed: each bucket's charge and core radius, each edge type's spring. */
|
|
39
|
+
pair?: {
|
|
40
|
+
axis: AxisId;
|
|
41
|
+
push?: Record<string, {
|
|
42
|
+
charge: number;
|
|
43
|
+
coreRadius: number;
|
|
44
|
+
}>;
|
|
45
|
+
spring?: Record<string, SpringPhysics>;
|
|
46
|
+
};
|
|
47
|
+
/** The horizontal layout axis. */
|
|
48
|
+
bandX?: LayoutTuningJSON;
|
|
49
|
+
/** The vertical layout axis. */
|
|
50
|
+
bandY?: LayoutTuningJSON;
|
|
51
|
+
/** Scales every charge (repulsion). */
|
|
52
|
+
pushMultiplier?: number;
|
|
53
|
+
/** Scales every core radius (the softening of a charge at close range). */
|
|
54
|
+
coreRadiusMultiplier?: number;
|
|
55
|
+
/** The power of (distance² + softening) a charge falls off by; 1 is inverse-square. */
|
|
56
|
+
chargeExponent?: number;
|
|
57
|
+
/** Scales every spring (attraction). */
|
|
58
|
+
pullMultiplier?: number;
|
|
59
|
+
/** Scales every rest length. */
|
|
60
|
+
distanceMultiplier?: number;
|
|
61
|
+
/** The power of a spring's displacement; 1 is Hooke's law. */
|
|
62
|
+
linkExponent?: number;
|
|
63
|
+
/** The horizontal band gravity; 0 switches it off. */
|
|
64
|
+
gravityX?: number;
|
|
65
|
+
/** The vertical band gravity; 0 switches it off. */
|
|
66
|
+
gravityY?: number;
|
|
67
|
+
/** The power of the displacement outside a rest band; 1 is linear. */
|
|
68
|
+
gravityBandExp?: number;
|
|
69
|
+
/** Radial layout: the vertical axis's bands become rings about the origin. */
|
|
70
|
+
radial?: boolean;
|
|
71
|
+
/** The inward pull on a node by its downstream reach; 0 is off. */
|
|
72
|
+
gravityTopological?: number;
|
|
73
|
+
/** The power of the reach score; higher makes the biggest hubs pull hardest. */
|
|
74
|
+
gravityTopologicalExp?: number;
|
|
75
|
+
/** The pull toward the origin — the baseline when no band axis acts. */
|
|
76
|
+
gravityCenter?: number;
|
|
77
|
+
/** Whether the topological pull acts. */
|
|
78
|
+
topologicalEnabled?: boolean;
|
|
79
|
+
/** How fast the simulation cools per tick. */
|
|
80
|
+
alphaDecay?: number;
|
|
81
|
+
/** The temperature the simulation stops at. */
|
|
82
|
+
alphaMin?: number;
|
|
83
|
+
/** How much of a node's velocity survives a tick. */
|
|
84
|
+
velocityDecay?: number;
|
|
85
|
+
};
|
|
86
|
+
/** The node scale: the size knob, and how small and how large zoom may draw a node. */
|
|
87
|
+
nodeScale?: {
|
|
88
|
+
baseMul?: number;
|
|
89
|
+
minZoomMul?: number;
|
|
90
|
+
maxZoomMul?: number;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** A layout as plain data — every tuning by its axis or scalar, every override by its bucket, every number — for a store or a URL. */
|
|
94
|
+
export declare function layoutToJSON(layout: LayoutState): LayoutJSON;
|
|
95
|
+
/**
|
|
96
|
+
* The layout `layoutToJSON` wrote — or a host's own partial one — over the
|
|
97
|
+
* taxonomy's defaults, or undefined when a part present is not its shape:
|
|
98
|
+
* stored data is not trusted to be, and a host falls back to the default
|
|
99
|
+
* rather than to a layout that means something else. A tuning on an axis
|
|
100
|
+
* other than the style axis is built without buckets and completed by the
|
|
101
|
+
* view when the graph arrives, its overrides kept where their values survive.
|
|
102
|
+
*/
|
|
103
|
+
export declare function layoutFromJSON(value: unknown, taxonomy: Taxonomy): LayoutState | undefined;
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { AxisValues, Relation } from './axis';
|
|
2
|
+
/** A node of the host's graph: an opaque id, a name, its values on the host's axes, and an optional parent, copy key, ring, badges and payload. */
|
|
3
|
+
export interface GraphNode {
|
|
4
|
+
/**
|
|
5
|
+
* Stable identity, owned by whoever produced the graph.
|
|
6
|
+
*
|
|
7
|
+
* **Opaque.** The engine compares ids and looks them up; it never parses one
|
|
8
|
+
* or reads structure out of it. A host is free to make them hierarchical, or
|
|
9
|
+
* not — the two behave identically here, and that is the property worth
|
|
10
|
+
* keeping: the moment anything splits an id on a separator, the id's format
|
|
11
|
+
* becomes part of this model's contract.
|
|
12
|
+
*/
|
|
13
|
+
id: string;
|
|
14
|
+
/**
|
|
15
|
+
* Where this node sits on every axis the taxonomy declares. Features elect
|
|
16
|
+
* one axis each and read it from here; nothing composes across axes.
|
|
17
|
+
*/
|
|
18
|
+
values: AxisValues;
|
|
19
|
+
name: string;
|
|
20
|
+
/**
|
|
21
|
+
* Metadata the host owns and the library does not interpret.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately opaque: it is per-domain display detail — the things a tooltip
|
|
24
|
+
* shows — and the moment the library reads a key out of it by name, that key
|
|
25
|
+
* is part of the library's vocabulary again.
|
|
26
|
+
*/
|
|
27
|
+
payload?: Readonly<Record<string, unknown>>;
|
|
28
|
+
/**
|
|
29
|
+
* The container this node sits in, when it has one. One string: containment
|
|
30
|
+
* is a tree, and an entity that sits in two containers is modelled as two
|
|
31
|
+
* nodes sharing a `copyKey`. The view derives the containment edge from
|
|
32
|
+
* this node to its container (`withContainmentEdges`) when the host does
|
|
33
|
+
* not list one, so a host writes the fact once; a host may still list
|
|
34
|
+
* containment edges of its own, between any two nodes.
|
|
35
|
+
*/
|
|
36
|
+
parentId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* A mark the host puts on a node that is set apart: a dashed ring drawn
|
|
39
|
+
* around it. What "set apart" means is the host's — say, a node that
|
|
40
|
+
* has no container. Absent, no ring.
|
|
41
|
+
*/
|
|
42
|
+
ring?: 'dashed';
|
|
43
|
+
/**
|
|
44
|
+
* The key every copy of one underlying entity shares. One entity placed in
|
|
45
|
+
* three containers produces three nodes with three `id`s and one `copyKey`;
|
|
46
|
+
* the view draws the copies with a halo, lights them together, counts them
|
|
47
|
+
* once on a chip and tells them apart in the hover box ("copy 2 of 3").
|
|
48
|
+
* Absent, the node is its own key. Like `id`, opaque: the host decides how
|
|
49
|
+
* to derive it.
|
|
50
|
+
*/
|
|
51
|
+
copyKey?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Short texts drawn as chips above the node, as given — a cardinality, a
|
|
54
|
+
* tag, a count. Display only, never identity. Absent or empty, no chips.
|
|
55
|
+
*/
|
|
56
|
+
badges?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
/** The key a node's copies share: its `copyKey`, or its own id. */
|
|
59
|
+
export declare const copyKeyOf: (node: Pick<GraphNode, "id" | "copyKey">) => string;
|
|
60
|
+
/**
|
|
61
|
+
* Every node id by the key it shares with its copies, over a whole graph —
|
|
62
|
+
* the index the view and a host's group overlay read. Derived here from the
|
|
63
|
+
* nodes, once per graph, rather than supplied beside them.
|
|
64
|
+
*/
|
|
65
|
+
export declare function copyGroups(graph: Pick<Graph, 'nodes'>): Map<string, Set<string>>;
|
|
66
|
+
/**
|
|
67
|
+
* Read one string out of a node's host-owned payload.
|
|
68
|
+
*
|
|
69
|
+
* The library offers the accessor, never the key: a caller that knows the key
|
|
70
|
+
* is by definition domain-aware, and this keeps that knowledge at the call site
|
|
71
|
+
* instead of smuggling it into the model. Returns undefined for a missing key
|
|
72
|
+
* or a non-string value rather than coercing, so a payload shape change surfaces
|
|
73
|
+
* as an absent field instead of "[object Object]" in a tooltip.
|
|
74
|
+
*/
|
|
75
|
+
export declare function payloadString(node: {
|
|
76
|
+
payload?: Readonly<Record<string, unknown>>;
|
|
77
|
+
}, key: string): string | undefined;
|
|
78
|
+
/** Read one number out of a node's host-owned payload; undefined for a missing key or a non-number, never a coercion. */
|
|
79
|
+
export declare function payloadNumber(node: {
|
|
80
|
+
payload?: Readonly<Record<string, unknown>>;
|
|
81
|
+
}, key: string): number | undefined;
|
|
82
|
+
/** An edge of the host's graph: two node ids, a relation, the edge's own axis values and an opaque payload. */
|
|
83
|
+
export interface GraphEdge {
|
|
84
|
+
id: string;
|
|
85
|
+
sourceId: string;
|
|
86
|
+
targetId: string;
|
|
87
|
+
/**
|
|
88
|
+
* What the edge means to the engine: a `containment` edge runs from a node
|
|
89
|
+
* to its container and is drawn dashed and undirected; a `dependency` edge
|
|
90
|
+
* is directed, walked by the highlight and re-pointed when an end is
|
|
91
|
+
* hidden. Absent, `dependency`. The containment edge from a node to its
|
|
92
|
+
* `parentId` is derived; a host may list containment edges of its own,
|
|
93
|
+
* which are drawn and sprung but never walked as the parent.
|
|
94
|
+
*/
|
|
95
|
+
relation?: Relation;
|
|
96
|
+
/** Where this edge sits on the host's own axes (a call kind, say) — what an edge type's `match` reads. Absent, none. */
|
|
97
|
+
values?: AxisValues;
|
|
98
|
+
/**
|
|
99
|
+
* Metadata the host owns and the library does not interpret — the same
|
|
100
|
+
* contract as `GraphNode.payload`. Per-call-site detail that is not identity
|
|
101
|
+
* and not an axis lives here.
|
|
102
|
+
*/
|
|
103
|
+
payload?: Readonly<Record<string, unknown>>;
|
|
104
|
+
}
|
|
105
|
+
/** The edge's relation: as declared, or `dependency`. A misspelt relation is `validateGraph`'s to name. */
|
|
106
|
+
export declare function relationOf(edge: Pick<GraphEdge, 'relation'>): Relation;
|
|
107
|
+
/** Does this edge express containment (as opposed to a dependency)? */
|
|
108
|
+
export declare function isContainment(edge: Pick<GraphEdge, 'relation'>): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Every node by id — the index a reader builds once per graph when it needs
|
|
111
|
+
* lookups. A duplicated id keeps its first node; `validateGraph` names the
|
|
112
|
+
* duplicate and the view drops the later one.
|
|
113
|
+
*/
|
|
114
|
+
export declare function nodesById(nodes: Iterable<GraphNode>): Map<string, GraphNode>;
|
|
115
|
+
/**
|
|
116
|
+
* The graph with a containment edge from every node to its `parentId`, for
|
|
117
|
+
* each node whose host listed none: `in:<node id>`, from the child to its
|
|
118
|
+
* container. A node whose parent is not in the graph gets none (validation
|
|
119
|
+
* names that), and a host's own edge to the parent stands. The host's
|
|
120
|
+
* objects are not mutated; a graph needing nothing is returned as is.
|
|
121
|
+
*/
|
|
122
|
+
export declare function withContainmentEdges(graph: Graph): Graph;
|
|
123
|
+
/**
|
|
124
|
+
* A graph as a reader sees it — its nodes, its edges, and a lookup by id —
|
|
125
|
+
* the shape a computed scalar, the layout frame and the view's visible-graph
|
|
126
|
+
* derivation all read. A `Simulation` satisfies it structurally.
|
|
127
|
+
*/
|
|
128
|
+
export interface ReadableGraph {
|
|
129
|
+
readonly nodes: readonly GraphNode[];
|
|
130
|
+
readonly edges: readonly GraphEdge[];
|
|
131
|
+
getNode(id: string): GraphNode | undefined;
|
|
132
|
+
}
|
|
133
|
+
/** A host's graph as a reader sees it: its nodes and edges, and a lookup by id built once. */
|
|
134
|
+
export declare function readableGraph(graph: Graph): ReadableGraph;
|
|
135
|
+
/**
|
|
136
|
+
* The host's graph: its nodes, in the order they are laid out, and its edges.
|
|
137
|
+
* Read, never written, by the view, which indexes the nodes itself.
|
|
138
|
+
*/
|
|
139
|
+
export interface Graph {
|
|
140
|
+
readonly nodes: readonly GraphNode[];
|
|
141
|
+
readonly edges: readonly GraphEdge[];
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether two graphs say the same thing: the same nodes in the same order
|
|
145
|
+
* with the same fields, and the same edges — values compared, payloads by
|
|
146
|
+
* identity. A new graph object equal to the last is the sign of a host
|
|
147
|
+
* rebuilding what it could have kept; the view says so once.
|
|
148
|
+
*/
|
|
149
|
+
export declare function graphEquals(a: Graph, b: Graph): boolean;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ReadableGraph } from './model';
|
|
2
|
+
/** A computed scalar a layout axis may key on: a number per node, read off the graph and mapped onto [0, 1]. */
|
|
3
|
+
export interface ScalarAxis {
|
|
4
|
+
/** Stable id — the tuning names the scalar by it. */
|
|
5
|
+
readonly id: string;
|
|
6
|
+
/** The control's label. Default: the id. */
|
|
7
|
+
readonly label?: string;
|
|
8
|
+
/** The control's hover text. Default: the label. */
|
|
9
|
+
readonly tooltip?: string;
|
|
10
|
+
/**
|
|
11
|
+
* One value per node — a finite number, which the frame maps onto [0, 1]
|
|
12
|
+
* as `normalize` says: non-negative under `max`, any under `range`,
|
|
13
|
+
* already in [0, 1] under `none`. A node absent from the map has no value
|
|
14
|
+
* on this axis and rests at the axis's centre, the same answer as a node
|
|
15
|
+
* in no bucket on a mapped axis. A true 0 is a value, and the built-ins
|
|
16
|
+
* answer it as one.
|
|
17
|
+
*/
|
|
18
|
+
compute(graph: ReadableGraph): Map<string, number>;
|
|
19
|
+
/**
|
|
20
|
+
* How the frame maps the answers onto [0, 1]: `max` (the default) divides
|
|
21
|
+
* by the largest, so 0 stays 0 and a negative answer is refused, loudly;
|
|
22
|
+
* `range` puts the smallest at 0 and the largest at 1, whatever their
|
|
23
|
+
* sign; `none` says the answers already lie in [0, 1], and the frame
|
|
24
|
+
* refuses one outside it.
|
|
25
|
+
*/
|
|
26
|
+
readonly normalize?: 'max' | 'range' | 'none';
|
|
27
|
+
}
|
|
28
|
+
/** A scalar with its words filled in — what `Taxonomy.scalars` carries. */
|
|
29
|
+
export interface ResolvedScalarAxis extends ScalarAxis {
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly tooltip: string;
|
|
32
|
+
}
|
|
33
|
+
/** A declared scalar with its defaults filled in: the id as label, the label as tooltip. */
|
|
34
|
+
export declare function resolveScalarAxis(scalar: ScalarAxis): ResolvedScalarAxis;
|
|
35
|
+
/**
|
|
36
|
+
* Where a scalar's unit interval lands on an axis: the world coordinate a
|
|
37
|
+
* score of 0 rests at, and the one a score of 1 rests at. Either may be the
|
|
38
|
+
* lower — a span reads like a chart's axis and flips by dragging its ends
|
|
39
|
+
* past each other.
|
|
40
|
+
*/
|
|
41
|
+
export interface ScalarSpan {
|
|
42
|
+
readonly zero: number;
|
|
43
|
+
readonly one: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A layout axis backed by a computed scalar: which scalar, and — only
|
|
47
|
+
* once the user has dragged an end — its span. Absent, the span is the
|
|
48
|
+
* taxonomy's (`defaultSpan`), so it follows the taxonomy as a band axis's
|
|
49
|
+
* declared bands do; the tuning stores what the user decided, nothing else.
|
|
50
|
+
*/
|
|
51
|
+
export interface ScalarTuning {
|
|
52
|
+
readonly scalar: string;
|
|
53
|
+
readonly span?: ScalarSpan;
|
|
54
|
+
}
|
|
55
|
+
/** The guard on a layout tuning: scalar, or a mapping with its overrides. */
|
|
56
|
+
export declare function isScalarTuning<T extends object>(t: ScalarTuning | T): t is ScalarTuning;
|
|
57
|
+
/**
|
|
58
|
+
* Downstream reach: how many dependency levels deep a node's subtree runs,
|
|
59
|
+
* propagated up the containment hierarchy (one rank per tier, so a container
|
|
60
|
+
* strictly out-ranks everything it hosts), normalized by the top container.
|
|
61
|
+
* Leaves score 0; a graph with no dependency edges scores 0 everywhere.
|
|
62
|
+
* Topological gravity reads this over the visible subgraph; as an axis it is
|
|
63
|
+
* read over the whole graph. Either way the answer covers exactly the graph's
|
|
64
|
+
* nodes: propagation stops at an ancestor the graph does not hold.
|
|
65
|
+
*/
|
|
66
|
+
export declare function computeDownstreamReach(graph: ReadableGraph): Map<string, number>;
|
|
67
|
+
/** Dependency edges in and out, as a fraction of the busiest node's. */
|
|
68
|
+
export declare function computeDegree(graph: ReadableGraph): Map<string, number>;
|
|
69
|
+
/** Containment depth: 0 at the top of the hierarchy, 1 at the deepest node. */
|
|
70
|
+
export declare function computeContainmentDepth(graph: ReadableGraph): Map<string, number>;
|
|
71
|
+
/**
|
|
72
|
+
* Divide by the maximum so the answer spans [0, 1]; a maximum of 0 is a true
|
|
73
|
+
* 0 for every node, not an absence — the span's zero end, not the centre.
|
|
74
|
+
*/
|
|
75
|
+
export declare function normalized(values: Map<string, number>): Map<string, number>;
|
|
76
|
+
/**
|
|
77
|
+
* Map the smallest answer to 0 and the largest to 1; answers all alike are a
|
|
78
|
+
* true 0 for every node — the span's zero end, not the centre.
|
|
79
|
+
*/
|
|
80
|
+
export declare function rangeNormalized(values: Map<string, number>): Map<string, number>;
|
|
81
|
+
/** The built-in reach scalar. */
|
|
82
|
+
export declare const downstreamReach: ScalarAxis;
|
|
83
|
+
/** The built-in degree scalar. */
|
|
84
|
+
export declare const degree: ScalarAxis;
|
|
85
|
+
/** The built-in depth scalar. */
|
|
86
|
+
export declare const containmentDepth: ScalarAxis;
|
|
87
|
+
/** The scalars every host gets: structural, so they need no domain knowledge. */
|
|
88
|
+
export declare const BUILT_IN_SCALARS: readonly ScalarAxis[];
|