@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 +9 -1
- package/dist/gobj-ui.cjs.js +215 -66
- package/dist/gobj-ui.es.js +215 -66
- package/package.json +1 -1
- package/src/treedb_layout.js +274 -83
- package/src/treedb_layout.test.js +95 -2
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
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -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
|
|
29466
|
-
let
|
|
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
|
|
29640
|
-
|
|
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
|
|
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
|
-
|
|
29710
|
+
weight.set(id, 1);
|
|
29646
29711
|
continue;
|
|
29647
29712
|
}
|
|
29648
29713
|
let n = 0;
|
|
29649
|
-
for (let kid of kids) n +=
|
|
29650
|
-
|
|
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 +=
|
|
29718
|
+
for (let root of t.roots) total += weight.get(root);
|
|
29654
29719
|
let single = t.roots.length === 1;
|
|
29655
|
-
let ring_of = (id) =>
|
|
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
|
|
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 (
|
|
29666
|
-
let { id, from, span, parent } =
|
|
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
|
|
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
|
-
|
|
29676
|
-
|
|
29677
|
-
|
|
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
|
|
29681
|
-
let part = span *
|
|
29682
|
-
|
|
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
|
|
29697
|
-
let part = 2 * Math.PI *
|
|
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 *
|
|
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
|
-
|
|
29713
|
-
|
|
29714
|
-
|
|
29715
|
-
|
|
29716
|
-
|
|
29717
|
-
|
|
29718
|
-
|
|
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
|
|
29724
|
-
|
|
29725
|
-
|
|
29726
|
-
|
|
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
|
}
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -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
|
|
29461
|
-
let
|
|
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
|
|
29635
|
-
|
|
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
|
|
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
|
-
|
|
29705
|
+
weight.set(id, 1);
|
|
29641
29706
|
continue;
|
|
29642
29707
|
}
|
|
29643
29708
|
let n = 0;
|
|
29644
|
-
for (let kid of kids) n +=
|
|
29645
|
-
|
|
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 +=
|
|
29713
|
+
for (let root of t.roots) total += weight.get(root);
|
|
29649
29714
|
let single = t.roots.length === 1;
|
|
29650
|
-
let ring_of = (id) =>
|
|
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
|
|
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 (
|
|
29661
|
-
let { id, from, span, parent } =
|
|
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
|
|
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
|
-
|
|
29671
|
-
|
|
29672
|
-
|
|
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
|
|
29676
|
-
let part = span *
|
|
29677
|
-
|
|
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
|
|
29692
|
-
let part = 2 * Math.PI *
|
|
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 *
|
|
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
|
-
|
|
29708
|
-
|
|
29709
|
-
|
|
29710
|
-
|
|
29711
|
-
|
|
29712
|
-
|
|
29713
|
-
|
|
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
|
|
29719
|
-
|
|
29720
|
-
|
|
29721
|
-
|
|
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
package/src/treedb_layout.js
CHANGED
|
@@ -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
|
-
/*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
707
|
-
let
|
|
708
|
-
let
|
|
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
|
|
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
|
-
|
|
793
|
+
weight.set(id, 1);
|
|
714
794
|
continue;
|
|
715
795
|
}
|
|
716
796
|
let n = 0;
|
|
717
797
|
for(let kid of kids) {
|
|
718
|
-
n +=
|
|
798
|
+
n += weight.get(kid);
|
|
719
799
|
}
|
|
720
|
-
|
|
800
|
+
weight.set(id, n);
|
|
721
801
|
}
|
|
722
802
|
let total = 0;
|
|
723
803
|
for(let root of t.roots) {
|
|
724
|
-
total +=
|
|
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) =>
|
|
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
|
|
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
|
|
742
|
-
while(
|
|
743
|
-
let {id, from, span, parent} =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
760
|
-
|
|
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 (
|
|
764
|
-
let part = span *
|
|
765
|
-
|
|
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 (
|
|
775
|
-
let part = 2 * Math.PI *
|
|
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 *
|
|
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
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
|
830
|
-
|
|
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
|