@yuneta/gobj-ui 7.23.153 → 7.23.155

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.
@@ -291,6 +291,14 @@ function tidy_tree_lr(nodes, edges, o)
291
291
  * the packing across the depths differs, so switching between the
292
292
  * two moves cards sideways and never reorders them.
293
293
  *
294
+ * And a run of three or more LEAF children -- a hall's devices, a
295
+ * region's places when none is open -- is STACKED: two columns
296
+ * under the parent with a corridor between them, row after row.
297
+ * That is what makes it compact when a whole level is open, where
298
+ * the contour alone gains nothing: every parent needed the whole
299
+ * row of its children beside it. The tree grows taller instead of
300
+ * wider. Its elbow edges run down the corridor (elbow_combs).
301
+ *
294
302
  * The contour arithmetic is Moen's ("Drawing dynamic trees", IEEE
295
303
  * Software, 1990) in the form mxGraph gave it in mxCompactTreeLayout
296
304
  * -- Copyright (c) 2006-2015 JGraph Ltd, Apache License 2.0 -- read in
@@ -397,12 +405,125 @@ function contour_merge(p1, p2)
397
405
  return total;
398
406
  }
399
407
 
408
+ /* How the compact tree STACKS a run of leaf children: two columns
409
+ * under their parent, a CORRIDOR between them that the elbow edges
410
+ * run down (treedb_elbow.js, elbow_combs), a gap between the rows
411
+ * that they turn into. The corridor is sized here from how many
412
+ * lines will run down it; the edges count the lanes from where the
413
+ * cards stand. */
414
+ export const STACK_MIN_RUN = 3; /* shorter runs stay in the row */
415
+ export const STACK_GAP = 40; /* between two rows of a stack */
416
+ export const STACK_LANE_PAD = 10; /* from a column to its first lane */
417
+ export const STACK_LANE_STEP = 6; /* between two lanes */
418
+ export const STACK_TRUNK_MIN = 36;
419
+
420
+ /* The corridor of a stack of `k` cards: every card below the first
421
+ * row gets a lane. */
422
+ export function stack_trunk_width(k)
423
+ {
424
+ let lanes = Math.max(0, k - 2);
425
+ return Math.max(STACK_TRUNK_MIN,
426
+ 2 * STACK_LANE_PAD + Math.max(0, lanes - 1) * STACK_LANE_STEP);
427
+ }
428
+
429
+ /* A stack in the LR frame (depth along x, siblings along y): `side`
430
+ * is how far each column reaches across, `starts`/`row_len` where
431
+ * each row begins and how long it is along the depth. Its first row
432
+ * IS the depth's column, so it lines up with every other card there. */
433
+ function stack_geometry(t, st, col_x, col_w)
434
+ {
435
+ let d = st.depth;
436
+ let k = st.ids.length;
437
+ let rows = Math.ceil(k / 2);
438
+ let side = [0, 0];
439
+ let row_len = [];
440
+ for(let i = 0; i < k; i++) {
441
+ let n = t.by_id.get(st.ids[i]);
442
+ let r = Math.floor(i / 2);
443
+ side[i % 2] = Math.max(side[i % 2], n.h);
444
+ row_len[r] = Math.max(row_len[r] || 0, n.w);
445
+ }
446
+ row_len[0] = col_w[d];
447
+ let trunk = stack_trunk_width(k);
448
+ let starts = [col_x[d]];
449
+ for(let r = 1; r < rows; r++) {
450
+ starts[r] = starts[r - 1] + row_len[r - 1] + STACK_GAP;
451
+ }
452
+ return {
453
+ side: side,
454
+ trunk: trunk,
455
+ starts: starts,
456
+ row_len: row_len,
457
+ len: starts[rows - 1] + row_len[rows - 1] - col_x[d],
458
+ ext: side[0] + trunk + side[1],
459
+ };
460
+ }
461
+
462
+ /* The cards of a stack whose block starts across at `top`: row by
463
+ * row, the first of each row against the corridor from one side,
464
+ * the second against it from the other. */
465
+ function place_stack(t, st, g, top, pos)
466
+ {
467
+ for(let i = 0; i < st.ids.length; i++) {
468
+ let n = t.by_id.get(st.ids[i]);
469
+ let r = Math.floor(i / 2);
470
+ let across = (i % 2 === 0)
471
+ ? top + g.side[0] - n.h / 2
472
+ : top + g.side[0] + g.trunk + n.h / 2;
473
+ pos.set(st.ids[i], {x: g.starts[r] + g.row_len[r] / 2, y: across});
474
+ }
475
+ }
476
+
400
477
  function compact_tree_lr(nodes, edges, o)
