@yuneta/gobj-ui 7.23.155 → 7.23.156

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
@@ -1430,7 +1430,15 @@ layouts by three thin adapters in `c_g6_nodes_tree.js`:
1430
1430
  cards touching, where the tidy tree's `ranksep` is a gap between COLUMNS
1431
1431
  and a column carries its own width. A fan that cannot overlap by
1432
1432
  construction, across the ring or along the radius; several roots share the
1433
- circle around an empty centre.
1433
+ circle around an empty centre. Since `7.23.156` a run of three or more
1434
+ **leaf** children is **stacked**, as in the compact tree: it asks its ring
1435
+ for one row of about the square root of its cards instead of one slot per
1436
+ leaf, and lays them in rows going outward, each as full as its room allows
1437
+ — rings round the parent when it is alone, a fan inside its sector when
1438
+ not. Measured over the arc it covers (a card is wider slanted), and a
1439
+ neighbour of the outer rings by the angle it covers, so nothing overlaps.
1440
+ On a real-shaped treedb it is 53–63% of the radial it was; `stack: false`
1441
+ keeps the old one.
1434
1442
 
1435
1443
  **Elbow edges** (`7.23.149`) are an option of the edges, not a layout: the
1436
1444
  toolbar's `GRAPH_EDGE_ELBOW` toggle sets `edge_shape` to `elbow`, and every
@@ -29423,6 +29423,56 @@ function stack_trunk_width(k) {
29423
29423
  let lanes = Math.max(0, k - 2);
29424
29424
  return Math.max(36, 20 + Math.max(0, lanes - 1) * 6);
29425
29425
  }
