ngwr 14.4.0 → 14.5.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.
Files changed (149) hide show
  1. package/README.md +3 -3
  2. package/fesm2022/ngwr-breadcrumbs.mjs +2 -2
  3. package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
  4. package/fesm2022/ngwr-cascader.mjs +15 -3
  5. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  6. package/fesm2022/ngwr-color-picker.mjs +18 -2
  7. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  8. package/fesm2022/ngwr-context-menu.mjs +35 -3
  9. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  10. package/fesm2022/ngwr-date-picker.mjs +30 -2
  11. package/fesm2022/ngwr-date-picker.mjs.map +1 -1
  12. package/fesm2022/ngwr-dialog.mjs +12 -1
  13. package/fesm2022/ngwr-dialog.mjs.map +1 -1
  14. package/fesm2022/ngwr-drag-drop.mjs +2 -2
  15. package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
  16. package/fesm2022/ngwr-drawer.mjs +48 -2
  17. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  18. package/fesm2022/ngwr-dropdown.mjs +22 -2
  19. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  20. package/fesm2022/ngwr-graph-testing.mjs +243 -0
  21. package/fesm2022/ngwr-graph-testing.mjs.map +1 -0
  22. package/fesm2022/ngwr-graph.mjs +1112 -0
  23. package/fesm2022/ngwr-graph.mjs.map +1 -0
  24. package/fesm2022/ngwr-i18n-ar.mjs +5 -0
  25. package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
  26. package/fesm2022/ngwr-i18n-cs.mjs +5 -0
  27. package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
  28. package/fesm2022/ngwr-i18n-de.mjs +5 -0
  29. package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
  30. package/fesm2022/ngwr-i18n-en.mjs +9 -0
  31. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  32. package/fesm2022/ngwr-i18n-es.mjs +5 -0
  33. package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
  34. package/fesm2022/ngwr-i18n-fr.mjs +5 -0
  35. package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
  36. package/fesm2022/ngwr-i18n-he.mjs +5 -0
  37. package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
  38. package/fesm2022/ngwr-i18n-hi.mjs +5 -0
  39. package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
  40. package/fesm2022/ngwr-i18n-id.mjs +5 -0
  41. package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
  42. package/fesm2022/ngwr-i18n-it.mjs +5 -0
  43. package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
  44. package/fesm2022/ngwr-i18n-ja.mjs +5 -0
  45. package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
  46. package/fesm2022/ngwr-i18n-ko.mjs +5 -0
  47. package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
  48. package/fesm2022/ngwr-i18n-nl.mjs +5 -0
  49. package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
  50. package/fesm2022/ngwr-i18n-pl.mjs +5 -0
  51. package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
  52. package/fesm2022/ngwr-i18n-pt.mjs +5 -0
  53. package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
  54. package/fesm2022/ngwr-i18n-ru.mjs +5 -0
  55. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  56. package/fesm2022/ngwr-i18n-sv.mjs +5 -0
  57. package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
  58. package/fesm2022/ngwr-i18n-tr.mjs +5 -0
  59. package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
  60. package/fesm2022/ngwr-i18n-uk.mjs +5 -0
  61. package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
  62. package/fesm2022/ngwr-i18n-vi.mjs +5 -0
  63. package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
  64. package/fesm2022/ngwr-i18n-zh-TW.mjs +5 -0
  65. package/fesm2022/ngwr-i18n-zh-TW.mjs.map +1 -1
  66. package/fesm2022/ngwr-i18n-zh.mjs +5 -0
  67. package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
  68. package/fesm2022/ngwr-lightbox.mjs +25 -4
  69. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  70. package/fesm2022/ngwr-list.mjs +2 -2
  71. package/fesm2022/ngwr-list.mjs.map +1 -1
  72. package/fesm2022/ngwr-mention.mjs +18 -2
  73. package/fesm2022/ngwr-mention.mjs.map +1 -1
  74. package/fesm2022/ngwr-overlay.mjs +119 -4
  75. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  76. package/fesm2022/ngwr-popconfirm.mjs +19 -3
  77. package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
  78. package/fesm2022/ngwr-popover-testing.mjs +22 -5
  79. package/fesm2022/ngwr-popover-testing.mjs.map +1 -1
  80. package/fesm2022/ngwr-popover.mjs +215 -27
  81. package/fesm2022/ngwr-popover.mjs.map +1 -1
  82. package/fesm2022/ngwr-segmented.mjs +34 -1
  83. package/fesm2022/ngwr-segmented.mjs.map +1 -1
  84. package/fesm2022/ngwr-select.mjs +15 -1
  85. package/fesm2022/ngwr-select.mjs.map +1 -1
  86. package/fesm2022/ngwr-toast.mjs +19 -2
  87. package/fesm2022/ngwr-toast.mjs.map +1 -1
  88. package/fesm2022/ngwr-tour.mjs +20 -6
  89. package/fesm2022/ngwr-tour.mjs.map +1 -1
  90. package/fesm2022/ngwr-tree.mjs +9 -2
  91. package/fesm2022/ngwr-tree.mjs.map +1 -1
  92. package/fesm2022/ngwr-version.mjs +1 -1
  93. package/fesm2022/ngwr-version.mjs.map +1 -1
  94. package/fesm2022/ngwr-window.mjs +18 -1
  95. package/fesm2022/ngwr-window.mjs.map +1 -1
  96. package/graph/styles/_index.scss +113 -0
  97. package/i18n/ar.json +5 -0
  98. package/i18n/cs.json +5 -0
  99. package/i18n/de.json +5 -0
  100. package/i18n/en.json +5 -0
  101. package/i18n/es.json +5 -0
  102. package/i18n/fr.json +5 -0
  103. package/i18n/he.json +5 -0
  104. package/i18n/hi.json +5 -0
  105. package/i18n/id.json +5 -0
  106. package/i18n/it.json +5 -0
  107. package/i18n/ja.json +5 -0
  108. package/i18n/ko.json +5 -0
  109. package/i18n/nl.json +5 -0
  110. package/i18n/pl.json +5 -0
  111. package/i18n/pt.json +5 -0
  112. package/i18n/ru.json +5 -0
  113. package/i18n/sv.json +5 -0
  114. package/i18n/tr.json +5 -0
  115. package/i18n/uk.json +5 -0
  116. package/i18n/vi.json +5 -0
  117. package/i18n/zh-TW.json +5 -0
  118. package/i18n/zh.json +5 -0
  119. package/llms-full.txt +12 -2
  120. package/llms.txt +8 -6
  121. package/package.json +10 -1
  122. package/schematics/use/symbol-map.json +2 -0
  123. package/segmented/styles/_index.scss +8 -0
  124. package/skills/ngwr/SKILL.md +1 -1
  125. package/skills/ngwr/references/catalog.md +2 -0
  126. package/styles.scss +1 -0
  127. package/theme/styles/_focus.scss +7 -0
  128. package/types/ngwr-cascader.d.ts +7 -0
  129. package/types/ngwr-color-picker.d.ts +8 -0
  130. package/types/ngwr-context-menu.d.ts +10 -0
  131. package/types/ngwr-date-picker.d.ts +16 -0
  132. package/types/ngwr-dialog.d.ts +1 -0
  133. package/types/ngwr-drawer.d.ts +28 -0
  134. package/types/ngwr-dropdown.d.ts +9 -0
  135. package/types/ngwr-graph-testing.d.ts +190 -0
  136. package/types/ngwr-graph.d.ts +203 -0
  137. package/types/ngwr-lightbox.d.ts +15 -0
  138. package/types/ngwr-mention.d.ts +7 -0
  139. package/types/ngwr-overlay.d.ts +62 -4
  140. package/types/ngwr-popconfirm.d.ts +1 -0
  141. package/types/ngwr-popover-testing.d.ts +22 -5
  142. package/types/ngwr-popover.d.ts +28 -8
  143. package/types/ngwr-segmented.d.ts +16 -0
  144. package/types/ngwr-select.d.ts +7 -0
  145. package/types/ngwr-toast.d.ts +2 -0
  146. package/types/ngwr-tour.d.ts +6 -0
  147. package/types/ngwr-tree.d.ts +2 -1
  148. package/types/ngwr-version.d.ts +1 -1
  149. package/types/ngwr-window.d.ts +1 -0
