@yuneta/gobj-ui 7.23.72 → 7.23.74

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
@@ -1882,6 +1882,35 @@ the middle, and **a toolbar left with a single group is centred**. An unknown
1882
1882
  name is reported, not silently dropped: a typo would otherwise remove the save
1883
1883
  button with no trace of why.
1884
1884
 
1885
+ ### `C_YUI_FORM` — which field is editable, and the one `writable` does not govern
1886
+
1887
+ A form opened to LOOK at a record has no editable field: that is a property of
1888
+ the opening (`readonly`), not of the schema, and it wins over everything. Under
1889
+ it the schema decides — a column needs `writable` — **with one exception, and
1890
+ it is the whole of linking: an fkey.**
1891
+
1892
+ `writable` governs the WRITE of a column's VALUE, and an fkey is not written,
1893
+ it is **linked**. An fkey is normally declared with no `writable` flag at all
1894
+ (`treedb_authzs`'s `users.roles` is `['fkey']`, and so is almost every fkey in
1895
+ the tree), so reading "not writable" as "not editable" takes away the only way
1896
+ to link a record to its parent — with `users.roles`, the only way to give a
1897
+ person a role. The other half of the same rule is in the topic view, where the
1898
+ record travels back: it sends "the writable cols, the fkeys (a link is edited
1899
+ by linking) and the pkey". The two halves have to agree; when they did not, the
1900
+ form disabled the control whose value the save was still waiting for.
1901
+
1902
+ The rule lives in `form_field_readonly.js`, pure and tested. A `file` column is
1903
+ an fkey too (`['fkey','file']`) and is deliberately NOT covered: it answers
1904
+ `type: "file"`, its bytes travel beside the record, and the SDK declares as
1905
+ legal a `file` column that only a load fills.
1906
+
1907
+ **And `readonly` is an attribute of a TEXT control and of nothing else.** A
1908
+ `<select>`, a checkbox and a radio accept it and let the reader change the
1909
+ value anyway; only `disabled` stops them, and tom-select has to be told through
1910
+ its API. So the form applies `disabled` to those three, and the record is still
1911
+ read from a disabled control — it is collected by reading each marked control,
1912
+ never by submitting the form.
1913
+
1885
1914
  ### Selecting rows in any table — `yui_table_select.js`
1886
1915
 
1887
1916
  Deleting twenty rows one confirmation at a time is not a workflow. Any view
