@yuneta/gobj-ui 7.23.156 → 7.23.157

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.
@@ -6,9 +6,7 @@
6
6
  * tidy tree's rows and centring, the radial tree's rings.
7
7
  ***********************************************************************/
8
8
  import { describe, test, expect } from "vitest";
9
- import {
10
- spanning_tree, layout_tree, layout_radial, layout_compact, stack_trunk_width, STACK_GAP,
11
- } from "./treedb_layout.js";
9
+ import { spanning_tree, layout_tree, layout_radial } from "./treedb_layout.js";
12
10
 
13
11
  /* es -> (norte, sur); norte -> nave; nave -> d0, d1 (hook rank 1) and
14
12
  * c1 (hook rank 2); c1 -> d0 as well (second parent). */
@@ -99,177 +97,6 @@ describe("the tidy tree, read down (the default)", () => {
99
97
  });
100
98
  });
101
99
 
102
- /* Every two cards of one row stand at least `gap` apart. */
103
- function expect_no_overlap_in_rows(list, pos, gap)
104
- {
105
- for(let i = 0; i < list.length; i++) {
106
- for(let j = i + 1; j < list.length; j++) {
107
- let a = list[i];
108
- let b = list[j];
109
- let pa = pos.get(a.id);
110
- let pb = pos.get(b.id);
111
- if(Math.abs(pa.y - pb.y) > 1e-6) {
112
- continue;
113
- }
114
- expect(Math.abs(pa.x - pb.x) + 1e-6).toBeGreaterThanOrEqual((a.w + b.w) / 2 + gap);
115
- }
116
- }
117
- }
118
-
119
- function width_of(list, pos)
120
- {
121
- let lo = Infinity;
122
- let hi = -Infinity;
123
- for(let n of list) {
124
- let p = pos.get(n.id);
125
- lo = Math.min(lo, p.x - n.w / 2);
126
- hi = Math.max(hi, p.x + n.w / 2);
127
- }
128
- return hi - lo;
129
- }
130
-
131
- /* r -> a, b; a -> hall -> six devices; b is a closed sibling. */
132
- const wide = [N("r", 172, 96), N("a", 172, 96), N("b", 172, 96), N("hall", 172, 96)]
133
- .concat([0, 1, 2, 3, 4, 5].map((i) => N(`d${i}`, 116, 40)));
134
- const wide_edges = [
135
- {source: "r", target: "a"}, {source: "r", target: "b"}, {source: "a", target: "hall"},
136
- ].concat([0, 1, 2, 3, 4, 5].map((i) => ({source: "hall", target: `d${i}`})));
137
-
138
- describe("the compact tree", () => {
139
- test("the rows are the tidy tree's rows; a stack starts on its row", () => {
140
- let tidy = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
141
- let pos = layout_compact(nodes, edges, {nodesep: 10, ranksep: 100});
142
- /* nave's three leaves (d0, d1, c1) are a stack: d0 and d1 are
143
- * its first row, on the tidy row; c1 opens the next one. */
144
- for(let id of ["es", "norte", "sur", "nave", "d0", "d1"]) {
145
- expect(pos.get(id).y).toBe(tidy.get(id).y);
146
- }
147
- expect(pos.get("c1").y).toBeGreaterThan(pos.get("d0").y);
148
- });
149
-
150
- test("no two cards of a row are closer than the gap", () => {
151
- expect_no_overlap_in_rows(nodes, layout_compact(nodes, edges, {nodesep: 10, ranksep: 100}), 10);
152
- expect_no_overlap_in_rows(wide, layout_compact(wide, wide_edges, {nodesep: 18, ranksep: 90}), 18);
153
- });
154
-
155
- test("two leaves stay side by side, and the parent is centred over them", () => {
156
- let two = [N("p", 172, 96), N("a", 116, 40), N("b", 116, 40)];
157
- let pos = layout_compact(two, [{source: "p", target: "a"}, {source: "p", target: "b"}],
158
- {nodesep: 10, ranksep: 100});
159
- expect(pos.get("a").y).toBe(pos.get("b").y);
160
- expect(pos.get("b").x - pos.get("a").x).toBe(116 + 10);
161
- expect(pos.get("p").x).toBeCloseTo((pos.get("a").x + pos.get("b").x) / 2, 6);
162
- });
163
-
164
- test("a closed sibling sits beside an open branch, not beside its block", () => {
165
- let opts = {nodesep: 18, ranksep: 90};
166
- let tidy = layout_tree(wide, wide_edges, opts);
167
- let pos = layout_compact(wide, wide_edges, opts);
168
- /* In the tidy tree `b` waits for the whole fan of devices; here it
169
- * only has to clear `a`, the card beside it. */
170
- expect(pos.get("b").x - pos.get("a").x).toBeLessThan(tidy.get("b").x - tidy.get("a").x);
171
- expect(pos.get("b").x - pos.get("a").x).toBeGreaterThanOrEqual(172 + 18 - 1e-6);
172
- expect(width_of(wide, pos)).toBeLessThanOrEqual(width_of(wide, tidy));
173
- });
174
-
175
- test("two trees sit side by side, not overlapped", () => {
176
- let pos = layout_compact([N("a", 100, 50), N("b", 100, 50)], [], {nodesep: 10});
177
- expect(pos.get("b").x - pos.get("a").x).toBe(100 + 20);
178
- });
179
-
180
- test("read right, it is the same tree with the axes swapped", () => {
181
- let down = layout_compact(wide, wide_edges, {nodesep: 18, ranksep: 90});
182
- let swapped = wide.map((n) => N(n.id, n.h, n.w));
183
- let right = layout_compact(swapped, wide_edges, {nodesep: 18, ranksep: 90, direction: "LR"});
184
- for(let n of wide) {
185
- expect(right.get(n.id).x).toBeCloseTo(down.get(n.id).y, 6);
186
- expect(right.get(n.id).y).toBeCloseTo(down.get(n.id).x, 6);
187
- }
188
- });
189
-
190
- test("a tree as deep as the data places every node, without recursion", () => {
191
- let chain = [];
192
- let links = [];
193
- for(let i = 0; i < 5000; i++) {
194
- chain.push(N(`n${i}`, 40, 20));
195
- if(i > 0) {
196
- links.push({source: `n${i - 1}`, target: `n${i}`});
197
- }
198
- }
199
- let pos = layout_compact(chain, links, {});
200
- expect(pos.size).toBe(5000);
201
- expect(Number.isFinite(pos.get("n4999").x)).toBe(true);
202
- });
203
- });
204
-
205
- describe("the compact tree stacks a run of leaves", () => {
206
- const P6 = N("p", 172, 96);
207
- const L6 = [0, 1, 2, 3, 4, 5].map((i) => N(`l${i}`, 116, 40));
208
- const E6 = L6.map((l) => ({source: "p", target: l.id}));
209
- const OPTS = {nodesep: 18, ranksep: 90};
210
-
211
- test("in two columns, row after row, against a corridor", () => {
212
- let pos = layout_compact([P6, ...L6], E6, OPTS);
213
- let y = (id) => pos.get(id).y;
214
- let x = (id) => pos.get(id).x;
215
- expect(y("l0")).toBe(y("l1"));
216
- expect(y("l2")).toBe(y("l3"));
217
- expect(y("l2") - y("l0")).toBe(40 + STACK_GAP);
218
- expect(y("l4") - y("l2")).toBe(40 + STACK_GAP);
219
- /* The first column lines up on its right edge, the second on
220
- * its left, and between them the corridor. */
221
- expect(x("l0")).toBe(x("l2"));
222
- expect(x("l1")).toBe(x("l5"));
223
- expect((x("l1") - 58) - (x("l0") + 58)).toBe(stack_trunk_width(6));
224
- /* The parent over the block. */
225
- expect(x("p")).toBeCloseTo(((x("l0") - 58) + (x("l1") + 58)) / 2, 6);
226
- });
227
-
228
- test("no two cards overlap", () => {
229
- let all = [P6, ...L6];
230
- let pos = layout_compact(all, E6, OPTS);
231
- for(let i = 0; i < all.length; i++) {
232
- for(let j = i + 1; j < all.length; j++) {
233
- let a = all[i];
234
- let b = all[j];
235
- let pa = pos.get(a.id);
236
- let pb = pos.get(b.id);
237
- let apart_x = Math.abs(pa.x - pb.x) >= (a.w + b.w) / 2;
238
- let apart_y = Math.abs(pa.y - pb.y) >= (a.h + b.h) / 2;
239
- expect(apart_x || apart_y).toBe(true);
240
- }
241
- }
242
- });
243
-
244
- test("a whole level of leaves takes a fraction of the row it took", () => {
245
- let tidy = layout_tree([P6, ...L6], E6, OPTS);
246
- let pos = layout_compact([P6, ...L6], E6, OPTS);
247
- let span = (p) => {
248
- let xs = L6.map((l) => p.get(l.id).x);
249
- return Math.max(...xs) - Math.min(...xs) + 116;
250
- };
251
- expect(span(pos)).toBeLessThan(span(tidy) / 2);
252
- });
253
-
254
- test("a run is broken by a child with children, and the order is kept", () => {
255
- /* p -> a, b, c (leaves), m (has a child), d, e (leaves). */
256
- let list = [N("p", 172, 96), N("a", 116, 40), N("b", 116, 40), N("c", 116, 40),
257
- N("m", 172, 96), N("d", 116, 40), N("e", 116, 40), N("mk", 116, 40)];
258
- let links = ["a", "b", "c", "m", "d", "e"].map((k) => ({source: "p", target: k}))
259
- .concat([{source: "m", target: "mk"}]);
260
- let pos = layout_compact(list, links, OPTS);
261
- /* a, b, c: a stack (c under a); d, e: a pair on the row. */
262
- expect(pos.get("c").y).toBeGreaterThan(pos.get("a").y);
263
- expect(pos.get("c").x).toBe(pos.get("a").x);
264
- expect(pos.get("d").y).toBe(pos.get("a").y);
265
- expect(pos.get("e").y).toBe(pos.get("a").y);
266
- /* Left to right: the stack, m, then d and e. */
267
- expect(pos.get("b").x).toBeLessThan(pos.get("m").x);
268
- expect(pos.get("m").x).toBeLessThan(pos.get("d").x);
269
- expect(pos.get("d").x).toBeLessThan(pos.get("e").x);
270
- });
271
- });
272
-
273
100
  describe("the tidy tree, read right", () => {
274
101
  test("depths are columns; a column is as wide as its widest card", () => {
275
102
  let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100, direction: "LR"});
@@ -325,7 +152,7 @@ describe("the tidy tree, read right", () => {
325
152
 
326
153
  describe("the radial tree", () => {
327
154
  test("one root sits at the centre, its children on a ring around it", () => {
328
- let pos = layout_radial(nodes, edges, {nodesep: 10, ranksep: 100, stack: false});
155
+ let pos = layout_radial(nodes, edges, {nodesep: 10, ranksep: 100});
329
156
  expect(pos.get("es")).toEqual({x: 0, y: 0});
330
157
  let r = Math.hypot(pos.get("norte").x, pos.get("norte").y);
331
158
  expect(r).toBeGreaterThanOrEqual(100);
@@ -346,7 +173,7 @@ describe("the radial tree", () => {
346
173
  many.push(N(`k${i}`, 172, 96));
347
174
  e.push({source: "root", target: `k${i}`, rank: 0});
348
175
  }
349
- let pos = layout_radial(many, e, {nodesep: 10, ranksep: 100, stack: false});
176
+ let pos = layout_radial(many, e, {nodesep: 10, ranksep: 100});
350
177
  let r = Math.hypot(pos.get("k0").x, pos.get("k0").y);
351
178
  /* 40 diagonals of ~197 plus gaps on the circumference */
352
179
  expect(2 * Math.PI * r).toBeGreaterThanOrEqual(40 * (Math.hypot(172, 96) + 10) - 1e-6);
@@ -380,99 +207,6 @@ describe("the radial tree", () => {
380
207
  });
381
208
  });
382
209
 
383
- /* No two cards of `list` overlap (axis-aligned boxes). */
384
- function expect_no_card_overlap(list, pos)
385
- {
386
- for(let i = 0; i < list.length; i++) {
387
- for(let j = i + 1; j < list.length; j++) {
388
- let a = list[i];
389
- let b = list[j];
390
- let pa = pos.get(a.id);
391
- let pb = pos.get(b.id);
392
- let apart_x = Math.abs(pa.x - pb.x) >= (a.w + b.w) / 2 - 1e-6;
393
- let apart_y = Math.abs(pa.y - pb.y) >= (a.h + b.h) / 2 - 1e-6;
394
- expect(apart_x || apart_y, `${a.id} and ${b.id} overlap`).toBe(true);
395
- }
396
- }
397
- }
398
-
399
- /* A treedb shaped like the central one: a root, 25 regions with a
400
- * few places each, and one hall with 24 devices. */
401
- function central_like()
402
- {
403
- let list = [N("es", 172, 96)];
404
- let links = [];
405
- let seed = 7;
406
- let rnd = () => {
407
- seed = (seed * 1103515245 + 12345) & 0x7fffffff;
408
- return seed / 0x7fffffff;
409
- };
410
- for(let p = 0; p < 25; p++) {
411
- list.push(N(`p${p}`, 172, 96));
412
- links.push({source: "es", target: `p${p}`});
413
- let n = 3 + Math.floor(rnd() * 5);
414
- for(let s = 0; s < n; s++) {
415
- list.push(N(`p${p}s${s}`, 172, 96));
416
- links.push({source: `p${p}`, target: `p${p}s${s}`});
417
- if(p === 3 && s === 1) {
418
- for(let d = 0; d < 24; d++) {
419
- list.push(N(`p${p}s${s}d${d}`, 116, 40));
420
- links.push({source: `p${p}s${s}`, target: `p${p}s${s}d${d}`});
421
- }
422
- }
423
- }
424
- }
425
- return {list, links};
426
- }
427
-
428
- describe("the radial tree stacks a run of leaves", () => {
429
- const HUB = [N("hub", 172, 96)].concat([...Array(24).keys()].map((i) => N(`d${i}`, 116, 40)));
430
- const SPOKES = HUB.slice(1).map((n) => ({source: "hub", target: n.id}));
431
- const OPTS = {nodesep: 24, ranksep: 180};
432
- const reach = (list, pos) => Math.max(...list.map((n) => Math.hypot(pos.get(n.id).x, pos.get(n.id).y)));
433
-
434
- test("a hub with 24 leaves: rows outward instead of one wide ring", () => {
435
- let flat = layout_radial(HUB, SPOKES, Object.assign({stack: false}, OPTS));
436
- let pos = layout_radial(HUB, SPOKES, OPTS);
437
- /* Small cards wholly round their parent: each row makes room
438
- * for a card at its worst slant, so the gain is least here --
439
- * smaller all the same, and nothing overlaps. */
440
- expect(reach(HUB, pos)).toBeLessThan(reach(HUB, flat));
441
- expect_no_card_overlap(HUB, pos);
442
- });
443
-
444
- test("big cards round a parent, alone on their ring: no two overlap", () => {
445
- /* 25 regions under a root, cards 172x96: rings round it, where
446
- * a card stands at every angle. */
447
- let list = [N("es", 172, 96)].concat([...Array(25).keys()].map((i) => N(`p${i}`, 172, 96)));
448
- let links = list.slice(1).map((n) => ({source: "es", target: n.id}));
449
- expect_no_card_overlap(list, layout_radial(list, links, OPTS));
450
- });
451
-
452
- test("the rows of a stack go outward, filled in order", () => {
453
- let pos = layout_radial(HUB, SPOKES, OPTS);
454
- let r = (id) => Math.hypot(pos.get(id).x, pos.get(id).y);
455
- for(let i = 1; i < 24; i++) {
456
- expect(r(`d${i}`)).toBeGreaterThanOrEqual(r(`d${i - 1}`) - 1e-6);
457
- }
458
- expect(r("d23")).toBeGreaterThan(r("d0"));
459
- });
460
-
461
- test("a treedb shaped like a real one: no two cards overlap, and it is smaller", () => {
462
- let {list, links} = central_like();
463
- let flat = layout_radial(list, links, Object.assign({stack: false}, OPTS));
464
- let pos = layout_radial(list, links, OPTS);
465
- expect_no_card_overlap(list, pos);
466
- expect(reach(list, pos)).toBeLessThan(reach(list, flat));
467
- });
468
-
469
- test("without leaves in runs it is the radial it was", () => {
470
- let few = [N("r", 172, 96), N("a", 172, 96), N("b", 172, 96)];
471
- let e2 = [{source: "r", target: "a"}, {source: "r", target: "b"}];
472
- expect(layout_radial(few, e2, OPTS)).toEqual(layout_radial(few, e2, Object.assign({stack: false}, OPTS)));
473
- });
474
- });
475
-
476
210
  describe("a deep tree", () => {
477
211
  /* A self-referent hook -- a place inside a place inside a place --
478
212
  * is as deep as the data says. The walks are iterative for exactly