401
478
  {
402
479
  let t = spanning_tree(nodes, edges);
403
- let order = walk_order(t);
404
480
  let pos = new Map();
405
481
 
482
+ /* A run of STACK_MIN_RUN or more consecutive LEAF children is laid
483
+ * out as one block, a STACK. The contour packing below takes the
484
+ * block as one leaf of its own length, so nothing is packed under
485
+ * it. Runs, not all the leaves of a parent: the children keep
486
+ * their order. A stack's key is an OBJECT, so it can never be
487
+ * taken for a node id, which is a string. */
488
+ let kids_of = new Map();
489
+ let stacks = new Map();
490
+ for(let [id, kids] of t.children) {
491
+ let out = [];
492
+ let run = [];
493
+ let flush = () => {
494
+ if(run.length >= STACK_MIN_RUN) {
495
+ let key = {stack: stacks.size};
496
+ stacks.set(key, {ids: run, depth: t.depth.get(run[0])});
497
+ out.push(key);
498
+ } else {
499
+ out.push(...run);
500
+ }
501
+ run = [];
502
+ };
503
+ for(let kid of kids) {
504
+ if((t.children.get(kid) || []).length === 0) {
505
+ run.push(kid);
506
+ } else {
507
+ flush();
508
+ out.push(kid);
509
+ }
510
+ }
511
+ flush();
512
+ kids_of.set(id, out);
513
+ }
514
+
515
+ /* Parents before children, a stack being one child. */
516
+ let order = [];
517
+ let todo = t.roots.slice().reverse();
518
+ while(todo.length) {
519
+ let id = todo.pop();
520
+ order.push(id);
521
+ let kids = kids_of.get(id) || [];
522
+ for(let i = kids.length - 1; i >= 0; i--) {
523
+ todo.push(kids[i]);
524
+ }
525
+ }
526
+
406
527
  /* Columns, as in the tidy tree: a depth is as wide as its widest
407
528
  * card, and a column's gap is `ranksep`. */
408
529
  let col_w = [];
@@ -423,25 +544,31 @@ function compact_tree_lr(nodes, edges, o)
423
544
  let dist = o.nodesep;
424
545
  let level_distance = o.ranksep - pad;
425
546
 
426
- /* `len` along the depth (the column's width: the rows are aligned),
427
- * `ext` across it (the card's own height). */
547
+ /* `len` along the depth (the column's width: the rows are aligned;
548
+ * a stack's own length), `ext` across it. */
428
549
  let tn = new Map();
429
550
  for(let id of order) {
430
- tn.set(id, {
431
- len: col_w[t.depth.get(id)],
432
- ext: t.by_id.get(id).h,
433
- off_x: 0,
434
- off_y: 0,
435
- y: 0,
436
- contour: null,
437
- });
551
+ let st = stacks.get(id);
552
+ if(st) {
553
+ let g = stack_geometry(t, st, col_x, col_w);
554
+ tn.set(id, {len: g.len, ext: g.ext, geom: g, off_x: 0, off_y: 0, y: 0, contour: null});
555
+ } else {
556
+ tn.set(id, {
557
+ len: col_w[t.depth.get(id)],
558
+ ext: t.by_id.get(id).h,
559
+ off_x: 0,
560
+ off_y: 0,
561
+ y: 0,
562
+ contour: null,
563
+ });
564
+ }
438
565
  }
439
566
 
440
567
  /* Contours, children before parents. */
441
568
  for(let i = order.length - 1; i >= 0; i--) {
442
569
  let id = order[i];
443
570
  let n = tn.get(id);
444
- let kids = t.children.get(id) || [];
571
+ let kids = kids_of.get(id) || [];
445
572
 
446
573
  if(!kids.length) {
447
574
  let upper = contour_line(n.len + dist, 0);
@@ -492,7 +619,7 @@ function compact_tree_lr(nodes, edges, o)
492
619
  if(!root_of.has(id)) {
493
620
  root_of.set(id, id);
494
621
  }
495
- let kids = t.children.get(id) || [];
622
+ let kids = kids_of.get(id) || [];
496
623
  if(!kids.length) {
497
624
  continue;
498
625
  }
@@ -524,9 +651,14 @@ function compact_tree_lr(nodes, edges, o)
524
651
  }
525
652
 
526
653
  for(let id of order) {
527
- let d = t.depth.get(id);
528
654
  let n = tn.get(id);
529
655
  let y = n.y + shift.get(root_of.get(id));
656
+ let st = stacks.get(id);
657
+ if(st) {
658
+ place_stack(t, st, n.geom, y, pos);
659
+ continue;
660
+ }
661
+ let d = t.depth.get(id);
530
662
  pos.set(id, {x: col_x[d] + col_w[d] / 2, y: y + n.ext / 2});
531
663
  }
532
664
  return pos;
@@ -6,7 +6,9 @@
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 { spanning_tree, layout_tree, layout_radial, layout_compact } from "./treedb_layout.js";
9
+ import {
10
+ spanning_tree, layout_tree, layout_radial, layout_compact, stack_trunk_width, STACK_GAP,
11
+ } from "./treedb_layout.js";
10
12
 
11
13
  /* es -> (norte, sur); norte -> nave; nave -> d0, d1 (hook rank 1) and
12
14
  * c1 (hook rank 2); c1 -> d0 as well (second parent). */
@@ -134,12 +136,15 @@ const wide_edges = [
134
136
  ].concat([0, 1, 2, 3, 4, 5].map((i) => ({source: "hall", target: `d${i}`})));
135
137
 
136
138
  describe("the compact tree", () => {
137
- test("the rows are the tidy tree's rows", () => {
139
+ test("the rows are the tidy tree's rows; a stack starts on its row", () => {
138
140
  let tidy = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
139
141
  let pos = layout_compact(nodes, edges, {nodesep: 10, ranksep: 100});
140
- for(let n of nodes) {
141
- expect(pos.get(n.id).y).toBe(tidy.get(n.id).y);
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);
142
146
  }
147
+ expect(pos.get("c1").y).toBeGreaterThan(pos.get("d0").y);
143
148
  });
144
149
 
145
150
  test("no two cards of a row are closer than the gap", () => {
@@ -147,14 +152,13 @@ describe("the compact tree", () => {
147
152
  expect_no_overlap_in_rows(wide, layout_compact(wide, wide_edges, {nodesep: 18, ranksep: 90}), 18);
148
153
  });
149
154
 
150
- test("a parent is centred over its children, which keep their order", () => {
151
- let pos = layout_compact(nodes, edges, {nodesep: 10, ranksep: 100});
152
- expect(pos.get("d0").x).toBeLessThan(pos.get("d1").x);
153
- expect(pos.get("d1").x).toBeLessThan(pos.get("c1").x);
154
- let first = pos.get("d0").x - 58;
155
- let last = pos.get("c1").x + 86;
156
- expect(pos.get("nave").x).toBeCloseTo((first + last) / 2, 6);
157
- expect(pos.get("norte").x).toBeLessThan(pos.get("sur").x);
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);
158
162
  });
159
163
 
160
164
  test("a closed sibling sits beside an open branch, not beside its block", () => {
@@ -198,6 +202,74 @@ describe("the compact tree", () => {
198
202
  });
199
203
  });
200
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
+
201
273
  describe("the tidy tree, read right", () => {
202
274
  test("depths are columns; a column is as wide as its widest card", () => {
203
275
  let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100, direction: "LR"});