@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.
@@ -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[];