@jmbarzee/force-graph 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,8 @@ their values on those axes, and mounts `GraphView`. The view lays the graph out
9
9
  pull on any axis the nodes carry, two layout axes keyed on an axis or a
10
10
  computed scalar), filters it on any declared axis, highlights it, and lets the
11
11
  user tune the forces — without knowing what any node or edge means. The host's
12
- vocabulary never enters this package.
12
+ vocabulary never enters this package. What the user sees and does is
13
+ [SPEC.md](SPEC.md).
13
14
 
14
15
  ## Install
15
16
 
@@ -143,7 +144,7 @@ never by spreading the built object.
143
144
  and no icon elsewhere, the abbreviation from the label, no tooltip),
144
145
  `initialSelection(values)` is the first open's selection (default every
145
146
  value on an allow-list, none on a narrowing axis), `presentation` is
146
- `chip` or `tag`.
147
+ `chip` or `tag`, and `reads: 'tags'` makes it a tag axis (see The graph).
147
148
  - **Scalars and hooks.** `scalars` adds computed axes (`id`, `label` and
148
149
  `tooltip` — defaults the id and the label — and `compute(graph)` answering a
149
150
  number per node; `normalize` maps
@@ -179,7 +180,14 @@ engine compares them and never parses one.
179
180
  the parent, and
180
181
  `validateGraph` names one that contradicts `parentId`. A host adds axes of
181
182
  its own to edges and nodes alike through `values`; any axis the nodes
182
- carry can be pushed, pulled and laid out on.
183
+ carry a value on can be pushed, pulled and laid out on.
184
+ - **Tags.** A node's `tags` map an axis to a list — its assignees, its
185
+ labels — read by a filter axis declared `reads: 'tags'`: a node passes it
186
+ when any of its tags does, and each tag's chip counts the node once. A tag
187
+ axis only filters — push, pull, a layout axis and colour never key on it —
188
+ and `validateGraph` warns of tags on any other axis. `valuesOn` and
189
+ `countsOn` read a tag axis when handed its descriptor
190
+ (`taxonomy.axisFor(id)`).
183
191
  - **Adornments.** A node may carry `badges` (short texts drawn as chips above
184
192
  it) and a `ring` (`dashed`, for a node set apart).
185
193
  - **Identity.** Keep one `graph` object until something in it changes: a new
@@ -295,15 +303,14 @@ the console. Everything else is optional.
295
303
  refused; with `classifyEdge` given, its answer stands. Derived containment
296
304
  edges are `in:<id>` (numbered on a collision) and reach `highlight` and
297
305
  `collapse`. An error diagnostic drops the edge, or the later node of a
298
- duplicated id, or clears the `parentId`, it names. The search dropdown lists visible matches; hidden ones badge the
299
- chip hiding them. The pane's theme observer watches every `data-theme` in
300
- the document unless `systemTheme` is given.
306
+ duplicated id, or clears the `parentId`, it names. The pane's theme
307
+ observer watches every `data-theme` in the document unless `systemTheme` is
308
+ given.
301
309
  - **Keyboard.** `keyboard` scopes the shortcuts: `pane` (the default — the
302
310
  focusable root handles the keys that reach it, so a page keeps Tab and
303
311
  Space for its own controls), `global` (the window — a page that is the
304
- pane, or a tab shell's active tab) or `none` (a hidden tab). Tab cycles
305
- nodes, Enter selects, Escape closes, arrows pan, `+`/`-` zoom, F fits, `/`
306
- opens search (while the search is offered), Space pauses, `?` lists them.
312
+ pane, or a tab shell's active tab) or `none` (a hidden tab). The keys are
313
+ [SPEC § Keyboard](SPEC.md#keyboard).
307
314
 
308
315
  ## Two views sharing a bar
309
316
 
@@ -342,9 +349,8 @@ last `linked.focus` asked of it with `onFocusResult` (a `FilterBar` ignores
342
349
  those two); it throws for a view the hook was not given (`views` is read
343
350
  once). `linked.active` is the view shown.
344
351
  `linked.switchTo(to, transition?)` switches from the active view and makes
345
- `to` active: a manual switch (the default) adopts the active view's
346
- selections except where locked, a focus widens the destination to show its
347
- target. `linked.focus(view, node)` is the focus switch and the request in one.
352
+ `to` active, reconciling as [SPEC § Linked views](SPEC.md#linked-views) says
353
+ (a manual switch is the default). `linked.focus(view, node)` is the focus switch and the request in one.
348
354
  `linked.updateFilter(view, fn)` rewrites one view's filter and
349
355
  `linked.updateFilters(fn)` every view's (a stale value pruned when the data
350
356
  changes — `reseedFilter` is the rule the hook itself applies to every view
@@ -353,17 +359,19 @@ when `graph` changes, on the axes the user has not touched); `linked.state`,
353
359
  for a host that persists them. The bar carries its own theme and scheme
354
360
  (`theme`, default `system`; `systemTheme` says what that means), so it needs
355
361
  no wrapper; give it the `graph` (or the `values` per axis — `valuesOn(records,
356
- axis)` reads them off any subjects carrying `values`) and, if the host reads
362
+ axis)` reads them off any subjects carrying `values`, or their `tags` given the
363
+ axis's descriptor) and, if the host reads
357
364
  its own data, the `counts`. The lock toggles appear only when `locks` is
358
365
  controlled with its callback — `useLinkedViews` does — or `chrome.locks` asks.
359
366
  The bar tracks the values that just turned on for its chip flash
360
367
  (`RecentlyChanged`, a set per axis); a host with its own rule passes
361
- `recentlyChanged`.
368
+ `recentlyChanged`. `refreshFlash` flashes the bar each time the host bumps
369
+ it.
362
370
 
363
371
  What the pane computes for its own bar is exported for the second view:
364
372
  `passesSelection({ filter, taxonomy, axes, node })` decides visibility on the
365
373
  axes the view shows (`valueOn(node, axis)` for a record that carries its
366
- values under other names), `countsOn(nodes, axes)` the badges,
374
+ values or tags under other names), `countsOn(nodes, axes)` the badges,
367
375
  `hiddenMatchesOf({ nodes, filter, taxonomy, axes, visibleIds, matches })` the
368
376
  search's hidden-match badges. `reconcileFilter({ destination, source, locks,
369
377
  transition, taxonomy, axes, graph })` is the switch the hook makes; `axes`
@@ -381,14 +389,17 @@ The pane resolves its theme from its own choice (Auto / Light / Dark) or, for
381
389
  `system`, from `systemTheme` when the host gives it, else the nearest
382
390
  ancestor's `data-theme` and then the OS — `useHostTheme(ref)` is that same
383
391
  read, for a host's own chrome, and `themeChoiceOf(value)` reads a stored
384
- choice back (`system` for anything else). Every
392
+ choice back (`system` for anything else). A server render knows neither
393
+ ancestors nor OS, so a server-rendered pane paints light until it hydrates; a
394
+ host that wants no flash resolves the theme on the server (a cookie, say) and
395
+ passes `systemTheme`. Every
385
396
  token the components read is a `--gv-*` custom property declared in the
386
397
  stylesheet per theme (`--gv-color-bg`, `--gv-color-text`, `--gv-accent`,
387
398
  `--gv-font-sans`, …), and every class is `gv-*`; a host overrides a token by
388
399
  redeclaring it on an ancestor. Each style value's fill and border are
389
400
  published on the pane under the name `taxonomy.colorVar(value)` answers —
390
- `--gv-node-` and the value id in kebab case (`nexusEndpoint` →
391
- `--gv-node-nexus-endpoint`), or a numbered name when the id kebab-cases to
401
+ `--gv-node-` and the value id in kebab case (a value id such as
402
+ `teachingLab` → `--gv-node-teaching-lab`), or a numbered name when the id kebab-cases to
392
403
  nothing or to another value's — and `-border` for the border. A host's own
393
404
  chrome beside the pane reads the same tints through `schemeVars(taxonomy,
394
405
  theme)` inline, or `schemeStylesheet(taxonomy, darkSelector?)` mounted once
@@ -410,15 +421,46 @@ draw in lanes.
410
421
  ## Limits
411
422
 
412
423
  The charge force is O(n²) over the visible nodes: a few hundred draw
413
- smoothly, a few thousand do not. The push/pull and layout controls bound their
414
- tokens by a ceiling (an axis with more values than the ceiling is offered but
415
- cannot be tuned on, and a tuning that outgrows it returns to the style axis).
416
- What is not built is filed as issues: colour by another axis
417
- ([#9](https://github.com/jmbarzee/force-graph/issues/9)), an opaque `Taxonomy`
418
- ([#10](https://github.com/jmbarzee/force-graph/issues/10)), undirected edges
419
- ([#11](https://github.com/jmbarzee/force-graph/issues/11)), per-edge weight
420
- ([#12](https://github.com/jmbarzee/force-graph/issues/12)), the pane's strings
421
- in other languages ([#13](https://github.com/jmbarzee/force-graph/issues/13)),
422
- the React 19 types gate ([#14](https://github.com/jmbarzee/force-graph/issues/14)).
424
+ smoothly, a few thousand do not (Barnes-Hut,
425
+ [#30](https://github.com/jmbarzee/force-graph/issues/30)). The push/pull and
426
+ layout controls bound their tokens by a ceiling
427
+ ([SPEC § The control panel](SPEC.md#the-control-panel)).
428
+ What is not built is filed as issues:
429
+
430
+ - **The graph and taxonomy** — colour by another axis
431
+ ([#9](https://github.com/jmbarzee/force-graph/issues/9)), an opaque
432
+ `Taxonomy` ([#10](https://github.com/jmbarzee/force-graph/issues/10)),
433
+ undirected edges ([#11](https://github.com/jmbarzee/force-graph/issues/11)),
434
+ per-edge weight ([#12](https://github.com/jmbarzee/force-graph/issues/12)),
435
+ an edge time series played back
436
+ ([#33](https://github.com/jmbarzee/force-graph/issues/33)), host-declared
437
+ pair-axis mappings ([#32](https://github.com/jmbarzee/force-graph/issues/32)),
438
+ a node drawn as a stack of ghosts
439
+ ([#19](https://github.com/jmbarzee/force-graph/issues/19)).
440
+ - **Groups** — structural colouring, a metric channel and a cohesion pull
441
+ ([#27](https://github.com/jmbarzee/force-graph/issues/27)); hiding through
442
+ the legend, one emphasis path, colours validated on arrival
443
+ ([#17](https://github.com/jmbarzee/force-graph/issues/17)).
444
+ - **Interaction** — selecting several nodes
445
+ ([#28](https://github.com/jmbarzee/force-graph/issues/28)), edge hover and
446
+ its outputs ([#16](https://github.com/jmbarzee/force-graph/issues/16)), a
447
+ focus that frames the neighbourhood
448
+ ([#21](https://github.com/jmbarzee/force-graph/issues/21)), a neighbourhood
449
+ frame that leaves hidden neighbours out
450
+ ([#26](https://github.com/jmbarzee/force-graph/issues/26)), the keyboard and
451
+ accessibility model ([#7](https://github.com/jmbarzee/force-graph/issues/7)),
452
+ positions, viewport and selection kept across a live reload
453
+ ([#5](https://github.com/jmbarzee/force-graph/issues/5)), animated reveals
454
+ ([#8](https://github.com/jmbarzee/force-graph/issues/8)), an empty state
455
+ when the filter hides every node
456
+ ([#6](https://github.com/jmbarzee/force-graph/issues/6)).
457
+ - **The control panel** — named presets
458
+ ([#29](https://github.com/jmbarzee/force-graph/issues/29)), grouping a ladder
459
+ of values and the Misc tab's read-outs
460
+ ([#31](https://github.com/jmbarzee/force-graph/issues/31)), a force tab with
461
+ nothing visible ([#4](https://github.com/jmbarzee/force-graph/issues/4)).
462
+ - **The package** — the pane's strings in other languages
463
+ ([#13](https://github.com/jmbarzee/force-graph/issues/13)), the React 19
464
+ types gate ([#14](https://github.com/jmbarzee/force-graph/issues/14)).
423
465
 
424
466
  `src/index.ts` names the whole surface.
@@ -80,6 +80,18 @@ interface FilterBarBaseProps {
80
80
  /** Flash the whole bar once whenever this value changes to something truthy — a counter a host bumps when its data reloads. */
81
81
  refreshFlash?: boolean | number;
82
82
  }
83
+ /** The first line of a row's items, laid out at these offsets from its top: its height, and how many items wrap past it. */
84
+ export declare function firstLine(items: readonly {
85
+ top: number;
86
+ bottom: number;
87
+ }[]): {
88
+ height: number;
89
+ rest: number;
90
+ };
91
+ /** The axes a change of filter opens: those whose selection changed, but for the ones it leaves as they are. */
92
+ export declare function axesOpenedBy(prev: FilterState, next: FilterState, leftAsIs: readonly AxisId[]): AxisId[];
93
+ /** The axes a filter re-seeded: their presented values changed between two readings, and the filter selects their seed. */
94
+ export declare function axesReseeded(taxonomy: Taxonomy, prev: ReadonlyMap<string, readonly string[]>, next: ReadonlyMap<string, readonly string[]>, filter: FilterState): AxisId[];
83
95
  /**
84
96
  * The shared filter bar: a section of chips per axis shown with its lock and
85
97
  * remove controls, the add-axis control, the search control, and the host's
@@ -18,9 +18,12 @@ export declare function themeOfMarker(marker: ThemeMarker | null): Theme | undef
18
18
  export declare function hostTheme(el: Element | null): Theme;
19
19
  /**
20
20
  * The host's theme for the element at `ref`, re-read when any ancestor's
21
- * `data-theme` changes and when the OS preference changes. With `enabled`
22
- * false nothing is read or observed, and the answer is light.
21
+ * `data-theme` changes and when the OS preference changes. Light until read,
22
+ * the server's answer, so a hydrating render matches it; with `enabled`
23
+ * false nothing is read or observed, and the answer stays light.
23
24
  */
24
25
  export declare function useHostTheme(ref: React.RefObject<HTMLElement | null>, enabled?: boolean): Theme;
25
- /** The resolved theme for the pane rooted at `ref`, given the user's choice and, for `system`, what the host says it means — else what its ancestors and the OS say. */
26
+ /** The pane's resolved theme, undefined until the host is read. */
27
+ export declare function useResolvedTheme(ref: React.RefObject<HTMLElement | null>, choice: ThemeChoice, systemTheme?: Theme): Theme | undefined;
28
+ /** The theme the pane rooted at `ref` draws in: the resolved theme, light until read. */
26
29
  export declare function useTheme(ref: React.RefObject<HTMLElement | null>, choice: ThemeChoice, systemTheme?: Theme): Theme;
@@ -1,4 +1,4 @@
1
- import { AxisId, AxisValues, ResolvedAxisDescriptor } from '../graph/axis';
1
+ import { AxisId, AxisValue, ResolvedAxisDescriptor } from '../graph/axis';
2
2
  import { Graph } from '../graph/model';
3
3
  import { Taxonomy } from '../graph/taxonomy';
4
4
  /** The chosen values on one axis. Empty means what the axis's descriptor says. */
@@ -45,13 +45,14 @@ export declare function withLock(locks: AxisLocks, axis: AxisId, locked: boolean
45
45
  /** Whether an axis is locked against adoption. */
46
46
  export declare function lockedFor(locks: AxisLocks, axis: AxisId): boolean;
47
47
  /**
48
- * Where a focus transition wants to land: the target's value per axis. An
49
- * axis the target says nothing about is absent, which the reconciler reads
50
- * as "no expansion needed". A host that also needs the target's name for a
51
- * view of its own carries it in a type of its own.
48
+ * Where a focus transition wants to land: the target's value per axis, and
49
+ * its tags per tag axis. An axis the target says nothing about is absent,
50
+ * which the reconciler reads as "no expansion needed". A host that also needs
51
+ * the target's name for a view of its own carries it in a type of its own.
52
52
  */
53
53
  export type FocusTarget = {
54
54
  values: Readonly<Record<AxisId, string | undefined>>;
55
+ tags?: Readonly<Record<AxisId, readonly string[] | undefined>>;
55
56
  };
56
57
  /** How a view is being switched to: by the user (`manual`: adopt the source's selections) or to show a node (`focus`: widen for its values). */
57
58
  export type ViewTransition = {
@@ -75,6 +76,8 @@ export type ReconcileResult = {
75
76
  * choice.
76
77
  */
77
78
  export declare function initialFilter(taxonomy: Pick<Taxonomy, 'filterAxes'>, graph: Pick<Graph, 'nodes'>): FilterState;
79
+ /** One axis's opening selection on the values the graph presents there: `initialFilter`'s, axis by axis. */
80
+ export declare function seedSelection(taxonomy: Pick<Taxonomy, 'filterAxes'>, d: ResolvedAxisDescriptor, values: readonly string[]): AxisSelection;
78
81
  /**
79
82
  * A filter re-seeded for a graph that changed: every filter axis the user has
80
83
  * not touched takes its opening selection on `graph` (`initialFilter`'s), so
@@ -113,8 +116,8 @@ export interface PassesSelectionInput<N> {
113
116
  axes: readonly AxisId[];
114
117
  /** The record judged: a graph node, or any record of the host's. */
115
118
  node: N;
116
- /** The record's value on an axis, for a record that carries its values under other names. Default: its `values`, when it has them. */
117
- valueOn?: (node: N, axis: AxisId) => string | undefined;
119
+ /** The record's value on an axis — its tags, on a tag axis — for a record that carries them under other names. Default: its `values` and `tags`, when it has them. */
120
+ valueOn?: (node: N, axis: AxisId) => AxisValue | readonly AxisValue[] | undefined;
118
121
  }
119
122
  /**
120
123
  * Does a record pass the selection on the axes a view shows? The policy —
@@ -124,20 +127,19 @@ export interface PassesSelectionInput<N> {
124
127
  */
125
128
  export declare function passesSelection<N>({ filter, taxonomy, axes, node, valueOn }: PassesSelectionInput<N>): boolean;
126
129
  /** `passesSelection` over descriptors already resolved — the view's hot path, which resolves them once per render. */
127
- export declare function passesDescriptors(filter: FilterState, axes: readonly ResolvedAxisDescriptor[], valueOn: (axis: string) => string | undefined): boolean;
130
+ export declare function passesDescriptors(filter: FilterState, axes: readonly ResolvedAxisDescriptor[], valuesOf: (axis: ResolvedAxisDescriptor) => readonly AxisValue[]): boolean;
128
131
  /** Search matches split by the filter: the visible ones, and the hidden ones tallied on the chip hiding each. */
129
132
  export interface HiddenMatches {
130
133
  /** The ids of the matching nodes the filter shows. */
131
134
  visible: ReadonlySet<string>;
132
135
  /** How many matching nodes the filter hides. */
133
136
  hiddenCount: number;
134
- /** The hidden matches per axis and value — on the first of the axes shown that each node fails, under its value there. */
137
+ /** The hidden matches per axis and value — on the first of the axes shown that each node fails, under its value there (on a tag axis, its first tag whose chip is off). */
135
138
  hiddenByAxis: ReadonlyMap<AxisId, ReadonlyMap<string, number>>;
136
139
  }
137
140
  /** What `hiddenMatchesOf` reads. */
138
141
  export interface HiddenMatchesInput<N extends {
139
142
  id: string;
140
- values?: AxisValues;
141
143
  }> {
142
144
  /** Every node the search ran over, visible or not. */
143
145
  nodes: Iterable<N>;
@@ -147,16 +149,16 @@ export interface HiddenMatchesInput<N extends {
147
149
  axes: readonly AxisId[];
148
150
  visibleIds: ReadonlySet<string>;
149
151
  matches: (node: N) => boolean;
150
- /** A node's value on an axis. Default: its `values`. */
151
- valueOn?: (node: N, axis: AxisId) => string | undefined;
152
+ /** A node's value on an axis — its tags, on a tag axis. Default: its `values` and `tags`. */
153
+ valueOn?: (node: N, axis: AxisId) => AxisValue | readonly AxisValue[] | undefined;
152
154
  }
153
155
  /**
154
156
  * Split a search's matches by the filter, the way the pane does under its
155
157
  * own bar: a match the filter shows lights up, a match it hides is counted
156
158
  * on the chip hiding it — the first axis in `axes` the node fails, under the
157
- * node's value there — so a host's second bar can wear the same badges.
159
+ * node's value there, or its first tag whose chip is off — so a host's second
160
+ * bar can wear the same badges.
158
161
  */
159
162
  export declare function hiddenMatchesOf<N extends {
160
163
  id: string;
161
- values?: AxisValues;
162
164
  }>({ nodes, filter, taxonomy, axes, visibleIds, matches, valueOn }: HiddenMatchesInput<N>): HiddenMatches;
@@ -7,6 +7,17 @@ export type AxisId = string;
7
7
  export type AxisValue = string;
8
8
  /** Where a node sits on every axis it has a value for. */
9
9
  export type AxisValues = Readonly<Record<AxisId, AxisValue>>;
10
+ /** The tags a node carries, on each tag axis it has any on. */
11
+ export type AxisTags = Readonly<Record<AxisId, readonly AxisValue[]>>;
12
+ /** How an axis is read off a node: its one value, or its tags. */
13
+ export type AxisReading = Pick<ResolvedAxisDescriptor, 'id' | 'reads'>;
14
+ /** What a node, a focus target or a host's record carries: its values and its tags, either possibly absent. */
15
+ export interface AxisSubject {
16
+ values?: Readonly<Record<AxisId, AxisValue | undefined>>;
17
+ tags?: Readonly<Record<AxisId, readonly AxisValue[] | undefined>>;
18
+ }
19
+ /** What a subject carries on an axis: its tags on a tag axis, else its one value; none when it carries nothing there. */
20
+ export declare function readAxis(subject: AxisSubject, axis: AxisReading): readonly AxisValue[];
10
21
  /**
11
22
  * Fold anything into a stable value key.
12
23
  *
@@ -136,6 +147,13 @@ export interface AxisDescriptor {
136
147
  * `emptyMeans: 'all'` axis.
137
148
  */
138
149
  presentation?: 'chip' | 'tag';
150
+ /**
151
+ * What a node carries on this axis: its one value in `values` (the
152
+ * default), or a list in `tags` — its assignees, its labels. A node passes
153
+ * a tag axis when any of its tags does. A tag axis only filters: push,
154
+ * pull, a layout axis and colour never key on it.
155
+ */
156
+ reads?: 'values' | 'tags';
139
157
  }
140
158
  /** A filter axis with every policy filled in — what `Taxonomy.filterAxes` carries. */
141
159
  export interface ResolvedAxisDescriptor extends AxisNamer {
@@ -151,6 +169,7 @@ export interface ResolvedAxisDescriptor extends AxisNamer {
151
169
  chipsFor(values: readonly AxisValue[]): readonly FilterChip[];
152
170
  initialSelection(values: readonly AxisValue[]): readonly AxisValue[];
153
171
  presentation: 'chip' | 'tag';
172
+ reads: 'values' | 'tags';
154
173
  }
155
174
  /** How the style axis names and adorns its values, for a descriptor's defaults. */
156
175
  export interface StyleNamer {
@@ -231,23 +250,22 @@ export declare function isIdentityMapping(mapping: AxisMapping): boolean;
231
250
  * The distinct values a set of subjects carries on one axis, in first-encounter
232
251
  * order. The domain of an axis is whatever the graph presents on it — the
233
252
  * 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.
253
+ * surface or a filter learns what an axis can take. An axis given by id reads
254
+ * values; given by its descriptor, a tag axis reads tags.
235
255
  */
236
- export declare function valuesOn(subjects: Iterable<{
237
- values: AxisValues;
238
- }>, axis: AxisId): AxisValue[];
256
+ export declare function valuesOn(subjects: Iterable<AxisSubject>, axis: AxisId | AxisReading): AxisValue[];
239
257
  /**
240
258
  * How many entities carry each value on each of the given axes — the chip
241
259
  * 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.
260
+ * one entity (`copyKey`) count once, and so does each of an entity's tags; a
261
+ * subject with neither key nor id counts as itself. An axis is read as
262
+ * `valuesOn` reads it, and one no subject carries anything on gets an empty
263
+ * map, never a missing one.
245
264
  */
246
- export declare function countsOn(subjects: Iterable<{
247
- values: AxisValues;
265
+ export declare function countsOn(subjects: Iterable<AxisSubject & {
248
266
  id?: string;
249
267
  copyKey?: string;
250
- }>, axes: readonly AxisId[]): ReadonlyMap<AxisId, ReadonlyMap<AxisValue, number>>;
268
+ }>, axes: readonly (AxisId | AxisReading)[]): ReadonlyMap<AxisId, ReadonlyMap<AxisValue, number>>;
251
269
  /**
252
270
  * Does this table declare `key` itself, rather than inheriting it?
253
271
  *
@@ -1,5 +1,5 @@
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. */
1
+ import { AxisTags, 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, tags and payload. */
3
3
  export interface GraphNode {
4
4
  /**
5
5
  * Stable identity, owned by whoever produced the graph.
@@ -16,6 +16,11 @@ export interface GraphNode {
16
16
  * one axis each and read it from here; nothing composes across axes.
17
17
  */
18
18
  values: AxisValues;
19
+ /**
20
+ * Several values on one axis — its assignees, its labels — on each axis
21
+ * the taxonomy declares to read tags. Read by the filter only.
22
+ */
23
+ tags?: AxisTags;
19
24
  name: string;
20
25
  /**
21
26
  * Metadata the host owns and the library does not interpret.
@@ -29,10 +29,10 @@ export type ContextLine = ReactNode | readonly ReactNode[];
29
29
  * policy and host hook through. Built once by `createTaxonomy`; derive a
30
30
  * variant from the spec, never by spreading this object.
31
31
  *
32
- * What a host reads: `filterAxes`, `axisFor` and `valueOn` to filter its own
33
- * records the way the view does; `styleOf`, `styleValues` and `colorVar` for
34
- * chrome of its own in the scheme's colours; `edgeTypes` for a legend;
35
- * `searchText` to match as the view matches. The rest is the engine's
32
+ * What a host reads: `filterAxes` and `axisFor` to filter its own records the
33
+ * way the view does, through `passesSelection`; `styleOf`, `styleValues` and
34
+ * `colorVar` for chrome of its own in the scheme's colours; `edgeTypes` for a
35
+ * legend; `searchText` to match as the view matches. The rest is the engine's
36
36
  * interface — resolution for the draw loop — and a host has no call to read
37
37
  * it.
38
38
  */
@@ -63,13 +63,6 @@ export interface Taxonomy {
63
63
  readonly filterAxes: readonly ResolvedAxisDescriptor[];
64
64
  /** One axis's policies, or undefined if the host does not declare it. */
65
65
  axisFor(axis: AxisId): ResolvedAxisDescriptor | undefined;
66
- /**
67
- * The value a subject presents on one axis — read off its values, for
68
- * every axis alike. An axis that is a projection of another is materialized
69
- * by the host when it builds the node, so nothing here knows which axes are
70
- * derived.
71
- */
72
- valueOn(subject: StyleSubject, axis: AxisId): AxisValue | undefined;
73
66
  /** Every style value, in declaration order (top of the hierarchy first). */
74
67
  readonly styleValues: readonly AxisValue[];
75
68
  /** Every edge type, in control-panel order, its defaults filled in. */
@@ -205,3 +198,5 @@ export interface TaxonomySpec {
205
198
  * fields.
206
199
  */
207
200
  export declare function createTaxonomy(spec: TaxonomySpec): Taxonomy;
201
+ /** The axes a taxonomy declares to read tags, which only filter. */
202
+ export declare function tagAxesOf(taxonomy: Pick<Taxonomy, 'filterAxes'>): AxisId[];
@@ -5,11 +5,12 @@ import { Taxonomy } from './taxonomy';
5
5
  * an edge whose endpoint is not in the graph, an unknown relation, a type
6
6
  * whose relation is not the edge's, a rule that threw, a parent not in the
7
7
  * graph. Warnings: an edge no type claims, a style value the taxonomy does
8
- * not declare, containment edges that contradict `parentId`, derived
8
+ * not declare, tags on an axis the taxonomy does not declare to read them,
9
+ * containment edges that contradict `parentId`, derived
9
10
  * containment edges no type claims (`containment-untyped`, from
10
11
  * `derivedContainmentDiagnostics`).
11
12
  */
12
- export type GraphDiagnosticCode = 'edge-id-duplicate' | 'edge-endpoint-missing' | 'edge-relation-unknown' | 'edge-type-rule-threw' | 'edge-type-unclassified' | 'edge-type-relation-mismatch' | 'parent-missing' | 'containment-parent-mismatch' | 'node-value-undeclared' | 'node-id-duplicate' | 'containment-untyped';
13
+ export type GraphDiagnosticCode = 'edge-id-duplicate' | 'edge-endpoint-missing' | 'edge-relation-unknown' | 'edge-type-rule-threw' | 'edge-type-unclassified' | 'edge-type-relation-mismatch' | 'parent-missing' | 'containment-parent-mismatch' | 'node-value-undeclared' | 'node-tags-undeclared' | 'node-id-duplicate' | 'containment-untyped';
13
14
  /** One defect in a host's graph, as data: its severity, its code, a message, and the node or edge it names. */
14
15
  export interface GraphDiagnostic {
15
16
  /** An error names something the view cannot draw as given; a warning, something it draws but the host should know. */
@@ -24,9 +25,9 @@ export interface GraphDiagnostic {
24
25
  * draw as given (a duplicated node id, a dangling endpoint, an unknown
25
26
  * relation, a type whose relation is not the edge's, a parent not in the
26
27
  * graph) and warnings for what it draws but the host should know (an
27
- * unclassified edge, an undeclared style value, containment edges that
28
- * contradict `parentId`). Pure; the same graph and taxonomy give the same
29
- * list, in graph order.
28
+ * unclassified edge, an undeclared style value, tags no axis reads,
29
+ * containment edges that contradict `parentId`). Pure; the same graph and
30
+ * taxonomy give the same list, in graph order.
30
31
  */
31
32
  export declare function validateGraph(graph: Graph, taxonomy: Taxonomy): GraphDiagnostic[];
32
33
  /**
@@ -18,7 +18,7 @@ export { toggleGroup, toggleValue } from './filter/toggle';
18
18
  export { initialFilter, filterFromJSON, filterStatesEqual, filterToJSON, hiddenMatchesOf, locksFromJSON, locksToJSON, matchesFocus, passesSelection, reseedFilter, lockedFor, selectionFor, withLock, withSelection } from './filter/types';
19
19
  export type { FilterState, FocusTarget, HiddenMatches, HiddenMatchesInput, AxisLocks, PassesSelectionInput, ReconcileResult, AxisSelection, ViewTransition } from './filter/types';
20
20
  export { countsOn, valuesOn } from './graph/axis';
21
- export type { AxisDescriptor, AxisId, AxisNamer, AxisValues, AxisValue, FilterChip, Relation, ResolvedAxisDescriptor } from './graph/axis';
21
+ export type { AxisDescriptor, AxisId, AxisNamer, AxisTags, AxisValues, AxisValue, FilterChip, Relation, ResolvedAxisDescriptor } from './graph/axis';
22
22
  export type { ActiveGroup, NodeGroup } from './graph/groups';
23
23
  export { copyGroups, isContainment, nodesById, payloadNumber, payloadString } from './graph/model';
24
24
  export type { Graph, GraphEdge, GraphNode, ReadableGraph } from './graph/model';