@@ -0,0 +1,1112 @@
1
+ import { Directionality } from '@angular/cdk/bidi';
2
+ import { NgTemplateOutlet } from '@angular/common';
3
+ import * as i0 from '@angular/core';
4
+ import { inject, TemplateRef, Directive, input, contentChild, LOCALE_ID, computed, isDevMode, effect, ViewEncapsulation, Component } from '@angular/core';
5
+ import { useI18nText, useI18nFormatter, WrI18n } from 'ngwr/i18n';
6
+ import { numAttr } from 'ngwr/utils';
7
+
8
+ /**
9
+ * @license
10
+ *
11
+ * Use of this source code is governed by an MIT-style license that can be
12
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
13
+ */
14
+ /**
15
+ * Replace the default card. The node is the implicit context; its box — the
16
+ * size the layout reserved, its position and the relation text read to a screen
17
+ * reader — stays with the graph, so a template cannot break the geometry.
18
+ *
19
+ * Render `node.label` somewhere in it: the relation text of every neighbour
20
+ * names this node by its label.
21
+ *
22
+ * @example
23
+ * ```html
24
+ * <wr-graph [nodes]="nodes" [edges]="edges">
25
+ * <ng-template wrGraphNode let-node>
26
+ * <strong>{{ node.label }}</strong> {{ node.data.role }}
27
+ * </ng-template>
28
+ * </wr-graph>
29
+ * ```
30
+ */
31
+ class WrGraphNodeTemplate {
32
+ template = inject(TemplateRef);
33
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrGraphNodeTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
34
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: WrGraphNodeTemplate, isStandalone: true, selector: "ng-template[wrGraphNode]", ngImport: i0 });
35
+ }
36
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrGraphNodeTemplate, decorators: [{
37
+ type: Directive,
38
+ args: [{
39
+ selector: 'ng-template[wrGraphNode]',
40
+ }]
41
+ }] });
42
+
43
+ /**
44
+ * @license
45
+ *
46
+ * Use of this source code is governed by an MIT-style license that can be
47
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
48
+ */
49
+ /** Gansner's cap. Counted in sweeps, never in time, so a slow machine draws the same picture. */
50
+ const MAX_SWEEPS = 24;
51
+ const MAX_STALE_SWEEPS = 4;
52
+ /**
53
+ * Depth-first from every item in rank order, placing each item as it is first
54
+ * reached. A tree comes out with no crossings before a single sweep runs, and
55
+ * the sweeps only ever keep a strictly better order, so a tree stays that way.
56
+ */
57
+ function initialOrder(graph) {
58
+ const layers = Array.from({ length: graph.rankCount }, () => []);
59
+ const byRank = Array.from({ length: graph.rankCount }, () => []);
60
+ graph.rank.forEach((rank, item) => byRank[rank].push(item));
61
+ const visited = graph.rank.map(() => false);
62
+ for (const bucket of byRank) {
63
+ for (const start of bucket) {
64
+ const stack = [start];
65
+ for (let item = stack.pop(); item !== undefined; item = stack.pop()) {
66
+ if (visited[item])
67
+ continue;
68
+ visited[item] = true;
69
+ layers[graph.rank[item]].push(item);
70
+ // Pushed in reverse so the first child is visited first, as a recursive DFS would.
71
+ const below = graph.down[item];
72
+ for (let i = below.length - 1; i >= 0; i--)
73
+ if (!visited[below[i]])
74
+ stack.push(below[i]);
75
+ }
76
+ }
77
+ }
78
+ return layers;
79
+ }
80
+ /**
81
+ * Re-sorts one layer by the barycenter of each item's neighbours on the
82
+ * reference layer. Items with no such neighbour are lifted out BEFORE the sort
83
+ * and keep their slot: a comparator asked to rank "no barycenter" against a
84
+ * number has no consistent answer, and an inconsistent comparator sorts
85
+ * differently in different engines.
86
+ *
87
+ * Barycenters are compared exactly. They are means of integer slot indices, so
88
+ * `sumA / countA < sumB / countB` becomes `sumA * countB < sumB * countA`, which
89
+ * stays in exact integer arithmetic at any size this component draws. Ties fall
90
+ * back to the current slot, so the comparator is total.
91
+ */
92
+ function reorder(layer, neighbours, slotOf) {
93
+ const candidates = [];
94
+ layer.forEach((item, slot) => {
95
+ const adjacent = neighbours[item];
96
+ if (adjacent.length === 0)
97
+ return;
98
+ let sum = 0;
99
+ for (const other of adjacent)
100
+ sum += slotOf[other];
101
+ candidates.push({ item, sum, count: adjacent.length, slot });
102
+ });
103
+ const slots = candidates.map(candidate => candidate.slot);
104
+ candidates.sort((a, b) => a.sum * b.count - b.sum * a.count || a.slot - b.slot);
105
+ candidates.forEach((candidate, k) => {
106
+ layer[slots[k]] = candidate.item;
107
+ slotOf[candidate.item] = slots[k];
108
+ });
109
+ }
110
+ /**
111
+ * Crossings between adjacent layers, counted pairwise. Quadratic in the
112
+ * segments between one pair of layers, which at the few hundred edges this
113
+ * component is meant for is cheaper to trust than an accumulator tree.
114
+ * Two segments sharing an end never count: their product is zero.
115
+ */
116
+ function countCrossings(layers, down) {
117
+ const slotOf = down.map(() => 0);
118
+ for (const layer of layers)
119
+ layer.forEach((item, slot) => (slotOf[item] = slot));
120
+ let crossings = 0;
121
+ for (let r = 0; r + 1 < layers.length; r++) {
122
+ const top = [];
123
+ const bottom = [];
124
+ for (const item of layers[r]) {
125
+ for (const next of down[item]) {
126
+ top.push(slotOf[item]);
127
+ bottom.push(slotOf[next]);
128
+ }
129
+ }
130
+ for (let i = 0; i < top.length; i++) {
131
+ for (let j = i + 1; j < top.length; j++) {
132
+ if ((top[i] - top[j]) * (bottom[i] - bottom[j]) < 0)
133
+ crossings++;
134
+ }
135
+ }
136
+ }
137
+ return crossings;
138
+ }
139
+ /**
140
+ * Stage 5. Alternating down and up barycenter sweeps, keeping the best order
141
+ * seen. "Best" moves only on a strict improvement, so an equally good later
142
+ * order never displaces an earlier one.
143
+ */
144
+ function orderLayers(graph) {
145
+ const layers = initialOrder(graph);
146
+ const slotOf = graph.rank.map(() => 0);
147
+ for (const layer of layers)
148
+ layer.forEach((item, slot) => (slotOf[item] = slot));
149
+ let best = layers.map(layer => layer.slice());
150
+ let fewest = countCrossings(layers, graph.down);
151
+ for (let sweep = 0, stale = 0; sweep < MAX_SWEEPS && stale < MAX_STALE_SWEEPS && fewest > 0; sweep++) {
152
+ if (sweep % 2 === 0) {
153
+ for (let r = 1; r < layers.length; r++)
154
+ reorder(layers[r], graph.up, slotOf);
155
+ }
156
+ else {
157
+ for (let r = layers.length - 2; r >= 0; r--)
158
+ reorder(layers[r], graph.down, slotOf);
159
+ }
160
+ const crossings = countCrossings(layers, graph.down);
161
+ if (crossings < fewest) {
162
+ fewest = crossings;
163
+ best = layers.map(layer => layer.slice());
164
+ stale = 0;
165
+ }
166
+ else {
167
+ stale++;
168
+ }
169
+ }
170
+ return best;
171
+ }
172
+
173
+ /**
174
+ * @license
175
+ *
176
+ * Use of this source code is governed by an MIT-style license that can be
177
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
178
+ */
179
+ /** Four down passes and four up passes, alternating, ending on up so parents settle over their children. */
180
+ const COORDINATE_PASSES = 8;
181
+ function separation(extents, a, b) {
182
+ return extents.right[a] + extents.halfGap[a] + extents.halfGap[b] + extents.left[b];
183
+ }
184
+ /**
185
+ * Moves one layer toward the mean x of each item's neighbours on the reference
186
+ * layer, without reordering it and without letting two items come closer than
187
+ * their separation.
188
+ *
189
+ * Why the result never overlaps: F is built left to right as
190
+ * `F[i] = max(desired[i], F[i-1] + sep[i])`, so `F[i] - F[i-1] >= sep[i]`; B is
191
+ * built right to left as `B[i] = min(desired[i], B[i+1] - sep[i+1])`, so
192
+ * `B[i+1] - B[i] >= sep[i+1]`. Both are feasible on their own, and the average
193
+ * of two feasible placements is feasible, because
194
+ * `(F[i] + B[i]) / 2 - (F[i-1] + B[i-1]) / 2` is half of a sum of two terms that
195
+ * are each at least `sep[i]`. That holds whatever the desired positions are,
196
+ * which is why no pass needs to repair a previous one. Averaging the two is also
197
+ * what keeps a crowded layer centred on what it wants instead of piled to one
198
+ * side.
199
+ */
200
+ function align(layer, neighbours, x, extents) {
201
+ const desired = layer.map(item => {
202
+ const adjacent = neighbours[item];
203
+ if (adjacent.length === 0)
204
+ return x[item];
205
+ let sum = 0;
206
+ for (const other of adjacent)
207
+ sum += x[other];
208
+ return sum / adjacent.length;
209
+ });
210
+ const forward = desired.slice();
211
+ for (let i = 1; i < layer.length; i++) {
212
+ forward[i] = Math.max(desired[i], forward[i - 1] + separation(extents, layer[i - 1], layer[i]));
213
+ }
214
+ const backward = desired.slice();
215
+ for (let i = layer.length - 2; i >= 0; i--) {
216
+ backward[i] = Math.min(desired[i], backward[i + 1] - separation(extents, layer[i], layer[i + 1]));
217
+ }
218
+ layer.forEach((item, i) => (x[item] = (forward[i] + backward[i]) / 2));
219
+ }
220
+ /** Stage 6, horizontal: the anchor x of every item, starting packed and relaxing toward neighbours. */
221
+ function placeItems(layers, graph, extents) {
222
+ const x = graph.rank.map(() => 0);
223
+ for (const layer of layers) {
224
+ layer.forEach((item, i) => {
225
+ x[item] = i === 0 ? extents.left[item] : x[layer[i - 1]] + separation(extents, layer[i - 1], item);
226
+ });
227
+ }
228
+ for (let pass = 0; pass < COORDINATE_PASSES; pass++) {
229
+ if (pass % 2 === 0) {
230
+ for (let r = 1; r < layers.length; r++)
231
+ align(layers[r], graph.up, x, extents);
232
+ }
233
+ else {
234
+ for (let r = layers.length - 2; r >= 0; r--)
235
+ align(layers[r], graph.down, x, extents);
236
+ }
237
+ }
238
+ return x;
239
+ }
240
+ /**
241
+ * Stage 6, vertical: a layer is as tall as its tallest node, layers are
242
+ * `layerGap` apart, and every item is centred in its layer — so a short node
243
+ * beside a tall one still lines up with it. The layer's band is
244
+ * `centre ± half`, written the way a node's border is (`centre ± height / 2`),
245
+ * so the tallest node's border and the band's edge are the same number.
246
+ */
247
+ function layerCentres(layers, extents, layerGap) {
248
+ const centre = [];
249
+ const half = [];
250
+ let top = 0;
251
+ layers.forEach((layer, r) => {
252
+ let tallest = 0;
253
+ for (const item of layer)
254
+ tallest = Math.max(tallest, extents.height[item]);
255
+ if (r > 0)
256
+ top += layerGap;
257
+ half.push(tallest / 2);
258
+ centre.push(top + tallest / 2);
259
+ top += tallest;
260
+ });
261
+ return { centre, half, height: top };
262
+ }
263
+
264
+ /**
265
+ * @license
266
+ *
267
+ * Use of this source code is governed by an MIT-style license that can be
268
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
269
+ */
270
+ /**
271
+ * Stage 1. Ids become integer indices in input order and everything after this
272
+ * works on arrays: a plain object keyed by id would enumerate `'2'` before
273
+ * `'10'` whatever order the consumer gave, which is exactly the trap that makes
274
+ * one graph draw two ways.
275
+ *
276
+ * A duplicate id keeps its first occurrence and an edge naming a missing node is
277
+ * dropped. Both are the component's to warn about; the layout only has to
278
+ * survive them.
279
+ */
280
+ function prepareGraph(input) {
281
+ const index = new Map();
282
+ const nodes = [];
283
+ for (const node of input.nodes) {
284
+ if (index.has(node.id))
285
+ continue;
286
+ index.set(node.id, nodes.length);
287
+ nodes.push(node);
288
+ }
289
+ const edges = [];
290
+ const ends = [];
291
+ for (const edge of input.edges) {
292
+ const from = index.get(edge.from);
293
+ const to = index.get(edge.to);
294
+ if (from === undefined || to === undefined)
295
+ continue;
296
+ edges.push(edge);
297
+ ends.push([from, to]);
298
+ }
299
+ // Weakly connected components by BFS, started from each unassigned node in
300
+ // input order — so component numbers already follow the smallest index.
301
+ const neighbours = nodes.map(() => []);
302
+ for (const [from, to] of ends) {
303
+ neighbours[from].push(to);
304
+ neighbours[to].push(from);
305
+ }
306
+ const componentOf = nodes.map(() => -1);
307
+ const components = [];
308
+ for (let start = 0; start < nodes.length; start++) {
309
+ if (componentOf[start] !== -1)
310
+ continue;
311
+ componentOf[start] = components.length;
312
+ const queue = [start];
313
+ // `for...of` over an array that grows while it is read visits the appended nodes too.
314
+ for (const current of queue) {
315
+ for (const next of neighbours[current]) {
316
+ if (componentOf[next] !== -1)
317
+ continue;
318
+ componentOf[next] = components.length;
319
+ queue.push(next);
320
+ }
321
+ }
322
+ components.push({ nodes: [], edges: [], loops: [] });
323
+ }
324
+ const local = [];
325
+ nodes.forEach((_, node) => {
326
+ const component = components[componentOf[node]];
327
+ local.push(component.nodes.length);
328
+ component.nodes.push(node);
329
+ });
330
+ ends.forEach(([from, to], slot) => {
331
+ const component = components[componentOf[from]];
332
+ const edge = { slot, from: local[from], to: local[to] };
333
+ (from === to ? component.loops : component.edges).push(edge);
334
+ });
335
+ return { nodes, edges, components };
336
+ }
337
+
338
+ /**
339
+ * @license
340
+ *
341
+ * Use of this source code is governed by an MIT-style license that can be
342
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
343
+ */
344
+ /**
345
+ * Stage 2. Depth-first search from the sources in input order, then from
346
+ * whatever is left; an edge into the current stack closes a cycle and is
347
+ * reversed. A back edge points at an ancestor on the current path, so both of
348
+ * its ends are in one strongly connected component by construction — no SCC
349
+ * pass is needed here. The stack is explicit because a long chain would
350
+ * otherwise recurse once per node.
351
+ */
352
+ function findBackEdges(count, edges) {
353
+ const out = Array.from({ length: count }, () => []);
354
+ const indegree = new Array(count).fill(0);
355
+ edges.forEach((edge, k) => {
356
+ out[edge.from].push(k);
357
+ indegree[edge.to]++;
358
+ });
359
+ const reversed = edges.map(() => false);
360
+ const visited = new Array(count).fill(false);
361
+ const onStack = new Array(count).fill(false);
362
+ const nodes = Array.from({ length: count }, (_, node) => node);
363
+ const starts = [...nodes.filter(node => indegree[node] === 0), ...nodes.filter(node => indegree[node] !== 0)];
364
+ for (const start of starts) {
365
+ if (visited[start])
366
+ continue;
367
+ visited[start] = onStack[start] = true;
368
+ const stack = [{ node: start, next: 0 }];
369
+ while (stack.length > 0) {
370
+ const frame = stack[stack.length - 1];
371
+ if (frame.next === out[frame.node].length) {
372
+ onStack[frame.node] = false;
373
+ stack.pop();
374
+ continue;
375
+ }
376
+ const k = out[frame.node][frame.next++];
377
+ const to = edges[k].to;
378
+ if (onStack[to]) {
379
+ reversed[k] = true;
380
+ }
381
+ else if (!visited[to]) {
382
+ visited[to] = onStack[to] = true;
383
+ stack.push({ node: to, next: 0 });
384
+ }
385
+ }
386
+ }
387
+ return reversed;
388
+ }
389
+ /**
390
+ * Stage 3. Longest path from the sources: a node sits one rank below its
391
+ * deepest parent, so an org chart keeps a leaf directly under its manager
392
+ * rather than sinking every leaf to the bottom rank the way a sink-based
393
+ * longest path does. Network simplex would be more compact and is several
394
+ * times the code; this is the simplest ranking that reads well.
395
+ *
396
+ * The one tightening pass fixes the cost this ranking is known for: a root
397
+ * whose only child is deep would otherwise sit on rank 0 and draw a long edge
398
+ * through every rank in between. Only a node with no incoming edge moves, and it
399
+ * moves to just above its highest child — so no other node's rank changes and
400
+ * every rank stays occupied.
401
+ */
402
+ function assignRanks(count, from, to) {
403
+ const out = Array.from({ length: count }, () => []);
404
+ const indegree = new Array(count).fill(0);
405
+ from.forEach((node, k) => {
406
+ out[node].push(to[k]);
407
+ indegree[to[k]]++;
408
+ });
409
+ // Kahn's algorithm with a FIFO queue seeded in input order.
410
+ const rank = new Array(count).fill(0);
411
+ const remaining = indegree.slice();
412
+ const order = [];
413
+ for (let node = 0; node < count; node++)
414
+ if (indegree[node] === 0)
415
+ order.push(node);
416
+ // `for...of` over an array that grows while it is read visits the appended nodes too.
417
+ for (const node of order) {
418
+ for (const child of out[node]) {
419
+ rank[child] = Math.max(rank[child], rank[node] + 1);
420
+ if (--remaining[child] === 0)
421
+ order.push(child);
422
+ }
423
+ }
424
+ for (let i = order.length - 1; i >= 0; i--) {
425
+ const node = order[i];
426
+ if (indegree[node] !== 0 || out[node].length === 0)
427
+ continue;
428
+ let highest = rank[out[node][0]];
429
+ for (const child of out[node])
430
+ highest = Math.min(highest, rank[child]);
431
+ if (highest - 1 > rank[node])
432
+ rank[node] = highest - 1;
433
+ }
434
+ let lowest = rank[0];
435
+ for (const value of rank)
436
+ lowest = Math.min(lowest, value);
437
+ return rank.map(value => value - lowest);
438
+ }
439
+ /**
440
+ * Stages 2–4 for one connected component: break cycles, rank, and give every
441
+ * edge that spans k ranks its own chain of k − 1 zero-size dummies, so parallel
442
+ * long edges stay apart instead of sharing one.
443
+ */
444
+ function layerGraph(count, edges) {
445
+ const reversed = findBackEdges(count, edges);
446
+ const upper = edges.map((edge, k) => (reversed[k] ? edge.to : edge.from));
447
+ const lower = edges.map((edge, k) => (reversed[k] ? edge.from : edge.to));
448
+ const rank = assignRanks(count, upper, lower);
449
+ const up = Array.from({ length: count }, () => []);
450
+ const down = Array.from({ length: count }, () => []);
451
+ const layered = edges.map((edge, k) => {
452
+ const chain = [upper[k]];
453
+ for (let r = rank[upper[k]] + 1; r < rank[lower[k]]; r++) {
454
+ chain.push(rank.length);
455
+ rank.push(r);
456
+ up.push([]);
457
+ down.push([]);
458
+ }
459
+ chain.push(lower[k]);
460
+ for (let i = 1; i < chain.length; i++) {
461
+ down[chain[i - 1]].push(chain[i]);
462
+ up[chain[i]].push(chain[i - 1]);
463
+ }
464
+ return { slot: edge.slot, reversed: reversed[k], chain };
465
+ });
466
+ let rankCount = 0;
467
+ for (const value of rank)
468
+ rankCount = Math.max(rankCount, value + 1);
469
+ return { rank, rankCount, up, down, edges: layered };
470
+ }
471
+
472
+ /**
473
+ * @license
474
+ *
475
+ * Use of this source code is governed by an MIT-style license that can be
476
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
477
+ */
478
+ /** Room between a node's right border and the ends of its self-loops, where the arrowhead lands. */
479
+ const LOOP_CLEARANCE = 4;
480
+ /**
481
+ * Spreads k ports along one side of each node, ordered by where each edge
482
+ * heads next so that ports on one side never cross each other; ties go to the
483
+ * earlier edge. Without the spread, parallel edges between adjacent layers
484
+ * would draw as one line. The inset shrinks to a quarter of the width on a
485
+ * narrow node, so any node wider than zero keeps half its side for the spread
486
+ * instead of stacking every port on its centre.
487
+ */
488
+ function assignPorts(sides, geometry, portGap, inset, into) {
489
+ sides.forEach((ports, node) => {
490
+ const sorted = ports.slice().sort((a, b) => {
491
+ const ax = geometry.x[a.toward];
492
+ const bx = geometry.x[b.toward];
493
+ return ax < bx ? -1 : ax > bx ? 1 : a.edge - b.edge;
494
+ });
495
+ const room = geometry.width[node] - 2 * Math.min(inset, geometry.width[node] / 4);
496
+ const spread = Math.max(0, Math.min(room, (sorted.length - 1) * portGap));
497
+ const step = sorted.length > 1 ? spread / (sorted.length - 1) : 0;
498
+ sorted.forEach((port, j) => (into[port.edge] = geometry.x[node] - spread / 2 + j * step));
499
+ });
500
+ }
501
+ /** How far right a node's layout box grows to hold its self-loops. */
502
+ function loopReserve(loops, step) {
503
+ return loops === 0 ? 0 : LOOP_CLEARANCE + loops * step;
504
+ }
505
+ /**
506
+ * Stage 8. Out of the bottom side, through each dummy, into the top side — as
507
+ * vertical stretches inside the layer bands joined by exactly one stretch per
508
+ * inter-layer gap: straight down from the port to the bottom of its node's band,
509
+ * straight through every intermediate band at the dummy's x (a point on the
510
+ * band's top and one on its bottom), and straight down from the top of the
511
+ * child's band to its port.
512
+ *
513
+ * Why bands and not a bend at each dummy's centre: the component draws a
514
+ * stretch as a cubic whose control points sit at the stretch's middle height,
515
+ * so a stretch that ended inside a band swept sideways at card height and cut
516
+ * straight through whatever unrelated card stood between its ends. Now every
517
+ * stretch that moves sideways starts and ends on the two boundaries of one gap,
518
+ * so its y never leaves the gap, where no node lives; and every vertical one
519
+ * keeps the x of a port (inside its own node's width) or of a dummy (half an
520
+ * edge gap plus half a node gap clear of any neighbour).
521
+ *
522
+ * A reversed edge is routed the way the layout sees it and then turned around,
523
+ * so its points still start at the real parent: it leaves the parent's top and
524
+ * enters the child's bottom, which is itself the visual cue that the edge
525
+ * closes a cycle.
526
+ */
527
+ function routeEdges(graph, geometry, portGap, inset) {
528
+ const outgoing = geometry.width.map(() => []);
529
+ const incoming = geometry.width.map(() => []);
530
+ graph.edges.forEach(({ chain }, edge) => {
531
+ outgoing[chain[0]].push({ edge, toward: chain[1] });
532
+ incoming[chain[chain.length - 1]].push({ edge, toward: chain[chain.length - 2] });
533
+ });
534
+ const start = graph.edges.map(() => 0);
535
+ const end = graph.edges.map(() => 0);
536
+ assignPorts(outgoing, geometry, portGap, inset, start);
537
+ assignPorts(incoming, geometry, portGap, inset, end);
538
+ const centre = (item) => geometry.centreY[graph.rank[item]];
539
+ const half = (item) => geometry.halfBand[graph.rank[item]];
540
+ return graph.edges.map(({ chain, reversed }, edge) => {
541
+ const upper = chain[0];
542
+ const lower = chain[chain.length - 1];
543
+ const points = [];
544
+ // A node as tall as its band, or a band with no height, would repeat a point.
545
+ const add = (x, y) => {
546
+ const last = points[points.length - 1];
547
+ if (last?.x !== x || last.y !== y)
548
+ points.push({ x, y });
549
+ };
550
+ add(start[edge], centre(upper) + geometry.height[upper] / 2);
551
+ add(start[edge], centre(upper) + half(upper));
552
+ for (let i = 1; i < chain.length - 1; i++) {
553
+ add(geometry.x[chain[i]], centre(chain[i]) - half(chain[i]));
554
+ add(geometry.x[chain[i]], centre(chain[i]) + half(chain[i]));
555
+ }
556
+ add(end[edge], centre(lower) - half(lower));
557
+ add(end[edge], centre(lower) - geometry.height[lower] / 2);
558
+ return reversed ? points.reverse() : points;
559
+ });
560
+ }
561
+ /**
562
+ * Self-loops as fixed rectangles beside the node's right side, nested outward
563
+ * one `step` apart, each a little taller than the one inside it. Every point is
564
+ * strictly outside the node, and the space is already reserved in the node's
565
+ * layout box, so a loop can never run into a neighbour. The side is a layout
566
+ * side: under RTL the whole drawing mirrors, loop included.
567
+ */
568
+ function routeLoops(loops, geometry, rank, step) {
569
+ const total = geometry.width.map(() => 0);
570
+ for (const loop of loops)
571
+ total[loop.from]++;
572
+ const drawn = geometry.width.map(() => 0);
573
+ return loops.map(({ from }) => {
574
+ const nth = drawn[from]++;
575
+ const inner = geometry.x[from] + geometry.width[from] / 2 + LOOP_CLEARANCE;
576
+ const outer = inner + (nth + 1) * step;
577
+ const centre = geometry.centreY[rank[from]];
578
+ const reach = ((geometry.height[from] / 2) * (nth + 1)) / (total[from] + 1);
579
+ return [
580
+ { x: inner, y: centre - reach },
581
+ { x: outer, y: centre - reach },
582
+ { x: outer, y: centre + reach },
583
+ { x: inner, y: centre + reach },
584
+ ];
585
+ });
586
+ }
587
+
588
+ /**
589
+ * @license
590
+ *
591
+ * Use of this source code is governed by an MIT-style license that can be
592
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
593
+ */
594
+ /**
595
+ * The seam a layout engine plugs into. Used as both the type and the DI token,
596
+ * the way `WrDateAdapter` is.
597
+ *
598
+ * The return type admits a Promise from the first release, even though the
599
+ * built-in engine is synchronous: an asynchronous engine (elk runs in a worker)
600
+ * has to fit without a breaking change, and widening a synchronous return type
601
+ * later would break every caller that reads the result directly.
602
+ */
603
+ class WrGraphLayout {
604
+ }
605
+
606
+ /**
607
+ * @license
608
+ *
609
+ * Use of this source code is governed by an MIT-style license that can be
610
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
611
+ */
612
+ function drawComponent(graph, component, gaps) {
613
+ const width = component.nodes.map(node => graph.nodes[node].width);
614
+ const height = component.nodes.map(node => graph.nodes[node].height);
615
+ const loops = width.map(() => 0);
616
+ for (const loop of component.loops)
617
+ loops[loop.from]++;
618
+ const layered = layerGraph(width.length, component.edges);
619
+ const layers = orderLayers(layered);
620
+ const real = (item) => item < width.length;
621
+ const extents = {
622
+ left: layered.rank.map((_, item) => (real(item) ? width[item] / 2 : 0)),
623
+ right: layered.rank.map((_, item) => (real(item) ? width[item] / 2 + loopReserve(loops[item], gaps.edgeGap) : 0)),
624
+ halfGap: layered.rank.map((_, item) => (real(item) ? gaps.nodeGap : gaps.edgeGap) / 2),
625
+ height: layered.rank.map((_, item) => (real(item) ? height[item] : 0)),
626
+ };
627
+ const x = placeItems(layers, layered, extents);
628
+ const { centre, half, height: total } = layerCentres(layers, extents, gaps.layerGap);
629
+ const geometry = { x, centreY: centre, halfBand: half, width, height };
630
+ const edges = routeEdges(layered, geometry, gaps.edgeGap, gaps.edgeGap / 2);
631
+ const selfLoops = routeLoops(component.loops, geometry, layered.rank, gaps.edgeGap);
632
+ // Every route point lies horizontally within some item's box — ports and the
633
+ // band points straight above or below them inside a node's width, bends on
634
+ // dummies, loops inside the reserve — so the items alone bound it.
635
+ let minX = x[0] - extents.left[0];
636
+ let maxX = x[0] + extents.right[0];
637
+ x.forEach((anchor, item) => {
638
+ minX = Math.min(minX, anchor - extents.left[item]);
639
+ maxX = Math.max(maxX, anchor + extents.right[item]);
640
+ });
641
+ return {
642
+ left: width.map((w, node) => x[node] - extents.left[node]),
643
+ top: height.map((h, node) => centre[layered.rank[node]] - h / 2),
644
+ routes: [
645
+ ...layered.edges.map((edge, k) => ({ slot: edge.slot, points: edges[k] })),
646
+ ...component.loops.map((loop, k) => ({ slot: loop.slot, points: selfLoops[k] })),
647
+ ],
648
+ minX,
649
+ width: maxX - minX,
650
+ height: total,
651
+ };
652
+ }
653
+ /**
654
+ * The built-in layered layout: parents above children, pure TypeScript, no DOM
655
+ * and no dependency, so it runs the same under prerender as in the browser.
656
+ *
657
+ * It is deliberately the light version of the pipeline: DFS cycle breaking,
658
+ * source-based longest-path ranking with one tightening pass, barycenter sweeps
659
+ * with a pairwise crossing count, and averaged forward/backward compaction for
660
+ * coordinates. Network simplex and Brandes–Köpf would draw some graphs more
661
+ * compactly at several times the code.
662
+ *
663
+ * Deterministic by construction rather than by luck: ids are turned into indices
664
+ * in input order before anything else, every loop is capped by a count, every
665
+ * comparator is total with an integer tie-break, and the arithmetic is limited
666
+ * to operations the language specifies exactly — so the server and the browser
667
+ * agree to the last bit. Nothing is kept between calls and the input is never
668
+ * written to.
669
+ */
670
+ class WrLayeredGraphLayout extends WrGraphLayout {
671
+ gaps;
672
+ constructor(options = {}) {
673
+ super();
674
+ this.gaps = {
675
+ nodeGap: options.nodeGap ?? 24,
676
+ layerGap: options.layerGap ?? 48,
677
+ edgeGap: options.edgeGap ?? 12,
678
+ componentGap: options.componentGap ?? 48,
679
+ };
680
+ }
681
+ /** Synchronous — a narrowing of the base signature, so a caller holding this class needs no `await`. */
682
+ layout(input) {
683
+ const graph = prepareGraph(input);
684
+ const placed = graph.nodes.map(() => ({ x: 0, y: 0 }));
685
+ const routes = graph.edges.map(() => []);
686
+ let width = 0;
687
+ let height = 0;
688
+ // Components go in one row, top-aligned, in the order of their first node.
689
+ // `+ 0` turns a negative zero into a positive one: the two compare equal with
690
+ // `===` and unequal everywhere a result is compared structurally.
691
+ graph.components.forEach((component, c) => {
692
+ const drawing = drawComponent(graph, component, this.gaps);
693
+ const offset = c === 0 ? 0 : width + this.gaps.componentGap;
694
+ const shift = offset - drawing.minX;
695
+ component.nodes.forEach((node, local) => {
696
+ placed[node] = { x: drawing.left[local] + shift + 0, y: drawing.top[local] + 0 };
697
+ });
698
+ for (const { slot, points } of drawing.routes) {
699
+ routes[slot] = points.map(point => ({ x: point.x + shift + 0, y: point.y + 0 }));
700
+ }
701
+ width = offset + drawing.width;
702
+ height = Math.max(height, drawing.height);
703
+ });
704
+ return {
705
+ nodes: graph.nodes.map((node, i) => ({ id: node.id, x: placed[i].x, y: placed[i].y })),
706
+ edges: graph.edges.map((edge, slot) => ({ id: edge.id, points: routes[slot] })),
707
+ width: width + 0,
708
+ height: height + 0,
709
+ };
710
+ }
711
+ }
712
+
713
+ /**
714
+ * @license
715
+ *
716
+ * Use of this source code is governed by an MIT-style license that can be
717
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
718
+ */
719
+ /** What a size falls back to when neither the node nor the graph supplies a usable one. */
720
+ const DEFAULT_NODE_WIDTH = 160;
721
+ const DEFAULT_NODE_HEIGHT = 48;
722
+ /**
723
+ * The arrowhead's size in pixels, and the length of the straight lead-in under it.
724
+ * A fixed user-space size rather than a multiple of the stroke, so a thinner or
725
+ * thicker `--wr-graph-edge-width` never shrinks the arrow off its lead-in.
726
+ */
727
+ const ARROW_SIZE = 6;
728
+ /**
729
+ * Instance counter for the arrowhead marker id. Several graphs share one page
730
+ * under `ViewEncapsulation.None`, so the id has to be per instance — and
731
+ * counted rather than `randomId()`, so the prerendered id is the one the
732
+ * browser renders again.
733
+ */
734
+ let graphUid = 0;
735
+ function isSize(value) {
736
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
737
+ }
738
+ function isThenable(value) {
739
+ return typeof value.then === 'function';
740
+ }
741
+ /** Two decimals are below anything a screen can show, and keep a 5 000-bend path short. `+ 0` drops a `-0`. */
742
+ function px(value) {
743
+ return Math.round(value * 100) / 100 + 0;
744
+ }
745
+ function prepare(nodes, edges, nodeWidth, nodeHeight) {
746
+ // A `Map` built in input order, never an object keyed by id: an object lists
747
+ // index-like keys (`'2'`, `'10'`) first, so the order would depend on the ids.
748
+ const kept = new Map();
749
+ const duplicates = [];
750
+ const badSizes = [];
751
+ nodes.forEach((node, index) => {
752
+ if (kept.has(node.id)) {
753
+ duplicates.push(node.id);
754
+ return;
755
+ }
756
+ if ((node.width != null && !isSize(node.width)) || (node.height != null && !isSize(node.height))) {
757
+ badSizes.push(node.id);
758
+ }
759
+ kept.set(node.id, {
760
+ node,
761
+ index,
762
+ width: isSize(node.width) ? node.width : nodeWidth,
763
+ height: isSize(node.height) ? node.height : nodeHeight,
764
+ });
765
+ });
766
+ const keptEdges = [];
767
+ const dangling = [];
768
+ edges.forEach((edge, index) => {
769
+ if (kept.has(edge.from) && kept.has(edge.to))
770
+ keptEdges.push({ id: `e${index}`, from: edge.from, to: edge.to });
771
+ else
772
+ dangling.push(`#${index} (from "${edge.from}" to "${edge.to}")`);
773
+ });
774
+ const list = [...kept.values()];
775
+ return {
776
+ nodes: list,
777
+ edges: keptEdges,
778
+ input: { nodes: list.map(({ node, width, height }) => ({ id: node.id, width, height })), edges: keptEdges },
779
+ duplicates,
780
+ dangling,
781
+ badSizes,
782
+ };
783
+ }
784
+ /**
785
+ * Names joined the way the locale joins a list, or the way `'en'` does when the
786
+ * runtime has no list data for that locale — never the host default.
787
+ *
788
+ * `new Intl.ListFormat('zz')` does not throw: a well-formed tag the runtime
789
+ * cannot serve silently resolves to whatever locale the machine runs in, which
790
+ * is one thing on the prerendering server and another in the browser. So the
791
+ * support is asked first, and only a malformed tag reaches the `catch`
792
+ * (`supportedLocalesOf` throws a `RangeError` for it).
793
+ */
794
+ function listFormat(locale) {
795
+ try {
796
+ if (Intl.ListFormat.supportedLocalesOf(locale).length)
797
+ return new Intl.ListFormat(locale, { type: 'conjunction' });
798
+ }
799
+ catch {
800
+ // Not a valid language tag at all; handled by the fallback below.
801
+ }
802
+ return new Intl.ListFormat('en', { type: 'conjunction' });
803
+ }
804
+ function warn(message) {
805
+ // eslint-disable-next-line no-console -- dev-mode validation
806
+ console.warn(`[NGWR] <wr-graph>: ${message}`);
807
+ }
808
+ /** What a layout returned that the component cannot draw faithfully. */
809
+ function layoutProblems(graph, result) {
810
+ const problems = [];
811
+ const placed = new Set(result.nodes.map(node => node.id));
812
+ const unplaced = graph.nodes.filter(({ node }) => !placed.has(node.id)).map(({ node }) => `"${node.id}"`);
813
+ if (unplaced.length)
814
+ problems.push(`no position for ${unplaced.join(', ')}`);
815
+ const numbers = [
816
+ result.width,
817
+ result.height,
818
+ ...result.nodes.flatMap(node => [node.x, node.y]),
819
+ ...result.edges.flatMap(edge => edge.points.flatMap(point => [point.x, point.y])),
820
+ ];
821
+ if (!numbers.every(Number.isFinite))
822
+ problems.push('a coordinate or the size is not a finite number');
823
+ return problems;
824
+ }
825
+ /**
826
+ * A graph of blocks joined by parent-child links, laid out in layers from top to
827
+ * bottom. Display only: nothing is selectable, and the graph scrolls inside its
828
+ * own viewport rather than the page.
829
+ *
830
+ * The layout runs in a `computed` with no DOM and no measuring, so the sizes are
831
+ * the consumer's: `nodeWidth` / `nodeHeight` for every node, or `width` /
832
+ * `height` on one. A prerender therefore ships the finished picture.
833
+ *
834
+ * What the lines show is also written out: every node carries visually hidden
835
+ * text naming its parents and children, and the lines themselves are hidden
836
+ * from assistive technology.
837
+ *
838
+ * @example
839
+ * ```html
840
+ * <wr-graph [nodes]="nodes" [edges]="edges" ariaLabel="Team structure" />
841
+ * ```
842
+ *
843
+ * @see https://ngwr.dev/reference/components/graph
844
+ */
845
+ class WrGraph {
846
+ /**
847
+ * The nodes, in any order. Input order breaks layout ties, so the same arrays
848
+ * always produce the same picture. Never mutated.
849
+ *
850
+ * @default []
851
+ */
852
+ nodes = input([], /* @ts-ignore */
853
+ ...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
854
+ /**
855
+ * Parent-child links, `from` the parent and `to` the child. Never mutated.
856
+ *
857
+ * @default []
858
+ */
859
+ edges = input([], /* @ts-ignore */
860
+ ...(ngDevMode ? [{ debugName: "edges" }] : /* istanbul ignore next */ []));
861
+ /**
862
+ * Width in pixels of a node that sets none of its own.
863
+ *
864
+ * @default 160
865
+ */
866
+ nodeWidth = input(160, { ...(ngDevMode ? { debugName: "nodeWidth" } : /* istanbul ignore next */ {}), transform: numAttr(160) });
867
+ /**
868
+ * Height in pixels of a node that sets none of its own.
869
+ *
870
+ * @default 48
871
+ */
872
+ nodeHeight = input(48, { ...(ngDevMode ? { debugName: "nodeHeight" } : /* istanbul ignore next */ {}), transform: numAttr(48) });
873
+ /** Accessible name of the scrolling viewport. Falls back to `graph.label`, then `'Graph'`. */
874
+ ariaLabel = input(null, /* @ts-ignore */
875
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
876
+ label = useI18nText(this.ariaLabel, 'graph.label', 'Graph');
877
+ // Each value is a whole sentence, terminator included, so the punctuation is the
878
+ // catalog's to choose — `。` in Japanese, `।` in Hindi.
879
+ parentsText = useI18nFormatter('graph.parents', 'Parents: {{names}}.');
880
+ childrenText = useI18nFormatter('graph.children', 'Children: {{names}}.');
881
+ nodeTemplate = contentChild(WrGraphNodeTemplate, /* @ts-ignore */
882
+ ...(ngDevMode ? [{ debugName: "nodeTemplate" }] : /* istanbul ignore next */ []));
883
+ /** `url(#…)` for every edge's `marker-end`. */
884
+ markerId = `wr-graph-${graphUid++}-arrow`;
885
+ markerUrl = `url(#${this.markerId})`;
886
+ arrowSize = ARROW_SIZE;
887
+ engine = inject(WrGraphLayout, { optional: true }) ?? new WrLayeredGraphLayout();
888
+ // Optional on purpose: `Directionality` is root-provided, so a consumer never has
889
+ // to supply it — and a bare `TestBed` does not either.
890
+ dir = inject(Directionality, { optional: true });
891
+ i18n = inject(WrI18n, { optional: true });
892
+ localeId = inject(LOCALE_ID);
893
+ isRtl = computed(() => this.dir?.valueSignal() === 'rtl', /* @ts-ignore */
894
+ ...(ngDevMode ? [{ debugName: "isRtl" }] : /* istanbul ignore next */ []));
895
+ locale = computed(() => this.i18n?.locale() ?? this.localeId, /* @ts-ignore */
896
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
897
+ /**
898
+ * The size of a node that sets none of its own. Each dimension falls back on its
899
+ * own, and the dev warning reads this same value, so it cannot name a size the
900
+ * layout did not use.
901
+ */
902
+ defaultSize = computed(() => {
903
+ const width = this.nodeWidth();
904
+ const height = this.nodeHeight();
905
+ return {
906
+ width: isSize(width) ? width : DEFAULT_NODE_WIDTH,
907
+ height: isSize(height) ? height : DEFAULT_NODE_HEIGHT,
908
+ };
909
+ }, /* @ts-ignore */
910
+ ...(ngDevMode ? [{ debugName: "defaultSize" }] : /* istanbul ignore next */ []));
911
+ prepared = computed(() => {
912
+ const { width, height } = this.defaultSize();
913
+ return prepare(this.nodes(), this.edges(), width, height);
914
+ }, /* @ts-ignore */
915
+ ...(ngDevMode ? [{ debugName: "prepared" }] : /* istanbul ignore next */ []));
916
+ /** The engine's answer, as returned. A fresh signal per run: nothing aborts a synchronous layout. */
917
+ raw = computed(() => this.engine.layout(this.prepared().input, { signal: new AbortController().signal }), /* @ts-ignore */
918
+ ...(ngDevMode ? [{ debugName: "raw" }] : /* istanbul ignore next */ []));
919
+ /**
920
+ * The drawable result. An asynchronous layout is declared by the interface and
921
+ * not supported by this release: a thenable draws nothing, and dev mode says so.
922
+ */
923
+ result = computed(() => {
924
+ const raw = this.raw();
925
+ return isThenable(raw) ? null : raw;
926
+ }, /* @ts-ignore */
927
+ ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
928
+ /** Layout joined to the nodes and edges, direction-free, nodes in reading order. */
929
+ placed = computed(() => {
930
+ const graph = this.prepared();
931
+ const result = this.result();
932
+ if (!result)
933
+ return { width: 0, height: 0, nodes: [], edges: [] };
934
+ const positions = new Map(result.nodes.map(placement => [placement.id, placement]));
935
+ const nodes = graph.nodes.flatMap(prepared => {
936
+ const at = positions.get(prepared.node.id);
937
+ return at ? [{ ...prepared, x: at.x, y: at.y }] : [];
938
+ });
939
+ // Reading order is layer, then inline position. A layer centres nodes of
940
+ // different heights on one line, so the layer is the vertical CENTRE, taken to
941
+ // two decimals so float noise cannot split one layer in two.
942
+ const layer = (node) => Math.round((node.y + node.height / 2) * 100);
943
+ nodes.sort((a, b) => layer(a) - layer(b) || a.x - b.x || a.index - b.index);
944
+ const routes = new Map(result.edges.map(route => [route.id, route.points]));
945
+ const edges = graph.edges.flatMap(edge => {
946
+ const points = routes.get(edge.id);
947
+ return points && points.length >= 2 ? [{ edge, points }] : [];
948
+ });
949
+ return { width: result.width, height: result.height, nodes, edges };
950
+ }, /* @ts-ignore */
951
+ ...(ngDevMode ? [{ debugName: "placed" }] : /* istanbul ignore next */ []));
952
+ /**
953
+ * Physical pixels. Mirrored here rather than in CSS so nodes and lines get the
954
+ * same numbers and cannot drift apart — and kept apart from the layout so a
955
+ * direction change does not lay the graph out again.
956
+ */
957
+ view = computed(() => {
958
+ const { width, height, nodes, edges } = this.placed();
959
+ const rtl = this.isRtl();
960
+ const x = (value, size = 0) => px(rtl ? width - value - size : value);
961
+ const at = (point) => `${x(point.x)} ${px(point.y)}`;
962
+ // A route as a smooth path. Every stretch between two route points is one cubic
963
+ // Bézier with both control points at the stretch's middle height, so a line
964
+ // leaves a parent vertically and runs through a bend without a corner — a
965
+ // reversed edge too, whose stretches simply climb.
966
+ //
967
+ // The last `ARROW_SIZE` pixels are a straight lead-in. A marker takes its angle
968
+ // from the tangent at the very end, but it covers several pixels of line, and on
969
+ // a curve those already point elsewhere — the line came out of the side of the
970
+ // arrowhead. On a straight piece the triangle and the line agree. A self-loop is
971
+ // one cubic out to its reserved side with the same lead-in coming back level.
972
+ //
973
+ // Only `+`, `-`, `/`, `abs`, `min` and `max`, so the server and the browser
974
+ // write the same numbers.
975
+ const curve = (points, loop) => {
976
+ const start = points[0];
977
+ const end = points[points.length - 1];
978
+ if (loop) {
979
+ const right = Math.max(...points.map(point => point.x));
980
+ const lead = Math.min(ARROW_SIZE, (right - end.x) / 2);
981
+ const outer = x(right);
982
+ return `M${at(start)} C${outer} ${px(start.y)} ${outer} ${px(end.y)} ${x(end.x + lead)} ${px(end.y)} L${at(end)}`;
983
+ }
984
+ const stretches = points.slice(1).map((to, i) => {
985
+ const from = points[i];
986
+ const last = i === points.length - 2;
987
+ const lead = last ? Math.min(ARROW_SIZE, Math.abs(to.y - from.y) / 2) : 0;
988
+ const toY = to.y < from.y ? to.y + lead : to.y - lead;
989
+ const middle = px((from.y + toY) / 2);
990
+ const bend = `C${x(from.x)} ${middle} ${x(to.x)} ${middle} ${x(to.x)} ${px(toY)}`;
991
+ return last ? `${bend} L${at(to)}` : bend;
992
+ });
993
+ return [`M${at(start)}`, ...stretches].join(' ');
994
+ };
995
+ return {
996
+ width: px(width),
997
+ height: px(height),
998
+ nodes: nodes.map(placed => ({
999
+ node: placed.node,
1000
+ width: placed.width,
1001
+ height: placed.height,
1002
+ transform: `translate(${x(placed.x, placed.width)}px, ${px(placed.y)}px)`,
1003
+ })),
1004
+ edges: edges.map(({ edge, points }) => ({
1005
+ id: edge.id,
1006
+ from: edge.from,
1007
+ to: edge.to,
1008
+ d: curve(points, edge.from === edge.to),
1009
+ })),
1010
+ };
1011
+ }, /* @ts-ignore */
1012
+ ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
1013
+ /**
1014
+ * The visually hidden sentence per node id: its parents, then its children, as
1015
+ * name lists. Never a count — the catalogs have no plural forms. A neighbour
1016
+ * linked by several edges is named once, and names follow reading order.
1017
+ */
1018
+ relations = computed(() => {
1019
+ const { nodes } = this.placed();
1020
+ const list = listFormat(this.locale());
1021
+ const order = new Map(nodes.map((placed, i) => [placed.node.id, i]));
1022
+ const parents = new Map();
1023
+ const children = new Map();
1024
+ const link = (map, id, neighbour) => {
1025
+ const set = map.get(id) ?? new Set();
1026
+ set.add(neighbour);
1027
+ map.set(id, set);
1028
+ };
1029
+ for (const { from, to } of this.prepared().edges) {
1030
+ if (!order.has(from) || !order.has(to))
1031
+ continue;
1032
+ link(parents, to, from);
1033
+ link(children, from, to);
1034
+ }
1035
+ const names = (ids) => {
1036
+ if (!ids?.size)
1037
+ return null;
1038
+ const sorted = [...ids].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
1039
+ return list.format(sorted.map(id => nodes[order.get(id) ?? 0].node.label));
1040
+ };
1041
+ const text = new Map();
1042
+ for (const { node } of nodes) {
1043
+ const sentences = [];
1044
+ const parentNames = names(parents.get(node.id));
1045
+ const childNames = names(children.get(node.id));
1046
+ if (parentNames)
1047
+ sentences.push(this.parentsText({ names: parentNames }));
1048
+ if (childNames)
1049
+ sentences.push(this.childrenText({ names: childNames }));
1050
+ // Every mark of punctuation comes from the catalog. The one character the
1051
+ // component adds is the space between two sentences — a word boundary to a
1052
+ // screen reader, and silent in a script that sets none.
1053
+ if (sentences.length)
1054
+ text.set(node.id, sentences.join(' '));
1055
+ }
1056
+ return text;
1057
+ }, /* @ts-ignore */
1058
+ ...(ngDevMode ? [{ debugName: "relations" }] : /* istanbul ignore next */ []));
1059
+ constructor() {
1060
+ if (isDevMode()) {
1061
+ effect(() => {
1062
+ const graph = this.prepared();
1063
+ const raw = this.raw();
1064
+ if (graph.duplicates.length) {
1065
+ warn(`node id(s) ${graph.duplicates.map(id => `"${id}"`).join(', ')} appear more than once. ` +
1066
+ `The first occurrence of each is drawn and the rest are skipped.`);
1067
+ }
1068
+ if (graph.dangling.length) {
1069
+ warn(`edge(s) ${graph.dangling.join(', ')} name a node that does not exist and were skipped.`);
1070
+ }
1071
+ if (graph.badSizes.length) {
1072
+ warn(`node(s) ${graph.badSizes.map(id => `"${id}"`).join(', ')} have a width or height that is not a ` +
1073
+ `positive finite number, so nodeWidth / nodeHeight is used instead.`);
1074
+ }
1075
+ const width = this.nodeWidth();
1076
+ const height = this.nodeHeight();
1077
+ const invalid = [
1078
+ ...(isSize(width) ? [] : [`nodeWidth (${String(width)})`]),
1079
+ ...(isSize(height) ? [] : [`nodeHeight (${String(height)})`]),
1080
+ ];
1081
+ if (invalid.length) {
1082
+ const used = this.defaultSize();
1083
+ warn(`${invalid.join(' and ')} must be a positive finite number, so a node with no size of its own is ` +
1084
+ `drawn ${used.width} × ${used.height}.`);
1085
+ }
1086
+ if (isThenable(raw)) {
1087
+ warn('the provided WrGraphLayout returned a Promise. Asynchronous layouts are not supported in this ' +
1088
+ 'release, so nothing is drawn.');
1089
+ return;
1090
+ }
1091
+ const problems = layoutProblems(graph, raw);
1092
+ if (problems.length)
1093
+ warn(`the layout result cannot be drawn faithfully: ${problems.join('; ')}.`);
1094
+ });
1095
+ }
1096
+ }
1097
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrGraph, deps: [], target: i0.ɵɵFactoryTarget.Component });
1098
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrGraph, isStandalone: true, selector: "wr-graph", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: true, isRequired: false, transformFunction: null }, nodeWidth: { classPropertyName: "nodeWidth", publicName: "nodeWidth", isSignal: true, isRequired: false, transformFunction: null }, nodeHeight: { classPropertyName: "nodeHeight", publicName: "nodeHeight", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "wr-graph" }, queries: [{ propertyName: "nodeTemplate", first: true, predicate: WrGraphNodeTemplate, descendants: true, isSignal: true }], ngImport: i0, template: "@let graph = view();\n<!-- The scroller is the one tab stop: a named group, so its label is allowed, and focusable so a keyboard can scroll it. -->\n<div class=\"wr-graph__viewport\" role=\"group\" tabindex=\"0\" [attr.aria-label]=\"label()\">\n <div class=\"wr-graph__stage\" [style.width.px]=\"graph.width\" [style.height.px]=\"graph.height\">\n <svg\n class=\"wr-graph__edges\"\n aria-hidden=\"true\"\n focusable=\"false\"\n [attr.width]=\"graph.width\"\n [attr.height]=\"graph.height\"\n >\n <defs>\n <marker\n [attr.id]=\"markerId\"\n viewBox=\"0 0 10 10\"\n refX=\"10\"\n refY=\"5\"\n markerUnits=\"userSpaceOnUse\"\n [attr.markerWidth]=\"arrowSize\"\n [attr.markerHeight]=\"arrowSize\"\n orient=\"auto\"\n >\n <polygon points=\"0,1 10,5 0,9\" />\n </marker>\n </defs>\n @for (edge of graph.edges; track edge.id) {\n <path\n class=\"wr-graph__edge\"\n [attr.d]=\"edge.d\"\n [attr.data-from]=\"edge.from\"\n [attr.data-to]=\"edge.to\"\n [attr.marker-end]=\"markerUrl\"\n />\n }\n </svg>\n <!-- Items are DIRECT children of the list: a wrapper carrying ARIA or a tabindex would break ownership. -->\n <div class=\"wr-graph__nodes\" [attr.role]=\"graph.nodes.length ? 'list' : null\">\n @for (entry of graph.nodes; track entry.node.id) {\n @let relation = relations().get(entry.node.id);\n <div\n class=\"wr-graph__node\"\n role=\"listitem\"\n [style.width.px]=\"entry.width\"\n [style.height.px]=\"entry.height\"\n [style.transform]=\"entry.transform\"\n >\n @if (nodeTemplate(); as custom) {\n <ng-container [ngTemplateOutlet]=\"custom.template\" [ngTemplateOutletContext]=\"{ $implicit: entry.node }\" />\n } @else {\n <div class=\"wr-graph__card\">{{ entry.node.label }}</div>\n }\n @if (relation) {\n <span class=\"wr-graph__sr-only\">{{ relation }}</span>\n }\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], encapsulation: i0.ViewEncapsulation.None });
1099
+ }
1100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrGraph, decorators: [{
1101
+ type: Component,
1102
+ args: [{ selector: 'wr-graph', imports: [NgTemplateOutlet], encapsulation: ViewEncapsulation.None, host: {
1103
+ class: 'wr-graph',
1104
+ }, template: "@let graph = view();\n<!-- The scroller is the one tab stop: a named group, so its label is allowed, and focusable so a keyboard can scroll it. -->\n<div class=\"wr-graph__viewport\" role=\"group\" tabindex=\"0\" [attr.aria-label]=\"label()\">\n <div class=\"wr-graph__stage\" [style.width.px]=\"graph.width\" [style.height.px]=\"graph.height\">\n <svg\n class=\"wr-graph__edges\"\n aria-hidden=\"true\"\n focusable=\"false\"\n [attr.width]=\"graph.width\"\n [attr.height]=\"graph.height\"\n >\n <defs>\n <marker\n [attr.id]=\"markerId\"\n viewBox=\"0 0 10 10\"\n refX=\"10\"\n refY=\"5\"\n markerUnits=\"userSpaceOnUse\"\n [attr.markerWidth]=\"arrowSize\"\n [attr.markerHeight]=\"arrowSize\"\n orient=\"auto\"\n >\n <polygon points=\"0,1 10,5 0,9\" />\n </marker>\n </defs>\n @for (edge of graph.edges; track edge.id) {\n <path\n class=\"wr-graph__edge\"\n [attr.d]=\"edge.d\"\n [attr.data-from]=\"edge.from\"\n [attr.data-to]=\"edge.to\"\n [attr.marker-end]=\"markerUrl\"\n />\n }\n </svg>\n <!-- Items are DIRECT children of the list: a wrapper carrying ARIA or a tabindex would break ownership. -->\n <div class=\"wr-graph__nodes\" [attr.role]=\"graph.nodes.length ? 'list' : null\">\n @for (entry of graph.nodes; track entry.node.id) {\n @let relation = relations().get(entry.node.id);\n <div\n class=\"wr-graph__node\"\n role=\"listitem\"\n [style.width.px]=\"entry.width\"\n [style.height.px]=\"entry.height\"\n [style.transform]=\"entry.transform\"\n >\n @if (nodeTemplate(); as custom) {\n <ng-container [ngTemplateOutlet]=\"custom.template\" [ngTemplateOutletContext]=\"{ $implicit: entry.node }\" />\n } @else {\n <div class=\"wr-graph__card\">{{ entry.node.label }}</div>\n }\n @if (relation) {\n <span class=\"wr-graph__sr-only\">{{ relation }}</span>\n }\n </div>\n }\n </div>\n </div>\n</div>\n" }]
1105
+ }], ctorParameters: () => [], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], edges: [{ type: i0.Input, args: [{ isSignal: true, alias: "edges", required: false }] }], nodeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeWidth", required: false }] }], nodeHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeHeight", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], nodeTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => WrGraphNodeTemplate), { isSignal: true }] }] } });
1106
+
1107
+ /**
1108
+ * Generated bundle index. Do not edit.
1109
+ */
1110
+
1111
+ export { WrGraph, WrGraphNodeTemplate };
1112
+ //# sourceMappingURL=ngwr-graph.mjs.map