29426
+ /************************************************************
29427
+ * The children of every node, with each run of STACK_MIN_RUN or
29428
+ * more consecutive LEAF children replaced by one STACK. Runs, not
29429
+ * all the leaves of a parent: the children keep their order. A
29430
+ * stack's key is an OBJECT, so it can never be taken for a node id,
29431
+ * which is a string. Shared by the compact tree and the radial.
29432
+ *
29433
+ * Returns {kids_of: Map<id, [id | stack]>, stacks: Map<stack, {ids, depth}>}
29434
+ ************************************************************/
29435
+ function leaf_runs(t) {
29436
+ let kids_of = /* @__PURE__ */ new Map();
29437
+ let stacks = /* @__PURE__ */ new Map();
29438
+ for (let [id, kids] of t.children) {
29439
+ let out = [];
29440
+ let run = [];
29441
+ let flush = () => {
29442
+ if (run.length >= 3) {
29443
+ let key = { stack: stacks.size };
29444
+ stacks.set(key, {
29445
+ ids: run,
29446
+ depth: t.depth.get(run[0])
29447
+ });
29448
+ out.push(key);
29449
+ } else out.push(...run);
29450
+ run = [];
29451
+ };
29452
+ for (let kid of kids) if ((t.children.get(kid) || []).length === 0) run.push(kid);
29453
+ else {
29454
+ flush();
29455
+ out.push(kid);
29456
+ }
29457
+ flush();
29458
+ kids_of.set(id, out);
29459
+ }
29460
+ return {
29461
+ kids_of,
29462
+ stacks
29463
+ };
29464
+ }
29465
+ function stacked_order(t, kids_of) {
29466
+ let order = [];
29467
+ let todo = t.roots.slice().reverse();
29468
+ while (todo.length) {
29469
+ let id = todo.pop();
29470
+ order.push(id);
29471
+ let kids = kids_of.get(id) || [];
29472
+ for (let i = kids.length - 1; i >= 0; i--) todo.push(kids[i]);
29473
+ }
29474
+ return order;
29475
+ }
29426
29476
  function stack_geometry(t, st, col_x, col_w) {
29427
29477
  let d = st.depth;
29428
29478
  let k = st.ids.length;
@@ -29462,38 +29512,8 @@ function place_stack(t, st, g, top, pos) {
29462
29512
  function compact_tree_lr(nodes, edges, o) {
29463
29513
  let t = spanning_tree(nodes, edges);
29464
29514
  let pos = /* @__PURE__ */ new Map();
29465
- let kids_of = /* @__PURE__ */ new Map();
29466
- let stacks = /* @__PURE__ */ new Map();
29467
- for (let [id, kids] of t.children) {
29468
- let out = [];
29469
- let run = [];
29470
- let flush = () => {
29471
- if (run.length >= 3) {
29472
- let key = { stack: stacks.size };
29473
- stacks.set(key, {
29474
- ids: run,
29475
- depth: t.depth.get(run[0])
29476
- });
29477
- out.push(key);
29478
- } else out.push(...run);
29479
- run = [];
29480
- };
29481
- for (let kid of kids) if ((t.children.get(kid) || []).length === 0) run.push(kid);
29482
- else {
29483
- flush();
29484
- out.push(kid);
29485
- }
29486
- flush();
29487
- kids_of.set(id, out);
29488
- }
29489
- let order = [];
29490
- let todo = t.roots.slice().reverse();
29491
- while (todo.length) {
29492
- let id = todo.pop();
29493
- order.push(id);
29494
- let kids = kids_of.get(id) || [];
29495
- for (let i = kids.length - 1; i >= 0; i--) todo.push(kids[i]);
29496
- }
29515
+ let { kids_of, stacks } = leaf_runs(t);
29516
+ let order = stacked_order(t, kids_of);
29497
29517
  let col_w = [];
29498
29518
  for (let [id, d] of t.depth) col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
29499
29519
  let col_x = [];
@@ -29630,56 +29650,103 @@ function extent_at(n, a) {
29630
29650
  *
29631
29651
  * ranksep the step between rings
29632
29652
  * nodesep the gap between neighbours on a ring
29653
+ * stack (default true) a run of three or more LEAF children
29654
+ * is a STACK, as in the compact tree: it asks its
29655
+ * ring for one row of about the square root of its
29656
+ * cards, not for every leaf -- a hall with a hundred
29657
+ * devices no longer blows the ring up -- and lays
29658
+ * them in rows going OUTWARD, each row as full as
29659
+ * the room it has: rings round the parent when it is
29660
+ * alone, a fan inside its sector when it is not.
29661
+ * false keeps every leaf on the ring.
29633
29662
  ************************************************************/
29634
29663
  function layout_radial(nodes, edges, opts) {
29635
- let o = Object.assign({}, DEFAULTS, opts || {});
29664
+ let o = Object.assign({}, DEFAULTS, { stack: true }, opts || {});
29636
29665
  let t = spanning_tree(nodes, edges);
29637
29666
  let pos = /* @__PURE__ */ new Map();
29638
29667
  if (!t.roots.length) return pos;
29639
- let order = walk_order(t);
29640
- let leaves = /* @__PURE__ */ new Map();
29668
+ let runs = o.stack ? leaf_runs(t) : {
29669
+ kids_of: t.children,
29670
+ stacks: /* @__PURE__ */ new Map()
29671
+ };
29672
+ let kids_of = runs.kids_of;
29673
+ let stacks = runs.stacks;
29674
+ let order = stacked_order(t, kids_of);
29675
+ for (let st of stacks.values()) {
29676
+ st.per_row = Math.ceil(Math.sqrt(st.ids.length));
29677
+ st.cards = st.ids.map((id) => t.by_id.get(id));
29678
+ }
29679
+ let stack_across = (st, a) => {
29680
+ let w = 0;
29681
+ for (let c of st.cards) w = Math.max(w, extent_at(c, a + Math.PI / 2));
29682
+ return {
29683
+ card: w,
29684
+ row: st.per_row * w + (st.per_row - 1) * o.nodesep
29685
+ };
29686
+ };
29687
+ let widest_along = (cards, a, half) => {
29688
+ let e = 0;
29689
+ for (let k = 0; k <= 16; k++) {
29690
+ let th = a - half + 2 * half * k / 16;
29691
+ for (let c of cards) e = Math.max(e, extent_at(c, th));
29692
+ }
29693
+ return e;
29694
+ };
29695
+ let depth_of = (id) => stacks.has(id) ? stacks.get(id).depth : t.depth.get(id);
29696
+ let circ = (a, b) => {
29697
+ let d = Math.abs(a - b) % (2 * Math.PI);
29698
+ return d > Math.PI ? 2 * Math.PI - d : d;
29699
+ };
29700
+ let weight = /* @__PURE__ */ new Map();
29641
29701
  for (let i = order.length - 1; i >= 0; i--) {
29642
29702
  let id = order[i];
29643
- let kids = t.children.get(id) || [];
29703
+ let st = stacks.get(id);
29704
+ if (st) {
29705
+ weight.set(id, st.per_row);
29706
+ continue;
29707
+ }
29708
+ let kids = kids_of.get(id) || [];
29644
29709
  if (!kids.length) {
29645
- leaves.set(id, 1);
29710
+ weight.set(id, 1);
29646
29711
  continue;
29647
29712
  }
29648
29713
  let n = 0;
29649
- for (let kid of kids) n += leaves.get(kid);
29650
- leaves.set(id, n);
29714
+ for (let kid of kids) n += weight.get(kid);
29715
+ weight.set(id, n);
29651
29716
  }
29652
29717
  let total = 0;
29653
- for (let root of t.roots) total += leaves.get(root);
29718
+ for (let root of t.roots) total += weight.get(root);
29654
29719
  let single = t.roots.length === 1;
29655
- let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
29720
+ let ring_of = (id) => depth_of(id) + (single ? 0 : 1);
29656
29721
  let angle = /* @__PURE__ */ new Map();
29657
29722
  let rings = [];
29658
29723
  let sector = (root_id, root_from, root_span, root_parent) => {
29659
- let stack = [{
29724
+ let todo = [{
29660
29725
  id: root_id,
29661
29726
  from: root_from,
29662
29727
  span: root_span,
29663
29728
  parent: root_parent
29664
29729
  }];
29665
- while (stack.length) {
29666
- let { id, from, span, parent } = stack.pop();
29730
+ while (todo.length) {
29731
+ let { id, from, span, parent } = todo.pop();
29667
29732
  let a = from + span / 2;
29668
29733
  angle.set(id, a);
29669
29734
  let ring = ring_of(id);
29670
29735
  if (!rings[ring]) rings[ring] = [];
29671
- let n = t.by_id.get(id);
29736
+ let st = stacks.get(id);
29737
+ let n = st ? null : t.by_id.get(id);
29672
29738
  rings[ring].push({
29673
29739
  id,
29674
29740
  angle: a,
29675
- diag: Math.hypot(n.w, n.h),
29676
- ext: extent_at(n, a),
29677
- parent_ext: parent ? extent_at(t.by_id.get(parent), a) : 0
29741
+ span,
29742
+ diag: st ? stack_across(st, a).row : Math.hypot(n.w, n.h),
29743
+ ext: st ? widest_along(st.cards, a, Math.min(span / 2, Math.PI)) : extent_at(n, a),
29744
+ parent_ext: !parent ? 0 : st ? widest_along([t.by_id.get(parent)], a, Math.min(span / 2, Math.PI)) : extent_at(t.by_id.get(parent), a)
29678
29745
  });
29679
29746
  let cursor = from;
29680
- for (let kid of t.children.get(id) || []) {
29681
- let part = span * leaves.get(kid) / leaves.get(id);
29682
- stack.push({
29747
+ for (let kid of kids_of.get(id) || []) {
29748
+ let part = span * weight.get(kid) / weight.get(id);
29749
+ todo.push({
29683
29750
  id: kid,
29684
29751
  from: cursor,
29685
29752
  span: part,
@@ -29693,38 +29760,120 @@ function layout_radial(nodes, edges, opts) {
29693
29760
  let root = t.roots[0];
29694
29761
  angle.set(root, 0);
29695
29762
  let cursor = -Math.PI / 2;
29696
- for (let kid of t.children.get(root) || []) {
29697
- let part = 2 * Math.PI * leaves.get(kid) / leaves.get(root);
29763
+ for (let kid of kids_of.get(root) || []) {
29764
+ let part = 2 * Math.PI * weight.get(kid) / weight.get(root);
29698
29765
  sector(kid, cursor, part, root);
29699
29766
  cursor += part;
29700
29767
  }
29701
29768
  } else {
29702
29769
  let cursor = -Math.PI / 2;
29703
29770
  for (let root of t.roots) {
29704
- let part = 2 * Math.PI * leaves.get(root) / total;
29771
+ let part = 2 * Math.PI * weight.get(root) / total;
29705
29772
  sector(root, cursor, part, null);
29706
29773
  cursor += part;
29707
29774
  }
29708
29775
  }
29776
+ let fit = (list, r0) => {
29777
+ let r = r0;
29778
+ let sorted = list.slice().sort((a, b) => a.angle - b.angle);
29779
+ for (let i = 0; i < sorted.length && sorted.length > 1; i++) {
29780
+ let a = sorted[i];
29781
+ let b = sorted[(i + 1) % sorted.length];
29782
+ let gap = i + 1 < sorted.length ? b.angle - a.angle : b.angle + 2 * Math.PI - a.angle;
29783
+ if (gap > 1e-9) r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
29784
+ }
29785
+ return r;
29786
+ };
29709
29787
  let radius = [0];
29788
+ let reaching = [];
29710
29789
  for (let ring = 1; ring < rings.length; ring++) {
29790
+ let members = rings[ring] || [];
29711
29791
  let r = radius[ring - 1] + o.ranksep;
29712
- for (let n of rings[ring] || []) r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
29713
- let list = (rings[ring] || []).slice().sort((a, b) => a.angle - b.angle);
29714
- for (let i = 0; i < list.length && list.length > 1; i++) {
29715
- let a = list[i];
29716
- let b = list[(i + 1) % list.length];
29717
- let gap = i + 1 < list.length ? b.angle - a.angle : b.angle + 2 * Math.PI - a.angle;
29718
- if (gap > 1e-9) r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
29792
+ let band = 0;
29793
+ for (let n of members) {
29794
+ r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
29795
+ band = Math.max(band, n.ext);
29796
+ }
29797
+ r = fit(members, r);
29798
+ for (let pass = 0; pass < 2; pass++) for (let g of reaching) {
29799
+ if (g.reach < r - band / 2 - o.nodesep) continue;
29800
+ for (let m of members) {
29801
+ let d = circ(g.angle, m.angle);
29802
+ if (d > g.half + 1e-9) r = Math.max(r, (m.diag / 2 + o.nodesep) / (d - g.half));
29803
+ else r = Math.max(r, g.reach + m.ext / 2 + o.nodesep);
29804
+ }
29719
29805
  }
29720
29806
  radius[ring] = r;
29807
+ let sorted = members.slice().sort((a, b) => a.angle - b.angle);
29808
+ for (let i = 0; i < sorted.length; i++) {
29809
+ let m = sorted[i];
29810
+ let st = stacks.get(m.id);
29811
+ if (!st) continue;
29812
+ let room = m.span / 2;
29813
+ if (sorted.length > 1) {
29814
+ let left = sorted[(i - 1 + sorted.length) % sorted.length];
29815
+ let right = sorted[(i + 1) % sorted.length];
29816
+ room = Math.min(room, circ(m.angle, left.angle) - (left.diag / 2 + o.nodesep) / r, circ(m.angle, right.angle) - (right.diag / 2 + o.nodesep) / r);
29817
+ }
29818
+ room = Math.min(room, Math.PI);
29819
+ let across = 0;
29820
+ let along = 0;
29821
+ for (let k = 0; k <= 16; k++) {
29822
+ let th = m.angle - room + 2 * room * k / 16;
29823
+ for (let c of st.cards) {
29824
+ across = Math.max(across, extent_at(c, th + Math.PI / 2));
29825
+ along = Math.max(along, extent_at(c, th));
29826
+ }
29827
+ }
29828
+ let rows = [];
29829
+ let left_to_place = st.ids.length;
29830
+ let rr = r;
29831
+ let half = 0;
29832
+ while (left_to_place > 0) {
29833
+ let fits = Math.floor(2 * room * rr / (across + o.nodesep));
29834
+ let cnt = Math.max(1, Math.min(left_to_place, fits));
29835
+ rows.push({
29836
+ rr,
29837
+ cnt
29838
+ });
29839
+ half = Math.max(half, (cnt * (across + o.nodesep) - o.nodesep) / 2 / rr);
29840
+ left_to_place -= cnt;
29841
+ rr += along + 40;
29842
+ }
29843
+ st.plan = {
29844
+ angle: m.angle,
29845
+ across,
29846
+ rows
29847
+ };
29848
+ reaching.push({
29849
+ angle: m.angle,
29850
+ half,
29851
+ reach: rows[rows.length - 1].rr + along / 2
29852
+ });
29853
+ }
29721
29854
  }
29722
29855
  for (let [id, a] of angle) {
29723
- let r = radius[ring_of(id)] || 0;
29724
- pos.set(id, {
29725
- x: r * Math.cos(a),
29726
- y: r * Math.sin(a)
29727
- });
29856
+ let st = stacks.get(id);
29857
+ if (!st) {
29858
+ let r0 = radius[ring_of(id)] || 0;
29859
+ pos.set(id, {
29860
+ x: r0 * Math.cos(a),
29861
+ y: r0 * Math.sin(a)
29862
+ });
29863
+ continue;
29864
+ }
29865
+ let i = 0;
29866
+ for (let row of st.plan.rows) {
29867
+ let step = (st.plan.across + o.nodesep) / row.rr;
29868
+ for (let j = 0; j < row.cnt; j++) {
29869
+ let th = a + (j - (row.cnt - 1) / 2) * step;
29870
+ pos.set(st.ids[i], {
29871
+ x: row.rr * Math.cos(th),
29872
+ y: row.rr * Math.sin(th)
29873
+ });
29874
+ i++;
29875
+ }
29876
+ }
29728
29877
  }
29729
29878
  return pos;
29730
29879
  }
@@ -29418,6 +29418,56 @@ function stack_trunk_width(k) {
29418
29418
  let lanes = Math.max(0, k - 2);
29419
29419
  return Math.max(36, 20 + Math.max(0, lanes - 1) * 6);
29420
29420
  }
29421
+ /************************************************************
29422
+ * The children of every node, with each run of STACK_MIN_RUN or
29423
+ * more consecutive LEAF children replaced by one STACK. Runs, not
29424
+ * all the leaves of a parent: the children keep their order. A
29425
+ * stack's key is an OBJECT, so it can never be taken for a node id,
29426
+ * which is a string. Shared by the compact tree and the radial.
29427
+ *
29428
+ * Returns {kids_of: Map<id, [id | stack]>, stacks: Map<stack, {ids, depth}>}
29429
+ ************************************************************/
29430
+ function leaf_runs(t) {
29431
+ let kids_of = /* @__PURE__ */ new Map();
29432
+ let stacks = /* @__PURE__ */ new Map();
29433
+ for (let [id, kids] of t.children) {
29434
+ let out = [];
29435
+ let run = [];
29436
+ let flush = () => {
29437
+ if (run.length >= 3) {
29438
+ let key = { stack: stacks.size };
29439
+ stacks.set(key, {
29440
+ ids: run,
29441
+ depth: t.depth.get(run[0])
29442
+ });
29443
+ out.push(key);
29444
+ } else out.push(...run);
29445
+ run = [];
29446
+ };
29447
+ for (let kid of kids) if ((t.children.get(kid) || []).length === 0) run.push(kid);
29448
+ else {
29449
+ flush();
29450
+ out.push(kid);
29451
+ }
29452
+ flush();
29453
+ kids_of.set(id, out);
29454
+ }
29455
+ return {
29456
+ kids_of,
29457
+ stacks
29458
+ };
29459
+ }
29460
+ function stacked_order(t, kids_of) {
29461
+ let order = [];
29462
+ let todo = t.roots.slice().reverse();
29463
+ while (todo.length) {
29464
+ let id = todo.pop();
29465
+ order.push(id);
29466
+ let kids = kids_of.get(id) || [];
29467
+ for (let i = kids.length - 1; i >= 0; i--) todo.push(kids[i]);
29468
+ }
29469
+ return order;
29470
+ }
29421
29471
  function stack_geometry(t, st, col_x, col_w) {
29422
29472
  let d = st.depth;
29423
29473
  let k = st.ids.length;
@@ -29457,38 +29507,8 @@ function place_stack(t, st, g, top, pos) {
29457
29507
  function compact_tree_lr(nodes, edges, o) {
29458
29508
  let t = spanning_tree(nodes, edges);
29459
29509
  let pos = /* @__PURE__ */ new Map();
29460
- let kids_of = /* @__PURE__ */ new Map();
29461
- let stacks = /* @__PURE__ */ new Map();
29462
- for (let [id, kids] of t.children) {
29463
- let out = [];
29464
- let run = [];
29465
- let flush = () => {
29466
- if (run.length >= 3) {
29467
- let key = { stack: stacks.size };
29468
- stacks.set(key, {
29469
- ids: run,
29470
- depth: t.depth.get(run[0])
29471
- });
29472
- out.push(key);
29473
- } else out.push(...run);
29474
- run = [];
29475
- };
29476
- for (let kid of kids) if ((t.children.get(kid) || []).length === 0) run.push(kid);
29477
- else {
29478
- flush();
29479
- out.push(kid);
29480
- }
29481
- flush();
29482
- kids_of.set(id, out);
29483
- }
29484
- let order = [];
29485
- let todo = t.roots.slice().reverse();
29486
- while (todo.length) {
29487
- let id = todo.pop();
29488
- order.push(id);
29489
- let kids = kids_of.get(id) || [];
29490
- for (let i = kids.length - 1; i >= 0; i--) todo.push(kids[i]);
29491
- }
29510
+ let { kids_of, stacks } = leaf_runs(t);
29511
+ let order = stacked_order(t, kids_of);
29492
29512
  let col_w = [];
29493
29513
  for (let [id, d] of t.depth) col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
29494
29514
  let col_x = [];
@@ -29625,56 +29645,103 @@ function extent_at(n, a) {
29625
29645
  *
29626
29646
  * ranksep the step between rings
29627
29647
  * nodesep the gap between neighbours on a ring
29648
+ * stack (default true) a run of three or more LEAF children
29649
+ * is a STACK, as in the compact tree: it asks its
29650
+ * ring for one row of about the square root of its
29651
+ * cards, not for every leaf -- a hall with a hundred
29652
+ * devices no longer blows the ring up -- and lays
29653
+ * them in rows going OUTWARD, each row as full as
29654
+ * the room it has: rings round the parent when it is
29655
+ * alone, a fan inside its sector when it is not.
29656
+ * false keeps every leaf on the ring.
29628
29657
  ************************************************************/
29629
29658
  function layout_radial(nodes, edges, opts) {
29630
- let o = Object.assign({}, DEFAULTS, opts || {});
29659
+ let o = Object.assign({}, DEFAULTS, { stack: true }, opts || {});
29631
29660
  let t = spanning_tree(nodes, edges);
29632
29661
  let pos = /* @__PURE__ */ new Map();
29633
29662
  if (!t.roots.length) return pos;
29634
- let order = walk_order(t);
29635
- let leaves = /* @__PURE__ */ new Map();
29663
+ let runs = o.stack ? leaf_runs(t) : {
29664
+ kids_of: t.children,
29665
+ stacks: /* @__PURE__ */ new Map()
29666
+ };
29667
+ let kids_of = runs.kids_of;
29668
+ let stacks = runs.stacks;
29669
+ let order = stacked_order(t, kids_of);
29670
+ for (let st of stacks.values()) {
29671
+ st.per_row = Math.ceil(Math.sqrt(st.ids.length));
29672
+ st.cards = st.ids.map((id) => t.by_id.get(id));
29673
+ }
29674
+ let stack_across = (st, a) => {
29675
+ let w = 0;
29676
+ for (let c of st.cards) w = Math.max(w, extent_at(c, a + Math.PI / 2));
29677
+ return {
29678
+ card: w,
29679
+ row: st.per_row * w + (st.per_row - 1) * o.nodesep
29680
+ };
29681
+ };
29682
+ let widest_along = (cards, a, half) => {
29683
+ let e = 0;
29684
+ for (let k = 0; k <= 16; k++) {
29685
+ let th = a - half + 2 * half * k / 16;
29686
+ for (let c of cards) e = Math.max(e, extent_at(c, th));
29687
+ }
29688
+ return e;
29689
+ };
29690
+ let depth_of = (id) => stacks.has(id) ? stacks.get(id).depth : t.depth.get(id);
29691
+ let circ = (a, b) => {
29692
+ let d = Math.abs(a - b) % (2 * Math.PI);
29693
+ return d > Math.PI ? 2 * Math.PI - d : d;
29694
+ };
29695
+ let weight = /* @__PURE__ */ new Map();
29636
29696
  for (let i = order.length - 1; i >= 0; i--) {
29637
29697
  let id = order[i];
29638
- let kids = t.children.get(id) || [];
29698
+ let st = stacks.get(id);
29699
+ if (st) {
29700
+ weight.set(id, st.per_row);
29701
+ continue;
29702
+ }
29703
+ let kids = kids_of.get(id) || [];
29639
29704
  if (!kids.length) {
29640
- leaves.set(id, 1);
29705
+ weight.set(id, 1);
29641
29706
  continue;
29642
29707
  }
29643
29708
  let n = 0;
29644
- for (let kid of kids) n += leaves.get(kid);
29645
- leaves.set(id, n);
29709
+ for (let kid of kids) n += weight.get(kid);
29710
+ weight.set(id, n);
29646
29711
  }
29647
29712
  let total = 0;
29648
- for (let root of t.roots) total += leaves.get(root);
29713
+ for (let root of t.roots) total += weight.get(root);
29649
29714
  let single = t.roots.length === 1;
29650
- let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
29715
+ let ring_of = (id) => depth_of(id) + (single ? 0 : 1);
29651
29716
  let angle = /* @__PURE__ */ new Map();
29652
29717
  let rings = [];
29653
29718
  let sector = (root_id, root_from, root_span, root_parent) => {
29654
- let stack = [{
29719
+ let todo = [{
29655
29720
  id: root_id,
29656
29721
  from: root_from,
29657
29722
  span: root_span,
29658
29723
  parent: root_parent
29659
29724
  }];
29660
- while (stack.length) {
29661
- let { id, from, span, parent } = stack.pop();
29725
+ while (todo.length) {
29726
+ let { id, from, span, parent } = todo.pop();
29662
29727
  let a = from + span / 2;
29663
29728
  angle.set(id, a);
29664
29729
  let ring = ring_of(id);
29665
29730
  if (!rings[ring]) rings[ring] = [];
29666
- let n = t.by_id.get(id);
29731
+ let st = stacks.get(id);
29732
+ let n = st ? null : t.by_id.get(id);
29667
29733
  rings[ring].push({
29668
29734
  id,
29669
29735
  angle: a,
29670
- diag: Math.hypot(n.w, n.h),
29671
- ext: extent_at(n, a),
29672
- parent_ext: parent ? extent_at(t.by_id.get(parent), a) : 0
29736
+ span,
29737
+ diag: st ? stack_across(st, a).row : Math.hypot(n.w, n.h),
29738
+ ext: st ? widest_along(st.cards, a, Math.min(span / 2, Math.PI)) : extent_at(n, a),
29739
+ parent_ext: !parent ? 0 : st ? widest_along([t.by_id.get(parent)], a, Math.min(span / 2, Math.PI)) : extent_at(t.by_id.get(parent), a)
29673
29740
  });
29674
29741
  let cursor = from;
29675
- for (let kid of t.children.get(id) || []) {
29676
- let part = span * leaves.get(kid) / leaves.get(id);
29677
- stack.push({
29742
+ for (let kid of kids_of.get(id) || []) {
29743
+ let part = span * weight.get(kid) / weight.get(id);
29744
+ todo.push({
29678
29745
  id: kid,
29679
29746
  from: cursor,
29680
29747
  span: part,
@@ -29688,38 +29755,120 @@ function layout_radial(nodes, edges, opts) {
29688
29755
  let root = t.roots[0];
29689
29756
  angle.set(root, 0);
29690
29757
  let cursor = -Math.PI / 2;
29691
- for (let kid of t.children.get(root) || []) {
29692
- let part = 2 * Math.PI * leaves.get(kid) / leaves.get(root);
29758
+ for (let kid of kids_of.get(root) || []) {
29759
+ let part = 2 * Math.PI * weight.get(kid) / weight.get(root);
29693
29760
  sector(kid, cursor, part, root);
29694
29761
  cursor += part;
29695
29762
  }
29696
29763
  } else {
29697
29764
  let cursor = -Math.PI / 2;
29698
29765
  for (let root of t.roots) {
29699
- let part = 2 * Math.PI * leaves.get(root) / total;
29766
+ let part = 2 * Math.PI * weight.get(root) / total;
29700
29767
  sector(root, cursor, part, null);
29701
29768
  cursor += part;
29702
29769
  }
29703
29770
  }
29771
+ let fit = (list, r0) => {
29772
+ let r = r0;
29773
+ let sorted = list.slice().sort((a, b) => a.angle - b.angle);
29774
+ for (let i = 0; i < sorted.length && sorted.length > 1; i++) {
29775
+ let a = sorted[i];
29776
+ let b = sorted[(i + 1) % sorted.length];
29777
+ let gap = i + 1 < sorted.length ? b.angle - a.angle : b.angle + 2 * Math.PI - a.angle;
29778
+ if (gap > 1e-9) r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
29779
+ }
29780
+ return r;
29781
+ };
29704
29782
  let radius = [0];
29783
+ let reaching = [];
29705
29784
  for (let ring = 1; ring < rings.length; ring++) {
29785
+ let members = rings[ring] || [];
29706
29786
  let r = radius[ring - 1] + o.ranksep;
29707
- for (let n of rings[ring] || []) r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
29708
- let list = (rings[ring] || []).slice().sort((a, b) => a.angle - b.angle);
29709
- for (let i = 0; i < list.length && list.length > 1; i++) {
29710
- let a = list[i];
29711
- let b = list[(i + 1) % list.length];
29712
- let gap = i + 1 < list.length ? b.angle - a.angle : b.angle + 2 * Math.PI - a.angle;
29713
- if (gap > 1e-9) r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
29787
+ let band = 0;
29788
+ for (let n of members) {
29789
+ r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
29790
+ band = Math.max(band, n.ext);
29791
+ }
29792
+ r = fit(members, r);
29793
+ for (let pass = 0; pass < 2; pass++) for (let g of reaching) {
29794
+ if (g.reach < r - band / 2 - o.nodesep) continue;
29795
+ for (let m of members) {
29796
+ let d = circ(g.angle, m.angle);
29797
+ if (d > g.half + 1e-9) r = Math.max(r, (m.diag / 2 + o.nodesep) / (d - g.half));
29798
+ else r = Math.max(r, g.reach + m.ext / 2 + o.nodesep);
29799
+ }
29714
29800
  }
29715
29801
  radius[ring] = r;
29802
+ let sorted = members.slice().sort((a, b) => a.angle - b.angle);
29803
+ for (let i = 0; i < sorted.length; i++) {
29804
+ let m = sorted[i];
29805
+ let st = stacks.get(m.id);
29806
+ if (!st) continue;
29807
+ let room = m.span / 2;
29808
+ if (sorted.length > 1) {
29809
+ let left = sorted[(i - 1 + sorted.length) % sorted.length];
29810
+ let right = sorted[(i + 1) % sorted.length];
29811
+ room = Math.min(room, circ(m.angle, left.angle) - (left.diag / 2 + o.nodesep) / r, circ(m.angle, right.angle) - (right.diag / 2 + o.nodesep) / r);
29812
+ }
29813
+ room = Math.min(room, Math.PI);
29814
+ let across = 0;
29815
+ let along = 0;
29816
+ for (let k = 0; k <= 16; k++) {
29817
+ let th = m.angle - room + 2 * room * k / 16;
29818
+ for (let c of st.cards) {
29819
+ across = Math.max(across, extent_at(c, th + Math.PI / 2));
29820
+ along = Math.max(along, extent_at(c, th));
29821
+ }
29822
+ }
29823
+ let rows = [];
29824
+ let left_to_place = st.ids.length;
29825
+ let rr = r;
29826
+ let half = 0;
29827
+ while (left_to_place > 0) {
29828
+ let fits = Math.floor(2 * room * rr / (across + o.nodesep));
29829
+ let cnt = Math.max(1, Math.min(left_to_place, fits));
29830
+ rows.push({
29831
+ rr,
29832
+ cnt
29833
+ });
29834
+ half = Math.max(half, (cnt * (across + o.nodesep) - o.nodesep) / 2 / rr);
29835
+ left_to_place -= cnt;
29836
+ rr += along + 40;
29837
+ }
29838
+ st.plan = {
29839
+ angle: m.angle,
29840
+ across,
29841
+ rows
29842
+ };
29843
+ reaching.push({
29844
+ angle: m.angle,
29845
+ half,
29846
+ reach: rows[rows.length - 1].rr + along / 2
29847
+ });
29848
+ }
29716
29849
  }
29717
29850
  for (let [id, a] of angle) {
29718
- let r = radius[ring_of(id)] || 0;
29719
- pos.set(id, {
29720
- x: r * Math.cos(a),
29721
- y: r * Math.sin(a)
29722
- });
29851
+ let st = stacks.get(id);
29852
+ if (!st) {
29853
+ let r0 = radius[ring_of(id)] || 0;
29854
+ pos.set(id, {
29855
+ x: r0 * Math.cos(a),
29856
+ y: r0 * Math.sin(a)
29857
+ });
29858
+ continue;
29859
+ }
29860
+ let i = 0;
29861
+ for (let row of st.plan.rows) {
29862
+ let step = (st.plan.across + o.nodesep) / row.rr;
29863
+ for (let j = 0; j < row.cnt; j++) {
29864
+ let th = a + (j - (row.cnt - 1) / 2) * step;
29865
+ pos.set(st.ids[i], {
29866
+ x: row.rr * Math.cos(th),
29867
+ y: row.rr * Math.sin(th)
29868
+ });
29869
+ i++;
29870
+ }
29871
+ }
29723
29872
  }
29724
29873
  return pos;
29725
29874
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.155",
3
+ "version": "7.23.156",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -426,6 +426,63 @@ export function stack_trunk_width(k)
426
426
  2 * STACK_LANE_PAD + Math.max(0, lanes - 1) * STACK_LANE_STEP);
427
427
  }
428
428
 
429
+ /************************************************************
430
+ * The children of every node, with each run of STACK_MIN_RUN or
431
+ * more consecutive LEAF children replaced by one STACK. Runs, not
432
+ * all the leaves of a parent: the children keep their order. A
433
+ * stack's key is an OBJECT, so it can never be taken for a node id,
434
+ * which is a string. Shared by the compact tree and the radial.
435
+ *
436
+ * Returns {kids_of: Map<id, [id | stack]>, stacks: Map<stack, {ids, depth}>}
437
+ ************************************************************/
438
+ function leaf_runs(t)
439
+ {
440
+ let kids_of = new Map();
441
+ let stacks = new Map();
442
+ for(let [id, kids] of t.children) {
443
+ let out = [];
444
+ let run = [];
445
+ let flush = () => {
446
+ if(run.length >= STACK_MIN_RUN) {
447
+ let key = {stack: stacks.size};
448
+ stacks.set(key, {ids: run, depth: t.depth.get(run[0])});
449
+ out.push(key);
450
+ } else {
451
+ out.push(...run);
452
+ }
453
+ run = [];
454
+ };
455
+ for(let kid of kids) {
456
+ if((t.children.get(kid) || []).length === 0) {
457
+ run.push(kid);
458
+ } else {
459
+ flush();
460
+ out.push(kid);
461
+ }
462
+ }
463
+ flush();
464
+ kids_of.set(id, out);
465
+ }
466
+ return {kids_of: kids_of, stacks: stacks};
467
+ }
468
+
469
+ /* Parents before children, a stack being one child (walk_order's
470
+ * form, over `kids_of`). */
471
+ function stacked_order(t, kids_of)
472
+ {
473
+ let order = [];
474
+ let todo = t.roots.slice().reverse();
475
+ while(todo.length) {
476
+ let id = todo.pop();
477
+ order.push(id);
478
+ let kids = kids_of.get(id) || [];
479
+ for(let i = kids.length - 1; i >= 0; i--) {
480
+ todo.push(kids[i]);
481
+ }
482
+ }
483
+ return order;
484
+ }
485
+
429
486
  /* A stack in the LR frame (depth along x, siblings along y): `side`
430
487
  * is how far each column reaches across, `starts`/`row_len` where
431
488
  * each row begins and how long it is along the depth. Its first row
@@ -479,50 +536,11 @@ function compact_tree_lr(nodes, edges, o)
479
536
  let t = spanning_tree(nodes, edges);
480
537
  let pos = new Map();
481
538
 
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
- }
539
+ /* Runs of leaf children become STACKS (leaf_runs); the contour
540
+ * packing below takes a stack as one leaf of its own length, so
541
+ * nothing is packed under it. */
542
+ let {kids_of, stacks} = leaf_runs(t);
543
+ let order = stacked_order(t, kids_of);
526
544
 
527
545
  /* Columns, as in the tidy tree: a depth is as wide as its widest
528
546
  * card, and a column's gap is `ranksep`. */
@@ -693,76 +711,148 @@ function extent_at(n, a)
693
711
  *
694
712
  * ranksep the step between rings
695
713
  * nodesep the gap between neighbours on a ring
714
+ * stack (default true) a run of three or more LEAF children
715
+ * is a STACK, as in the compact tree: it asks its
716
+ * ring for one row of about the square root of its
717
+ * cards, not for every leaf -- a hall with a hundred
718
+ * devices no longer blows the ring up -- and lays
719
+ * them in rows going OUTWARD, each row as full as
720
+ * the room it has: rings round the parent when it is
721
+ * alone, a fan inside its sector when it is not.
722
+ * false keeps every leaf on the ring.
696
723
  ************************************************************/
697
724
  export function layout_radial(nodes, edges, opts)
698
725
  {
699
- let o = Object.assign({}, DEFAULTS, opts || {});
726
+ let o = Object.assign({}, DEFAULTS, {stack: true}, opts || {});
700
727
  let t = spanning_tree(nodes, edges);
701
728
  let pos = new Map();
702
729
  if(!t.roots.length) {
703
730
  return pos;
704
731
  }
705
732
 
706
- /* Leaves under each node: the sector it deserves. Post-order. */
707
- let order = walk_order(t);
708
- let leaves = new Map();
733
+ let runs = o.stack? leaf_runs(t) : {kids_of: t.children, stacks: new Map()};
734
+ let kids_of = runs.kids_of;
735
+ let stacks = runs.stacks;
736
+ let order = stacked_order(t, kids_of);
737
+
738
+ /* A stack asks its ring for one row of about the square root of
739
+ * its cards; how many each row really holds is decided once the
740
+ * ring's radius is known (pass 2), by the room it has there. */
741
+ for(let st of stacks.values()) {
742
+ st.per_row = Math.ceil(Math.sqrt(st.ids.length));
743
+ st.cards = st.ids.map((id) => t.by_id.get(id));
744
+ }
745
+ /* A stack across its ring, at angle `a`: a card's width there, and
746
+ * the row it asks for. Along the radius: how far one row reaches. */
747
+ let stack_across = (st, a) => {
748
+ let w = 0;
749
+ for(let c of st.cards) {
750
+ w = Math.max(w, extent_at(c, a + Math.PI / 2));
751
+ }
752
+ return {card: w, row: st.per_row * w + (st.per_row - 1) * o.nodesep};
753
+ };
754
+ let stack_along = (st, a) => {
755
+ let e = 0;
756
+ for(let c of st.cards) {
757
+ e = Math.max(e, extent_at(c, a));
758
+ }
759
+ return e;
760
+ };
761
+ /* The widest a set of cards is along the direction `th`, over
762
+ * the arc a-half..a+half: a stack's cards stand anywhere in its
763
+ * sector, and a card is wider slanted than square. */
764
+ let widest_along = (cards, a, half) => {
765
+ let e = 0;
766
+ for(let k = 0; k <= 16; k++) {
767
+ let th = a - half + 2 * half * k / 16;
768
+ for(let c of cards) {
769
+ e = Math.max(e, extent_at(c, th));
770
+ }
771
+ }
772
+ return e;
773
+ };
774
+ let depth_of = (id) => stacks.has(id)? stacks.get(id).depth : t.depth.get(id);
775
+ let circ = (a, b) => {
776
+ let d = Math.abs(a - b) % (2 * Math.PI);
777
+ return (d > Math.PI)? 2 * Math.PI - d : d;
778
+ };
779
+
780
+ /* What each node needs across its ring, the sector it deserves: a
781
+ * card is one, a stack is one ROW of cards, a parent the sum of
782
+ * its children. Post-order. */
783
+ let weight = new Map();
709
784
  for(let i = order.length - 1; i >= 0; i--) {
710
785
  let id = order[i];
711
- let kids = t.children.get(id) || [];
786
+ let st = stacks.get(id);
787
+ if(st) {
788
+ weight.set(id, st.per_row);
789
+ continue;
790
+ }
791
+ let kids = kids_of.get(id) || [];
712
792
  if(!kids.length) {
713
- leaves.set(id, 1);
793
+ weight.set(id, 1);
714
794
  continue;
715
795
  }
716
796
  let n = 0;
717
797
  for(let kid of kids) {
718
- n += leaves.get(kid);
798
+ n += weight.get(kid);
719
799
  }
720
- leaves.set(id, n);
800
+ weight.set(id, n);
721
801
  }
722
802
  let total = 0;
723
803
  for(let root of t.roots) {
724
- total += leaves.get(root);
804
+ total += weight.get(root);
725
805
  }
726
806
 
727
807
  /* One root sits at the centre; several sit on the first ring
728
808
  * around an empty centre, so every depth shifts out by one. */
729
809
  let single = (t.roots.length === 1);
730
- let ring_of = (id) => t.depth.get(id) + (single? 0 : 1);
810
+ let ring_of = (id) => depth_of(id) + (single? 0 : 1);
731
811
 
732
812
  /* Pass 1, the ANGLES: a node's children split its sector by
733
- * their leaves, and a node sits at the middle of its sector.
813
+ * their weight, and a node sits at the middle of its sector.
734
814
  * Angles do not depend on the radii, so they come first. */
735
815
  let angle = new Map();
736
- let rings = []; /* ring -> [{id, angle, diag}] */
816
+ let rings = []; /* ring -> [{id, angle, span, diag, ext, parent_ext}] */
737
817
  /* An explicit stack, not recursion: the sectors of a ring are
738
818
  * sorted by angle in pass 2, so the order they are pushed in does
739
819
  * not matter -- only that every node gets its own. */
740
820
  let sector = (root_id, root_from, root_span, root_parent) => {
741
- let stack = [{id: root_id, from: root_from, span: root_span, parent: root_parent}];
742
- while(stack.length) {
743
- let {id, from, span, parent} = stack.pop();
821
+ let todo = [{id: root_id, from: root_from, span: root_span, parent: root_parent}];
822
+ while(todo.length) {
823
+ let {id, from, span, parent} = todo.pop();
744
824
  let a = from + span / 2;
745
825
  angle.set(id, a);
746
826
  let ring = ring_of(id);
747
827
  if(!rings[ring]) {
748
828
  rings[ring] = [];
749
829
  }
750
- let n = t.by_id.get(id);
830
+ let st = stacks.get(id);
831
+ let n = st? null : t.by_id.get(id);
751
832
  rings[ring].push({
752
833
  id: id,
753
834
  angle: a,
754
- diag: Math.hypot(n.w, n.h),
835
+ span: span,
836
+ /* How much it needs along the ring: a card's diagonal,
837
+ * a stack's first row. */
838
+ diag: st? stack_across(st, a).row : Math.hypot(n.w, n.h),
755
839
  /* How much of the card lies along the RADIUS at this
756
840
  * angle -- the projection of the rectangle on that
757
841
  * direction -- for itself and for the node it hangs
758
842
  * from, which is the one it has to clear. */
759
- ext: extent_at(n, a),
760
- parent_ext: parent? extent_at(t.by_id.get(parent), a) : 0,
843
+ /* A stack is measured, and measures its parent, over
844
+ * the whole sector it can cover: round a parent its
845
+ * first row stands at every angle. */
846
+ ext: st? widest_along(st.cards, a, Math.min(span / 2, Math.PI))
847
+ : extent_at(n, a),
848
+ parent_ext: !parent? 0
849
+ : (st? widest_along([t.by_id.get(parent)], a, Math.min(span / 2, Math.PI))
850
+ : extent_at(t.by_id.get(parent), a)),
761
851
  });
762
852
  let cursor = from;
763
- for(let kid of (t.children.get(id) || [])) {
764
- let part = span * leaves.get(kid) / leaves.get(id);
765
- stack.push({id: kid, from: cursor, span: part, parent: id});
853
+ for(let kid of (kids_of.get(id) || [])) {
854
+ let part = span * weight.get(kid) / weight.get(id);
855
+ todo.push({id: kid, from: cursor, span: part, parent: id});
766
856
  cursor += part;
767
857
  }
768
858
  }
@@ -771,15 +861,15 @@ export function layout_radial(nodes, edges, opts)
771
861
  let root = t.roots[0];
772
862
  angle.set(root, 0);
773
863
  let cursor = -Math.PI / 2; /* the first child at the top */
774
- for(let kid of (t.children.get(root) || [])) {
775
- let part = 2 * Math.PI * leaves.get(kid) / leaves.get(root);
864
+ for(let kid of (kids_of.get(root) || [])) {
865
+ let part = 2 * Math.PI * weight.get(kid) / weight.get(root);
776
866
  sector(kid, cursor, part, root);
777
867
  cursor += part;
778
868
  }
779
869
  } else {
780
870
  let cursor = -Math.PI / 2;
781
871
  for(let root of t.roots) {
782
- let part = 2 * Math.PI * leaves.get(root) / total;
872
+ let part = 2 * Math.PI * weight.get(root) / total;
783
873
  sector(root, cursor, part, null);
784
874
  cursor += part;
785
875
  }
@@ -803,31 +893,132 @@ export function layout_radial(nodes, edges, opts)
803
893
  * the one it HANGS FROM, in the direction it hangs in -- the
804
894
  * two are radially aligned, or nearly, since a child lives
805
895
  * inside its parent's sector -- and the ring goes out far
806
- * enough for the worst of them. */
896
+ * enough for the worst of them.
897
+ *
898
+ * Then the STACKS of the ring get their rows: each row holds as
899
+ * many cards as fit in the room the stack has -- its own sector,
900
+ * less what its neighbours on the ring take up -- at that row's
901
+ * radius. A stack alone on its ring has the whole circle, and its
902
+ * rows are rings round its parent; one between neighbours fans
903
+ * out inside its sector, wider as it goes. The rows reaching out
904
+ * are neighbours of the outer rings too, by the ANGLE they cover. */
905
+ let fit = (list, r0) => {
906
+ let r = r0;
907
+ let sorted = list.slice().sort((a, b) => a.angle - b.angle);
908
+ for(let i = 0; i < sorted.length && sorted.length > 1; i++) {
909
+ let a = sorted[i];
910
+ let b = sorted[(i + 1) % sorted.length];
911
+ let gap = (i + 1 < sorted.length)
912
+ ? b.angle - a.angle
913
+ : b.angle + 2 * Math.PI - a.angle;
914
+ if(gap > 1e-9) {
915
+ r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
916
+ }
917
+ }
918
+ return r;
919
+ };
807
920
  let radius = [0];
921
+ let reaching = []; /* planned stacks: {angle, half, reach} */
808
922
  for(let ring = 1; ring < rings.length; ring++) {
923
+ let members = rings[ring] || [];
809
924
  let r = radius[ring - 1] + o.ranksep;
810
- for(let n of (rings[ring] || [])) {
925
+ let band = 0;
926
+ for(let n of members) {
811
927
  r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
928
+ band = Math.max(band, n.ext);
812
929
  }
813
- let list = (rings[ring] || []).slice().sort((a, b) => a.angle - b.angle);
814
- for(let i = 0; i < list.length && list.length > 1; i++) {
815
- let a = list[i];
816
- let b = list[(i + 1) % list.length];
817
- let gap = (i + 1 < list.length)
818
- ? b.angle - a.angle
819
- : b.angle + 2 * Math.PI - a.angle;
820
- if(gap > 1e-9) {
821
- r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
930
+ r = fit(members, r);
931
+ /* Clear the rows of the inner stacks that reach this far: by
932
+ * the angle they cover, or, for a card standing in it, by
933
+ * going past their reach. */
934
+ for(let pass = 0; pass < 2; pass++) {
935
+ for(let g of reaching) {
936
+ if(g.reach < r - band / 2 - o.nodesep) {
937
+ continue;
938
+ }
939
+ for(let m of members) {
940
+ let d = circ(g.angle, m.angle);
941
+ if(d > g.half + 1e-9) {
942
+ r = Math.max(r, (m.diag / 2 + o.nodesep) / (d - g.half));
943
+ } else {
944
+ r = Math.max(r, g.reach + m.ext / 2 + o.nodesep);
945
+ }
946
+ }
822
947
  }
823
948
  }
824
949
  radius[ring] = r;
950
+
951
+ let sorted = members.slice().sort((a, b) => a.angle - b.angle);
952
+ for(let i = 0; i < sorted.length; i++) {
953
+ let m = sorted[i];
954
+ let st = stacks.get(m.id);
955
+ if(!st) {
956
+ continue;
957
+ }
958
+ /* The room each side: to the neighbour, less its half and
959
+ * the gap; never past the stack's own sector. */
960
+ let room = m.span / 2;
961
+ if(sorted.length > 1) {
962
+ let left = sorted[(i - 1 + sorted.length) % sorted.length];
963
+ let right = sorted[(i + 1) % sorted.length];
964
+ room = Math.min(room,
965
+ circ(m.angle, left.angle) - (left.diag / 2 + o.nodesep) / r,
966
+ circ(m.angle, right.angle) - (right.diag / 2 + o.nodesep) / r);
967
+ }
968
+ room = Math.min(room, Math.PI);
969
+ /* Measured over the whole arc it will cover, not at its
970
+ * middle: round a parent a card stands at every angle,
971
+ * and a 172x96 card is wider at 45 degrees than facing
972
+ * down. */
973
+ let across = 0;
974
+ let along = 0;
975
+ for(let k = 0; k <= 16; k++) {
976
+ let th = m.angle - room + 2 * room * k / 16;
977
+ for(let c of st.cards) {
978
+ across = Math.max(across, extent_at(c, th + Math.PI / 2));
979
+ along = Math.max(along, extent_at(c, th));
980
+ }
981
+ }
982
+ let rows = [];
983
+ let left_to_place = st.ids.length;
984
+ let rr = r;
985
+ let half = 0;
986
+ while(left_to_place > 0) {
987
+ let fits = Math.floor(2 * room * rr / (across + o.nodesep));
988
+ let cnt = Math.max(1, Math.min(left_to_place, fits));
989
+ rows.push({rr: rr, cnt: cnt});
990
+ half = Math.max(half, ((cnt * (across + o.nodesep) - o.nodesep) / 2) / rr);
991
+ left_to_place -= cnt;
992
+ rr += along + STACK_GAP;
993
+ }
994
+ st.plan = {angle: m.angle, across: across, rows: rows};
995
+ reaching.push({
996
+ angle: m.angle,
997
+ half: half,
998
+ reach: rows[rows.length - 1].rr + along / 2,
999
+ });
1000
+ }
825
1001
  }
826
1002
 
827
- /* Pass 3, the positions. */
1003
+ /* Pass 3, the positions: a node at its angle on its ring; a
1004
+ * stack's cards row by row outward, each row centred on the
1005
+ * stack's angle, a card's width plus the gap apart along the arc. */
828
1006
  for(let [id, a] of angle) {
829
- let r = radius[ring_of(id)] || 0;
830
- pos.set(id, {x: r * Math.cos(a), y: r * Math.sin(a)});
1007
+ let st = stacks.get(id);
1008
+ if(!st) {
1009
+ let r0 = radius[ring_of(id)] || 0;
1010
+ pos.set(id, {x: r0 * Math.cos(a), y: r0 * Math.sin(a)});
1011
+ continue;
1012
+ }
1013
+ let i = 0;
1014
+ for(let row of st.plan.rows) {
1015
+ let step = (st.plan.across + o.nodesep) / row.rr;
1016
+ for(let j = 0; j < row.cnt; j++) {
1017
+ let th = a + (j - (row.cnt - 1) / 2) * step;
1018
+ pos.set(st.ids[i], {x: row.rr * Math.cos(th), y: row.rr * Math.sin(th)});
1019
+ i++;
1020
+ }
1021
+ }
831
1022
  }
832
1023
 
833
1024
  return pos;
@@ -325,7 +325,7 @@ describe("the tidy tree, read right", () => {
325
325
 
326
326
  describe("the radial tree", () => {
327
327
  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});
328
+ let pos = layout_radial(nodes, edges, {nodesep: 10, ranksep: 100, stack: false});
329
329
  expect(pos.get("es")).toEqual({x: 0, y: 0});
330
330
  let r = Math.hypot(pos.get("norte").x, pos.get("norte").y);
331
331
  expect(r).toBeGreaterThanOrEqual(100);
@@ -346,7 +346,7 @@ describe("the radial tree", () => {
346
346
  many.push(N(`k${i}`, 172, 96));
347
347
  e.push({source: "root", target: `k${i}`, rank: 0});
348
348
  }
349
- let pos = layout_radial(many, e, {nodesep: 10, ranksep: 100});
349
+ let pos = layout_radial(many, e, {nodesep: 10, ranksep: 100, stack: false});
350
350
  let r = Math.hypot(pos.get("k0").x, pos.get("k0").y);
351
351
  /* 40 diagonals of ~197 plus gaps on the circumference */
352
352
  expect(2 * Math.PI * r).toBeGreaterThanOrEqual(40 * (Math.hypot(172, 96) + 10) - 1e-6);
@@ -380,6 +380,99 @@ describe("the radial tree", () => {
380
380
  });
381
381
  });
382
382
 
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
+
383
476
  describe("a deep tree", () => {
384
477
  /* A self-referent hook -- a place inside a place inside a place --
385
478
  * is as deep as the data says. The walks are iterative for exactly