@@ -27370,7 +27370,7 @@ function fold_reveal(model, state, node_key) {
27370
27370
  /***********************************************************************
27371
27371
  * treedb_layout.js
27372
27372
  *
27373
- * Two layouts made for a treedb, and nothing else.
27373
+ * Three layouts made for a treedb, and nothing else.
27374
27374
  *
27375
27375
  * What a treedb graph IS, once it is folded (treedb_fold_model.js):
27376
27376
  * a forest read left to right. A main topic is a tree (places:
@@ -27394,8 +27394,9 @@ function fold_reveal(model, state, node_key) {
27394
27394
  * - the force family scatters a tree, which is the pile with
27395
27395
  * physics.
27396
27396
  *
27397
- * So: a tidy tree (`tree`) and an outline (`outline`), both fed by
27398
- * the SAME spanning tree, chosen deterministically:
27397
+ * So: a tidy tree (`tree`), an outline (`outline`) and a radial
27398
+ * tree (`radial`), all three fed by the SAME spanning tree, chosen
27399
+ * deterministically:
27399
27400
  *
27400
27401
  * - roots are the nodes with no incoming edge, in node order;
27401
27402
  * - a node belongs to the FIRST parent that reaches it in a
@@ -27418,8 +27419,13 @@ function fold_reveal(model, state, node_key) {
27418
27419
  * viewer draws, and what a treedb is when read as one. Tall, but
27419
27420
  * nothing is ever beside anything: the reader scrolls a list.
27420
27421
  *
27422
+ * `radial`: the root in the middle and a ring per depth, each
27423
+ * subtree owning a sector proportional to its leaves; the radius
27424
+ * of a ring grows until its neighbours sit side by side, so
27425
+ * nothing overlaps by construction (see `layout_radial`).
27426
+ *
27421
27427
  * Pure: takes plain nodes and edges, returns positions. The G6
27422
- * classes in c_g6_nodes_tree.js are two thin adapters over it.
27428
+ * classes in c_g6_nodes_tree.js are three thin adapters over it.
27423
27429
  *
27424
27430
  * Copyright (c) 2026, ArtGins.
27425
27431
  * All Rights Reserved.
@@ -27492,6 +27498,28 @@ function spanning_tree(nodes, edges) {
27492
27498
  };
27493
27499
  }
27494
27500
  /************************************************************
27501
+ * The ids of a spanning tree in PARENT-BEFORE-CHILD order.
27502
+ *
27503
+ * Every pass below used to be a recursive walk, which is the natural
27504
+ * way to write it and the one that dies on a deep tree: a
27505
+ * self-referent hook (a place inside a place inside a place) is as
27506
+ * deep as the data says, and the stack is not. Read this list
27507
+ * forwards for a pre-order pass and BACKWARDS for a post-order one
27508
+ * -- a child always sits after its parent, so reversed it always
27509
+ * sits before it, which is the whole of what post-order was for.
27510
+ ************************************************************/
27511
+ function walk_order(t) {
27512
+ let order = [];
27513
+ let stack = t.roots.slice().reverse();
27514
+ while (stack.length) {
27515
+ let id = stack.pop();
27516
+ order.push(id);
27517
+ let kids = t.children.get(id) || [];
27518
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27519
+ }
27520
+ return order;
27521
+ }
27522
+ /************************************************************
27495
27523
  * The tidy tree, left to right.
27496
27524
  *
27497
27525
  * Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
@@ -27512,56 +27540,45 @@ function layout_tree(nodes, edges, opts) {
27512
27540
  col_x[d] = x;
27513
27541
  x += (col_w[d] || 0) + o.ranksep;
27514
27542
  }
27543
+ let order = walk_order(t);
27515
27544
  let height = /* @__PURE__ */ new Map();
27516
- let measure = (id) => {
27545
+ for (let i = order.length - 1; i >= 0; i--) {
27546
+ let id = order[i];
27517
27547
  let kids = t.children.get(id) || [];
27518
27548
  let own = t.by_id.get(id).h;
27519
- if (!kids.length) {
27520
- height.set(id, own);
27521
- return own;
27522
- }
27523
27549
  let sum = 0;
27524
- for (let i = 0; i < kids.length; i++) {
27525
- sum += measure(kids[i]);
27526
- if (i > 0) sum += o.nodesep;
27550
+ for (let k = 0; k < kids.length; k++) {
27551
+ sum += height.get(kids[k]);
27552
+ if (k > 0) sum += o.nodesep;
27527
27553
  }
27528
- let h = Math.max(own, sum);
27529
- height.set(id, h);
27530
- return h;
27531
- };
27532
- for (let root of t.roots) measure(root);
27533
- let place = (id, top) => {
27534
- let n = t.by_id.get(id);
27554
+ height.set(id, kids.length ? Math.max(own, sum) : own);
27555
+ }
27556
+ let tops = /* @__PURE__ */ new Map();
27557
+ let top = 0;
27558
+ for (let root of t.roots) {
27559
+ tops.set(root, top);
27560
+ top += height.get(root) + o.nodesep * 2;
27561
+ }
27562
+ for (let id of order) {
27535
27563
  let d = t.depth.get(id);
27536
27564
  let kids = t.children.get(id) || [];
27537
27565
  let block = height.get(id);
27538
- let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
27539
- if (!kids.length) {
27540
- pos.set(id, {
27541
- x: cx,
27542
- y: top + block / 2
27543
- });
27544
- return;
27545
- }
27566
+ let own_top = tops.get(id);
27567
+ pos.set(id, {
27568
+ x: col_x[d] + col_w[d] / 2,
27569
+ y: own_top + block / 2
27570
+ });
27571
+ if (!kids.length) continue;
27546
27572
  let kids_h = 0;
27547
- for (let i = 0; i < kids.length; i++) {
27548
- kids_h += height.get(kids[i]);
27549
- if (i > 0) kids_h += o.nodesep;
27573
+ for (let k = 0; k < kids.length; k++) {
27574
+ kids_h += height.get(kids[k]);
27575
+ if (k > 0) kids_h += o.nodesep;
27550
27576
  }
27551
- let cursor = top + (block - kids_h) / 2;
27577
+ let cursor = own_top + (block - kids_h) / 2;
27552
27578
  for (let kid of kids) {
27553
- place(kid, cursor);
27579
+ tops.set(kid, cursor);
27554
27580
  cursor += height.get(kid) + o.nodesep;
27555
27581
  }
27556
- pos.set(id, {
27557
- x: cx,
27558
- y: top + block / 2
27559
- });
27560
- };
27561
- let top = 0;
27562
- for (let root of t.roots) {
27563
- place(root, top);
27564
- top += height.get(root) + o.nodesep * 2;
27565
27582
  }
27566
27583
  return pos;
27567
27584
  }
@@ -27588,40 +27605,53 @@ function layout_radial(nodes, edges, opts) {
27588
27605
  let t = spanning_tree(nodes, edges);
27589
27606
  let pos = /* @__PURE__ */ new Map();
27590
27607
  if (!t.roots.length) return pos;
27608
+ let order = walk_order(t);
27591
27609
  let leaves = /* @__PURE__ */ new Map();
27592
- let count = (id) => {
27610
+ for (let i = order.length - 1; i >= 0; i--) {
27611
+ let id = order[i];
27593
27612
  let kids = t.children.get(id) || [];
27594
27613
  if (!kids.length) {
27595
27614
  leaves.set(id, 1);
27596
- return 1;
27615
+ continue;
27597
27616
  }
27598
27617
  let n = 0;
27599
- for (let kid of kids) n += count(kid);
27618
+ for (let kid of kids) n += leaves.get(kid);
27600
27619
  leaves.set(id, n);
27601
- return n;
27602
- };
27620
+ }
27603
27621
  let total = 0;
27604
- for (let root of t.roots) total += count(root);
27622
+ for (let root of t.roots) total += leaves.get(root);
27605
27623
  let single = t.roots.length === 1;
27606
27624
  let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
27607
27625
  let angle = /* @__PURE__ */ new Map();
27608
27626
  let rings = [];
27609
- let sector = (id, from, span) => {
27610
- let a = from + span / 2;
27611
- angle.set(id, a);
27612
- let ring = ring_of(id);
27613
- if (!rings[ring]) rings[ring] = [];
27614
- let n = t.by_id.get(id);
27615
- rings[ring].push({
27616
- id,
27617
- angle: a,
27618
- diag: Math.hypot(n.w, n.h)
27619
- });
27620
- let cursor = from;
27621
- for (let kid of t.children.get(id) || []) {
27622
- let part = span * leaves.get(kid) / leaves.get(id);
27623
- sector(kid, cursor, part);
27624
- cursor += part;
27627
+ let sector = (root_id, root_from, root_span) => {
27628
+ let stack = [{
27629
+ id: root_id,
27630
+ from: root_from,
27631
+ span: root_span
27632
+ }];
27633
+ while (stack.length) {
27634
+ let { id, from, span } = stack.pop();
27635
+ let a = from + span / 2;
27636
+ angle.set(id, a);
27637
+ let ring = ring_of(id);
27638
+ if (!rings[ring]) rings[ring] = [];
27639
+ let n = t.by_id.get(id);
27640
+ rings[ring].push({
27641
+ id,
27642
+ angle: a,
27643
+ diag: Math.hypot(n.w, n.h)
27644
+ });
27645
+ let cursor = from;
27646
+ for (let kid of t.children.get(id) || []) {
27647
+ let part = span * leaves.get(kid) / leaves.get(id);
27648
+ stack.push({
27649
+ id: kid,
27650
+ from: cursor,
27651
+ span: part
27652
+ });
27653
+ cursor += part;
27654
+ }
27625
27655
  }
27626
27656
  };
27627
27657
  if (single) {
@@ -27672,15 +27702,20 @@ function layout_outline(nodes, edges, opts) {
27672
27702
  let t = spanning_tree(nodes, edges);
27673
27703
  let pos = /* @__PURE__ */ new Map();
27674
27704
  let y = 0;
27675
- let visit = (id) => {
27676
- let n = t.by_id.get(id);
27677
- let d = t.depth.get(id);
27678
- pos.set(id, {
27679
- x: d * o.indent + n.w / 2,
27680
- y: y + n.h / 2
27681
- });
27682
- y += n.h + o.nodesep;
27683
- for (let kid of t.children.get(id) || []) visit(kid);
27705
+ let visit = (root_id) => {
27706
+ let stack = [root_id];
27707
+ while (stack.length) {
27708
+ let id = stack.pop();
27709
+ let n = t.by_id.get(id);
27710
+ let d = t.depth.get(id);
27711
+ pos.set(id, {
27712
+ x: d * o.indent + n.w / 2,
27713
+ y: y + n.h / 2
27714
+ });
27715
+ y += n.h + o.nodesep;
27716
+ let kids = t.children.get(id) || [];
27717
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27718
+ }
27684
27719
  };
27685
27720
  for (let root of t.roots) {
27686
27721
  visit(root);
@@ -62107,6 +62142,8 @@ function ac_set_operation_mode(gobj, event, kw, src) {
62107
62142
  * into the URL, so what you are looking at stays linkable.
62108
62143
  ************************************************************/
62109
62144
  var LEGEND_GLYPH_STYLE = "font-size:1.5em; line-height:1; color:inherit;";
62145
+ var LEGEND_NAME_STYLE = "font-size:1.2em; line-height:1.2;";
62146
+ var LEGEND_COUNT_STYLE = "margin-left:.4rem; font-size:1em;";
62110
62147
  function refresh_legend(gobj) {
62111
62148
  let priv = gobj.priv;
62112
62149
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
@@ -62132,21 +62169,21 @@ function refresh_legend(gobj) {
62132
62169
  "span",
62133
62170
  {
62134
62171
  class: "GRAPH_LEGEND_NAME",
62135
- style: hidden ? "text-decoration:line-through; opacity:.6;" : ""
62172
+ style: LEGEND_NAME_STYLE + (hidden ? " text-decoration:line-through; opacity:.6;" : "")
62136
62173
  },
62137
62174
  topic_name
62138
62175
  ], [
62139
62176
  "span",
62140
62177
  {
62141
62178
  class: "GRAPH_LEGEND_COUNT has-text-grey",
62142
- style: "margin-left:.35rem; font-size:.8em;"
62179
+ style: LEGEND_COUNT_STYLE
62143
62180
  },
62144
62181
  hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
62145
62182
  ]);
62146
62183
  let body_attrs = {
62147
- class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " is-primary is-light" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62184
+ class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " GRAPH_LEGEND_FOCUSED" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62148
62185
  type: "button",
62149
- style: "gap:0; flex:0 0 auto;",
62186
+ style: "gap:0; flex:0 0 auto;" + (focused ? " box-shadow: inset 0 0 0 2px var(--bulma-text, #2e333d);" : ""),
62150
62187
  title: is_main ? (0, i18next.t)("main topic") : hidden ? (0, i18next.t)("show topic") : (0, i18next.t)("hide topic"),
62151
62188
  "aria-label": topic_name,
62152
62189
  "aria-pressed": hidden ? "false" : "true"
@@ -62168,7 +62205,7 @@ function refresh_legend(gobj) {
62168
62205
  if (is_main && state.main_chosen) extras.push([
62169
62206
  "button",
62170
62207
  {
62171
- class: "GRAPH_LEGEND_STAR button is-small is-warning is-light",
62208
+ class: "GRAPH_LEGEND_STAR button is-small pressed_state",
62172
62209
  type: "button",
62173
62210
  style: "padding:0 .4rem;",
62174
62211
  title: (0, i18next.t)("main topic"),
@@ -62229,7 +62266,7 @@ function refresh_legend(gobj) {
62229
62266
  if (!hidden && entry.linked && entry.loose > 0) extras.push([
62230
62267
  "button",
62231
62268
  {
62232
- class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " is-warning is-light" : ""),
62269
+ class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " pressed_state" : ""),
62233
62270
  type: "button",
62234
62271
  style: "padding:0 .45rem;",
62235
62272
  title: (0, i18next.t)("loose records"),
@@ -62249,7 +62286,7 @@ function refresh_legend(gobj) {
62249
62286
  if (!hidden) extras.push([
62250
62287
  "button",
62251
62288
  {
62252
- class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " is-primary is-light" : ""),
62289
+ class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " pressed_state" : ""),
62253
62290
  type: "button",
62254
62291
  style: "padding:0 .4rem;",
62255
62292
  title: (0, i18next.t)("highlight topic"),
@@ -27365,7 +27365,7 @@ function fold_reveal(model, state, node_key) {
27365
27365
  /***********************************************************************
27366
27366
  * treedb_layout.js
27367
27367
  *
27368
- * Two layouts made for a treedb, and nothing else.
27368
+ * Three layouts made for a treedb, and nothing else.
27369
27369
  *
27370
27370
  * What a treedb graph IS, once it is folded (treedb_fold_model.js):
27371
27371
  * a forest read left to right. A main topic is a tree (places:
@@ -27389,8 +27389,9 @@ function fold_reveal(model, state, node_key) {
27389
27389
  * - the force family scatters a tree, which is the pile with
27390
27390
  * physics.
27391
27391
  *
27392
- * So: a tidy tree (`tree`) and an outline (`outline`), both fed by
27393
- * the SAME spanning tree, chosen deterministically:
27392
+ * So: a tidy tree (`tree`), an outline (`outline`) and a radial
27393
+ * tree (`radial`), all three fed by the SAME spanning tree, chosen
27394
+ * deterministically:
27394
27395
  *
27395
27396
  * - roots are the nodes with no incoming edge, in node order;
27396
27397
  * - a node belongs to the FIRST parent that reaches it in a
@@ -27413,8 +27414,13 @@ function fold_reveal(model, state, node_key) {
27413
27414
  * viewer draws, and what a treedb is when read as one. Tall, but
27414
27415
  * nothing is ever beside anything: the reader scrolls a list.
27415
27416
  *
27417
+ * `radial`: the root in the middle and a ring per depth, each
27418
+ * subtree owning a sector proportional to its leaves; the radius
27419
+ * of a ring grows until its neighbours sit side by side, so
27420
+ * nothing overlaps by construction (see `layout_radial`).
27421
+ *
27416
27422
  * Pure: takes plain nodes and edges, returns positions. The G6
27417
- * classes in c_g6_nodes_tree.js are two thin adapters over it.
27423
+ * classes in c_g6_nodes_tree.js are three thin adapters over it.
27418
27424
  *
27419
27425
  * Copyright (c) 2026, ArtGins.
27420
27426
  * All Rights Reserved.
@@ -27487,6 +27493,28 @@ function spanning_tree(nodes, edges) {
27487
27493
  };
27488
27494
  }
27489
27495
  /************************************************************
27496
+ * The ids of a spanning tree in PARENT-BEFORE-CHILD order.
27497
+ *
27498
+ * Every pass below used to be a recursive walk, which is the natural
27499
+ * way to write it and the one that dies on a deep tree: a
27500
+ * self-referent hook (a place inside a place inside a place) is as
27501
+ * deep as the data says, and the stack is not. Read this list
27502
+ * forwards for a pre-order pass and BACKWARDS for a post-order one
27503
+ * -- a child always sits after its parent, so reversed it always
27504
+ * sits before it, which is the whole of what post-order was for.
27505
+ ************************************************************/
27506
+ function walk_order(t) {
27507
+ let order = [];
27508
+ let stack = t.roots.slice().reverse();
27509
+ while (stack.length) {
27510
+ let id = stack.pop();
27511
+ order.push(id);
27512
+ let kids = t.children.get(id) || [];
27513
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27514
+ }
27515
+ return order;
27516
+ }
27517
+ /************************************************************
27490
27518
  * The tidy tree, left to right.
27491
27519
  *
27492
27520
  * Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
@@ -27507,56 +27535,45 @@ function layout_tree(nodes, edges, opts) {
27507
27535
  col_x[d] = x;
27508
27536
  x += (col_w[d] || 0) + o.ranksep;
27509
27537
  }
27538
+ let order = walk_order(t);
27510
27539
  let height = /* @__PURE__ */ new Map();
27511
- let measure = (id) => {
27540
+ for (let i = order.length - 1; i >= 0; i--) {
27541
+ let id = order[i];
27512
27542
  let kids = t.children.get(id) || [];
27513
27543
  let own = t.by_id.get(id).h;
27514
- if (!kids.length) {
27515
- height.set(id, own);
27516
- return own;
27517
- }
27518
27544
  let sum = 0;
27519
- for (let i = 0; i < kids.length; i++) {
27520
- sum += measure(kids[i]);
27521
- if (i > 0) sum += o.nodesep;
27545
+ for (let k = 0; k < kids.length; k++) {
27546
+ sum += height.get(kids[k]);
27547
+ if (k > 0) sum += o.nodesep;
27522
27548
  }
27523
- let h = Math.max(own, sum);
27524
- height.set(id, h);
27525
- return h;
27526
- };
27527
- for (let root of t.roots) measure(root);
27528
- let place = (id, top) => {
27529
- let n = t.by_id.get(id);
27549
+ height.set(id, kids.length ? Math.max(own, sum) : own);
27550
+ }
27551
+ let tops = /* @__PURE__ */ new Map();
27552
+ let top = 0;
27553
+ for (let root of t.roots) {
27554
+ tops.set(root, top);
27555
+ top += height.get(root) + o.nodesep * 2;
27556
+ }
27557
+ for (let id of order) {
27530
27558
  let d = t.depth.get(id);
27531
27559
  let kids = t.children.get(id) || [];
27532
27560
  let block = height.get(id);
27533
- let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
27534
- if (!kids.length) {
27535
- pos.set(id, {
27536
- x: cx,
27537
- y: top + block / 2
27538
- });
27539
- return;
27540
- }
27561
+ let own_top = tops.get(id);
27562
+ pos.set(id, {
27563
+ x: col_x[d] + col_w[d] / 2,
27564
+ y: own_top + block / 2
27565
+ });
27566
+ if (!kids.length) continue;
27541
27567
  let kids_h = 0;
27542
- for (let i = 0; i < kids.length; i++) {
27543
- kids_h += height.get(kids[i]);
27544
- if (i > 0) kids_h += o.nodesep;
27568
+ for (let k = 0; k < kids.length; k++) {
27569
+ kids_h += height.get(kids[k]);
27570
+ if (k > 0) kids_h += o.nodesep;
27545
27571
  }
27546
- let cursor = top + (block - kids_h) / 2;
27572
+ let cursor = own_top + (block - kids_h) / 2;
27547
27573
  for (let kid of kids) {
27548
- place(kid, cursor);
27574
+ tops.set(kid, cursor);
27549
27575
  cursor += height.get(kid) + o.nodesep;
27550
27576
  }
27551
- pos.set(id, {
27552
- x: cx,
27553
- y: top + block / 2
27554
- });
27555
- };
27556
- let top = 0;
27557
- for (let root of t.roots) {
27558
- place(root, top);
27559
- top += height.get(root) + o.nodesep * 2;
27560
27577
  }
27561
27578
  return pos;
27562
27579
  }
@@ -27583,40 +27600,53 @@ function layout_radial(nodes, edges, opts) {
27583
27600
  let t = spanning_tree(nodes, edges);
27584
27601
  let pos = /* @__PURE__ */ new Map();
27585
27602
  if (!t.roots.length) return pos;
27603
+ let order = walk_order(t);
27586
27604
  let leaves = /* @__PURE__ */ new Map();
27587
- let count = (id) => {
27605
+ for (let i = order.length - 1; i >= 0; i--) {
27606
+ let id = order[i];
27588
27607
  let kids = t.children.get(id) || [];
27589
27608
  if (!kids.length) {
27590
27609
  leaves.set(id, 1);
27591
- return 1;
27610
+ continue;
27592
27611
  }
27593
27612
  let n = 0;
27594
- for (let kid of kids) n += count(kid);
27613
+ for (let kid of kids) n += leaves.get(kid);
27595
27614
  leaves.set(id, n);
27596
- return n;
27597
- };
27615
+ }
27598
27616
  let total = 0;
27599
- for (let root of t.roots) total += count(root);
27617
+ for (let root of t.roots) total += leaves.get(root);
27600
27618
  let single = t.roots.length === 1;
27601
27619
  let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
27602
27620
  let angle = /* @__PURE__ */ new Map();
27603
27621
  let rings = [];
27604
- let sector = (id, from, span) => {
27605
- let a = from + span / 2;
27606
- angle.set(id, a);
27607
- let ring = ring_of(id);
27608
- if (!rings[ring]) rings[ring] = [];
27609
- let n = t.by_id.get(id);
27610
- rings[ring].push({
27611
- id,
27612
- angle: a,
27613
- diag: Math.hypot(n.w, n.h)
27614
- });
27615
- let cursor = from;
27616
- for (let kid of t.children.get(id) || []) {
27617
- let part = span * leaves.get(kid) / leaves.get(id);
27618
- sector(kid, cursor, part);
27619
- cursor += part;
27622
+ let sector = (root_id, root_from, root_span) => {
27623
+ let stack = [{
27624
+ id: root_id,
27625
+ from: root_from,
27626
+ span: root_span
27627
+ }];
27628
+ while (stack.length) {
27629
+ let { id, from, span } = stack.pop();
27630
+ let a = from + span / 2;
27631
+ angle.set(id, a);
27632
+ let ring = ring_of(id);
27633
+ if (!rings[ring]) rings[ring] = [];
27634
+ let n = t.by_id.get(id);
27635
+ rings[ring].push({
27636
+ id,
27637
+ angle: a,
27638
+ diag: Math.hypot(n.w, n.h)
27639
+ });
27640
+ let cursor = from;
27641
+ for (let kid of t.children.get(id) || []) {
27642
+ let part = span * leaves.get(kid) / leaves.get(id);
27643
+ stack.push({
27644
+ id: kid,
27645
+ from: cursor,
27646
+ span: part
27647
+ });
27648
+ cursor += part;
27649
+ }
27620
27650
  }
27621
27651
  };
27622
27652
  if (single) {
@@ -27667,15 +27697,20 @@ function layout_outline(nodes, edges, opts) {
27667
27697
  let t = spanning_tree(nodes, edges);
27668
27698
  let pos = /* @__PURE__ */ new Map();
27669
27699
  let y = 0;
27670
- let visit = (id) => {
27671
- let n = t.by_id.get(id);
27672
- let d = t.depth.get(id);
27673
- pos.set(id, {
27674
- x: d * o.indent + n.w / 2,
27675
- y: y + n.h / 2
27676
- });
27677
- y += n.h + o.nodesep;
27678
- for (let kid of t.children.get(id) || []) visit(kid);
27700
+ let visit = (root_id) => {
27701
+ let stack = [root_id];
27702
+ while (stack.length) {
27703
+ let id = stack.pop();
27704
+ let n = t.by_id.get(id);
27705
+ let d = t.depth.get(id);
27706
+ pos.set(id, {
27707
+ x: d * o.indent + n.w / 2,
27708
+ y: y + n.h / 2
27709
+ });
27710
+ y += n.h + o.nodesep;
27711
+ let kids = t.children.get(id) || [];
27712
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27713
+ }
27679
27714
  };
27680
27715
  for (let root of t.roots) {
27681
27716
  visit(root);
@@ -62102,6 +62137,8 @@ function ac_set_operation_mode(gobj, event, kw, src) {
62102
62137
  * into the URL, so what you are looking at stays linkable.
62103
62138
  ************************************************************/
62104
62139
  var LEGEND_GLYPH_STYLE = "font-size:1.5em; line-height:1; color:inherit;";
62140
+ var LEGEND_NAME_STYLE = "font-size:1.2em; line-height:1.2;";
62141
+ var LEGEND_COUNT_STYLE = "margin-left:.4rem; font-size:1em;";
62105
62142
  function refresh_legend(gobj) {
62106
62143
  let priv = gobj.priv;
62107
62144
  let $container = gobj_read_attr(gobj, "$container");
@@ -62127,21 +62164,21 @@ function refresh_legend(gobj) {
62127
62164
  "span",
62128
62165
  {
62129
62166
  class: "GRAPH_LEGEND_NAME",
62130
- style: hidden ? "text-decoration:line-through; opacity:.6;" : ""
62167
+ style: LEGEND_NAME_STYLE + (hidden ? " text-decoration:line-through; opacity:.6;" : "")
62131
62168
  },
62132
62169
  topic_name
62133
62170
  ], [
62134
62171
  "span",
62135
62172
  {
62136
62173
  class: "GRAPH_LEGEND_COUNT has-text-grey",
62137
- style: "margin-left:.35rem; font-size:.8em;"
62174
+ style: LEGEND_COUNT_STYLE
62138
62175
  },
62139
62176
  hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
62140
62177
  ]);
62141
62178
  let body_attrs = {
62142
- class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " is-primary is-light" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62179
+ class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " GRAPH_LEGEND_FOCUSED" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62143
62180
  type: "button",
62144
- style: "gap:0; flex:0 0 auto;",
62181
+ style: "gap:0; flex:0 0 auto;" + (focused ? " box-shadow: inset 0 0 0 2px var(--bulma-text, #2e333d);" : ""),
62145
62182
  title: is_main ? t("main topic") : hidden ? t("show topic") : t("hide topic"),
62146
62183
  "aria-label": topic_name,
62147
62184
  "aria-pressed": hidden ? "false" : "true"
@@ -62163,7 +62200,7 @@ function refresh_legend(gobj) {
62163
62200
  if (is_main && state.main_chosen) extras.push([
62164
62201
  "button",
62165
62202
  {
62166
- class: "GRAPH_LEGEND_STAR button is-small is-warning is-light",
62203
+ class: "GRAPH_LEGEND_STAR button is-small pressed_state",
62167
62204
  type: "button",
62168
62205
  style: "padding:0 .4rem;",
62169
62206
  title: t("main topic"),
@@ -62224,7 +62261,7 @@ function refresh_legend(gobj) {
62224
62261
  if (!hidden && entry.linked && entry.loose > 0) extras.push([
62225
62262
  "button",
62226
62263
  {
62227
- class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " is-warning is-light" : ""),
62264
+ class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " pressed_state" : ""),
62228
62265
  type: "button",
62229
62266
  style: "padding:0 .45rem;",
62230
62267
  title: t("loose records"),
@@ -62244,7 +62281,7 @@ function refresh_legend(gobj) {
62244
62281
  if (!hidden) extras.push([
62245
62282
  "button",
62246
62283
  {
62247
- class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " is-primary is-light" : ""),
62284
+ class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " pressed_state" : ""),
62248
62285
  type: "button",
62249
62286
  style: "padding:0 .4rem;",
62250
62287
  title: t("highlight topic"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.72",
3
+ "version": "7.23.74",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -2339,6 +2339,27 @@ function ac_set_operation_mode(gobj, event, kw, src)
2339
2339
  * stays small so the strip keeps its height; the glyph inside grows. */
2340
2340
  const LEGEND_GLYPH_STYLE = 'font-size:1.5em; line-height:1; color:inherit;';
2341
2341
 
2342
+ /* And the same for what a chip SAYS. The legend is a layer control --
2343
+ * a gadget somebody uses, not a footnote somebody glances at -- and a
2344
+ * topic name is the thing it is read for, so 0.75rem is the wrong
2345
+ * size for it. The button stays `is-small` because the strip has to
2346
+ * hold a chip per topic on one row; the name and the count come back
2347
+ * up inside it, exactly as the glyphs did. */
2348
+ const LEGEND_NAME_STYLE = 'font-size:1.2em; line-height:1.2;';
2349
+ const LEGEND_COUNT_STYLE = 'margin-left:.4rem; font-size:1em;';
2350
+
2351
+ /* The focused chip is HIGHLIGHTED, not pressed. The body of a chip is
2352
+ * the show/hide toggle, and that is what its `aria-pressed` says;
2353
+ * the focus belongs to the crosshair next to it, which is the button
2354
+ * that looks pressed. 7.23.73 painted the body with `pressed_state`
2355
+ * as well, so the eye and the screen reader read two different
2356
+ * states off one button -- and the count, `has-text-grey` by Bulma's
2357
+ * `!important`, sat grey on the near-black at 2.7:1. A ring inside
2358
+ * the border marks the chip without repainting anything in it.
2359
+ * Inset, because a chip lives in a `has-addons` group and a shadow
2360
+ * outside the box is clipped by its neighbours. */
2361
+ const LEGEND_FOCUSED_STYLE = 'box-shadow: inset 0 0 0 2px var(--bulma-text, #2e333d);';
2362
+
2342
2363
  function refresh_legend(gobj)
2343
2364
  {
2344
2365
  let priv = gobj.priv;
@@ -2389,10 +2410,11 @@ function refresh_legend(gobj)
2389
2410
  `border:1px solid rgba(0,0,0,.25);` +
2390
2411
  (hidden? 'opacity:.35;' : '')}],
2391
2412
  ['span', {class: 'GRAPH_LEGEND_NAME',
2392
- style: hidden? 'text-decoration:line-through; opacity:.6;' : ''},
2413
+ style: LEGEND_NAME_STYLE +
2414
+ (hidden? ' text-decoration:line-through; opacity:.6;' : '')},
2393
2415
  topic_name],
2394
2416
  ['span', {class: 'GRAPH_LEGEND_COUNT has-text-grey',
2395
- style: 'margin-left:.35rem; font-size:.8em;'},
2417
+ style: LEGEND_COUNT_STYLE},
2396
2418
  hidden? `${entry.total}` : `${entry.visible}/${entry.total}`]
2397
2419
  );
2398
2420
 
@@ -2402,10 +2424,10 @@ function refresh_legend(gobj)
2402
2424
  * every chip was dead except its star. */
2403
2425
  let body_attrs = {
2404
2426
  class: 'GRAPH_LEGEND_ITEM button is-small' +
2405
- (focused? ' is-primary is-light' : '') +
2427
+ (focused? ' GRAPH_LEGEND_FOCUSED' : '') +
2406
2428
  (is_main? ' GRAPH_LEGEND_MAIN' : ''),
2407
2429
  type: 'button',
2408
- style: 'gap:0; flex:0 0 auto;',
2430
+ style: 'gap:0; flex:0 0 auto;' + (focused? ' ' + LEGEND_FOCUSED_STYLE : ''),
2409
2431
  title: is_main? t('main topic') : (hidden? t('show topic') : t('hide topic')),
2410
2432
  'aria-label': topic_name,
2411
2433
  'aria-pressed': hidden? 'false' : 'true',
@@ -2430,7 +2452,7 @@ function refresh_legend(gobj)
2430
2452
  * is a button too: pressing it hands the choice back to the
2431
2453
  * graph (deduced again). On a deduced one it is a mark. */
2432
2454
  if(is_main && state.main_chosen) {
2433
- extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small is-warning is-light',
2455
+ extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small pressed_state',
2434
2456
  type: 'button', style: 'padding:0 .4rem;',
2435
2457
  title: t('main topic'), 'data-i18n-title': 'main topic',
2436
2458
  'aria-label': t('main topic'), 'aria-pressed': 'true'},
@@ -2466,7 +2488,7 @@ function refresh_legend(gobj)
2466
2488
  if(!hidden && entry.linked && entry.loose > 0) {
2467
2489
  extras.push(['button', {
2468
2490
  class: 'GRAPH_LEGEND_LOOSE button is-small' +
2469
- (entry.loose_shown? ' is-warning is-light' : ''),
2491
+ (entry.loose_shown? ' pressed_state' : ''),
2470
2492
  type: 'button', style: 'padding:0 .45rem;',
2471
2493
  title: t('loose records'), 'data-i18n-title': 'loose records',
2472
2494
  'aria-label': t('loose records'),
@@ -2483,7 +2505,7 @@ function refresh_legend(gobj)
2483
2505
  if(!hidden) {
2484
2506
  extras.push(['button', {
2485
2507
  class: 'GRAPH_LEGEND_FOCUS button is-small' +
2486
- (focused? ' is-primary is-light' : ''),
2508
+ (focused? ' pressed_state' : ''),
2487
2509
  type: 'button', style: 'padding:0 .4rem;',
2488
2510
  title: t('highlight topic'), 'data-i18n-title': 'highlight topic',
2489
2511
  'aria-label': t('highlight topic'),
@@ -1,7 +1,7 @@
1
1
  /***********************************************************************
2
2
  * treedb_layout.js
3
3
  *
4
- * Two layouts made for a treedb, and nothing else.
4
+ * Three layouts made for a treedb, and nothing else.
5
5
  *
6
6
  * What a treedb graph IS, once it is folded (treedb_fold_model.js):
7
7
  * a forest read left to right. A main topic is a tree (places:
@@ -25,8 +25,9 @@
25
25
  * - the force family scatters a tree, which is the pile with
26
26
  * physics.
27
27
  *
28
- * So: a tidy tree (`tree`) and an outline (`outline`), both fed by
29
- * the SAME spanning tree, chosen deterministically:
28
+ * So: a tidy tree (`tree`), an outline (`outline`) and a radial
29
+ * tree (`radial`), all three fed by the SAME spanning tree, chosen
30
+ * deterministically:
30
31
  *
31
32
  * - roots are the nodes with no incoming edge, in node order;
32
33
  * - a node belongs to the FIRST parent that reaches it in a
@@ -49,8 +50,13 @@
49
50
  * viewer draws, and what a treedb is when read as one. Tall, but
50
51
  * nothing is ever beside anything: the reader scrolls a list.
51
52
  *
53
+ * `radial`: the root in the middle and a ring per depth, each
54
+ * subtree owning a sector proportional to its leaves; the radius
55
+ * of a ring grows until its neighbours sit side by side, so
56
+ * nothing overlaps by construction (see `layout_radial`).
57
+ *
52
58
  * Pure: takes plain nodes and edges, returns positions. The G6
53
- * classes in c_g6_nodes_tree.js are two thin adapters over it.
59
+ * classes in c_g6_nodes_tree.js are three thin adapters over it.
54
60
  *
55
61
  * Copyright (c) 2026, ArtGins.
56
62
  * All Rights Reserved.
@@ -142,6 +148,32 @@ export function spanning_tree(nodes, edges)
142
148
  return {roots: roots, children: children, depth: depth, by_id: by_id};
143
149
  }
144
150
 
151
+ /************************************************************
152
+ * The ids of a spanning tree in PARENT-BEFORE-CHILD order.
153
+ *
154
+ * Every pass below used to be a recursive walk, which is the natural
155
+ * way to write it and the one that dies on a deep tree: a
156
+ * self-referent hook (a place inside a place inside a place) is as
157
+ * deep as the data says, and the stack is not. Read this list
158
+ * forwards for a pre-order pass and BACKWARDS for a post-order one
159
+ * -- a child always sits after its parent, so reversed it always
160
+ * sits before it, which is the whole of what post-order was for.
161
+ ************************************************************/
162
+ function walk_order(t)
163
+ {
164
+ let order = [];
165
+ let stack = t.roots.slice().reverse();
166
+ while(stack.length) {
167
+ let id = stack.pop();
168
+ order.push(id);
169
+ let kids = t.children.get(id) || [];
170
+ for(let i = kids.length - 1; i >= 0; i--) {
171
+ stack.push(kids[i]);
172
+ }
173
+ }
174
+ return order;
175
+ }
176
+
145
177
  /************************************************************
146
178
  * The tidy tree, left to right.
147
179
  *
@@ -167,60 +199,54 @@ export function layout_tree(nodes, edges, opts)
167
199
  x += (col_w[d] || 0) + o.ranksep;
168
200
  }
169
201
 
170
- /* Subtree heights, post-order. */
202
+ /* Subtree heights: post-order, so children before parents. */
203
+ let order = walk_order(t);
171
204
  let height = new Map();
172
- let measure = (id) => {
205
+ for(let i = order.length - 1; i >= 0; i--) {
206
+ let id = order[i];
173
207
  let kids = t.children.get(id) || [];
174
208
  let own = t.by_id.get(id).h;
175
- if(!kids.length) {
176
- height.set(id, own);
177
- return own;
178
- }
179
209
  let sum = 0;
180
- for(let i = 0; i < kids.length; i++) {
181
- sum += measure(kids[i]);
182
- if(i > 0) {
210
+ for(let k = 0; k < kids.length; k++) {
211
+ sum += height.get(kids[k]);
212
+ if(k > 0) {
183
213
  sum += o.nodesep;
184
214
  }
185
215
  }
186
- let h = Math.max(own, sum);
187
- height.set(id, h);
188
- return h;
189
- };
190
- for(let root of t.roots) {
191
- measure(root);
216
+ height.set(id, kids.length? Math.max(own, sum) : own);
192
217
  }
193
218
 
194
219
  /* Placement, pre-order: a node is centred on the block of its
195
- * children, which starts at the top of its own block. */
196
- let place = (id, top) => {
197
- let n = t.by_id.get(id);
220
+ * children, which starts at the top of its own block. Every node
221
+ * in a column is centred on the column, whatever its own width --
222
+ * the cards of one depth line up by their middles. */
223
+ let tops = new Map();
224
+ let top = 0;
225
+ for(let root of t.roots) {
226
+ tops.set(root, top);
227
+ top += height.get(root) + o.nodesep * 2;
228
+ }
229
+ for(let id of order) {
198
230
  let d = t.depth.get(id);
199
231
  let kids = t.children.get(id) || [];
200
232
  let block = height.get(id);
201
- let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
233
+ let own_top = tops.get(id);
234
+ pos.set(id, {x: col_x[d] + col_w[d] / 2, y: own_top + block / 2});
202
235
  if(!kids.length) {
203
- pos.set(id, {x: cx, y: top + block / 2});
204
- return;
236
+ continue;
205
237
  }
206
238
  let kids_h = 0;
207
- for(let i = 0; i < kids.length; i++) {
208
- kids_h += height.get(kids[i]);
209
- if(i > 0) {
239
+ for(let k = 0; k < kids.length; k++) {
240
+ kids_h += height.get(kids[k]);
241
+ if(k > 0) {
210
242
  kids_h += o.nodesep;
211
243
  }
212
244
  }
213
- let cursor = top + (block - kids_h) / 2;
245
+ let cursor = own_top + (block - kids_h) / 2;
214
246
  for(let kid of kids) {
215
- place(kid, cursor);
247
+ tops.set(kid, cursor);
216
248
  cursor += height.get(kid) + o.nodesep;
217
249
  }
218
- pos.set(id, {x: cx, y: top + block / 2});
219
- };
220
- let top = 0;
221
- for(let root of t.roots) {
222
- place(root, top);
223
- top += height.get(root) + o.nodesep * 2;
224
250
  }
225
251
 
226
252
  return pos;
@@ -253,24 +279,25 @@ export function layout_radial(nodes, edges, opts)
253
279
  return pos;
254
280
  }
255
281
 
256
- /* Leaves under each node: the sector it deserves. */
282
+ /* Leaves under each node: the sector it deserves. Post-order. */
283
+ let order = walk_order(t);
257
284
  let leaves = new Map();
258
- let count = (id) => {
285
+ for(let i = order.length - 1; i >= 0; i--) {
286
+ let id = order[i];
259
287
  let kids = t.children.get(id) || [];
260
288
  if(!kids.length) {
261
289
  leaves.set(id, 1);
262
- return 1;
290
+ continue;
263
291
  }
264
292
  let n = 0;
265
293
  for(let kid of kids) {
266
- n += count(kid);
294
+ n += leaves.get(kid);
267
295
  }
268
296
  leaves.set(id, n);
269
- return n;
270
- };
297
+ }
271
298
  let total = 0;
272
299
  for(let root of t.roots) {
273
- total += count(root);
300
+ total += leaves.get(root);
274
301
  }
275
302
 
276
303
  /* One root sits at the centre; several sit on the first ring
@@ -283,20 +310,27 @@ export function layout_radial(nodes, edges, opts)
283
310
  * Angles do not depend on the radii, so they come first. */
284
311
  let angle = new Map();
285
312
  let rings = []; /* ring -> [{id, angle, diag}] */
286
- let sector = (id, from, span) => {
287
- let a = from + span / 2;
288
- angle.set(id, a);
289
- let ring = ring_of(id);
290
- if(!rings[ring]) {
291
- rings[ring] = [];
292
- }
293
- let n = t.by_id.get(id);
294
- rings[ring].push({id: id, angle: a, diag: Math.hypot(n.w, n.h)});
295
- let cursor = from;
296
- for(let kid of (t.children.get(id) || [])) {
297
- let part = span * leaves.get(kid) / leaves.get(id);
298
- sector(kid, cursor, part);
299
- cursor += part;
313
+ /* An explicit stack, not recursion: the sectors of a ring are
314
+ * sorted by angle in pass 2, so the order they are pushed in does
315
+ * not matter -- only that every node gets its own. */
316
+ let sector = (root_id, root_from, root_span) => {
317
+ let stack = [{id: root_id, from: root_from, span: root_span}];
318
+ while(stack.length) {
319
+ let {id, from, span} = stack.pop();
320
+ let a = from + span / 2;
321
+ angle.set(id, a);
322
+ let ring = ring_of(id);
323
+ if(!rings[ring]) {
324
+ rings[ring] = [];
325
+ }
326
+ let n = t.by_id.get(id);
327
+ rings[ring].push({id: id, angle: a, diag: Math.hypot(n.w, n.h)});
328
+ let cursor = from;
329
+ for(let kid of (t.children.get(id) || [])) {
330
+ let part = span * leaves.get(kid) / leaves.get(id);
331
+ stack.push({id: kid, from: cursor, span: part});
332
+ cursor += part;
333
+ }
300
334
  }
301
335
  };
302
336
  if(single) {
@@ -362,13 +396,20 @@ export function layout_outline(nodes, edges, opts)
362
396
  let pos = new Map();
363
397
 
364
398
  let y = 0;
365
- let visit = (id) => {
366
- let n = t.by_id.get(id);
367
- let d = t.depth.get(id);
368
- pos.set(id, {x: d * o.indent + n.w / 2, y: y + n.h / 2});
369
- y += n.h + o.nodesep;
370
- for(let kid of (t.children.get(id) || [])) {
371
- visit(kid);
399
+ let visit = (root_id) => {
400
+ let stack = [root_id];
401
+ while(stack.length) {
402
+ let id = stack.pop();
403
+ let n = t.by_id.get(id);
404
+ let d = t.depth.get(id);
405
+ pos.set(id, {x: d * o.indent + n.w / 2, y: y + n.h / 2});
406
+ y += n.h + o.nodesep;
407
+ /* Reversed, so a sibling pops in the order it was given:
408
+ * here the order of the walk IS the order of the rows. */
409
+ let kids = t.children.get(id) || [];
410
+ for(let i = kids.length - 1; i >= 0; i--) {
411
+ stack.push(kids[i]);
412
+ }
372
413
  }
373
414
  };
374
415
  for(let root of t.roots) {
@@ -170,3 +170,35 @@ describe("the outline", () => {
170
170
  expect(pos.get("norte").y).toBe(96 + 10 + 48);
171
171
  });
172
172
  });
173
+
174
+ describe("a deep tree", () => {
175
+ /* A self-referent hook -- a place inside a place inside a place --
176
+ * is as deep as the data says. The walks are iterative for exactly
177
+ * this: recursive ones died here with "Maximum call stack size
178
+ * exceeded", and a graph that cannot be drawn is not a layout
179
+ * choice, it is an exception in the console. */
180
+ const DEEP = 20000;
181
+ const deep_nodes = [];
182
+ const deep_edges = [];
183
+ for(let i = 0; i < DEEP; i++) {
184
+ deep_nodes.push({id: `n${i}`, w: 100, h: 40});
185
+ if(i > 0) {
186
+ deep_edges.push({source: `n${i - 1}`, target: `n${i}`, rank: 0});
187
+ }
188
+ }
189
+
190
+ test("the spanning tree reaches the bottom", () => {
191
+ let t = spanning_tree(deep_nodes, deep_edges);
192
+ expect(t.roots).toEqual(["n0"]);
193
+ expect(t.depth.get(`n${DEEP - 1}`)).toBe(DEEP - 1);
194
+ });
195
+
196
+ test("the three layouts place every node of it", () => {
197
+ for(let layout of [layout_tree, layout_outline, layout_radial]) {
198
+ let pos = layout(deep_nodes, deep_edges);
199
+ expect(pos.size).toBe(DEEP);
200
+ expect(Number.isFinite(pos.get(`n${DEEP - 1}`).x)).toBe(true);
201
+ expect(Number.isFinite(pos.get(`n${DEEP - 1}`).y)).toBe(true);
202
+ }
203
+ });
204
+ });