@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 +29 -0
- package/dist/gobj-ui.cjs.js +116 -79
- package/dist/gobj-ui.es.js +116 -79
- package/package.json +1 -1
- package/src/c_yui_treedb_graph.js +29 -7
- package/src/treedb_layout.js +105 -64
- package/src/treedb_layout.test.js +32 -0
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
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -27370,7 +27370,7 @@ function fold_reveal(model, state, node_key) {
|
|
|
27370
27370
|
/***********************************************************************
|
|
27371
27371
|
* treedb_layout.js
|
|
27372
27372
|
*
|
|
27373
|
-
*
|
|
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`)
|
|
27398
|
-
* the SAME spanning tree, chosen
|
|
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
|
|
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
|
|
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
|
|
27525
|
-
sum +=
|
|
27526
|
-
if (
|
|
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
|
-
|
|
27529
|
-
|
|
27530
|
-
|
|
27531
|
-
|
|
27532
|
-
for (let root of t.roots)
|
|
27533
|
-
|
|
27534
|
-
|
|
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
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
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
|
|
27548
|
-
kids_h += height.get(kids[
|
|
27549
|
-
if (
|
|
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 =
|
|
27577
|
+
let cursor = own_top + (block - kids_h) / 2;
|
|
27552
27578
|
for (let kid of kids) {
|
|
27553
|
-
|
|
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
|
|
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
|
-
|
|
27615
|
+
continue;
|
|
27597
27616
|
}
|
|
27598
27617
|
let n = 0;
|
|
27599
|
-
for (let kid of kids) n +=
|
|
27618
|
+
for (let kid of kids) n += leaves.get(kid);
|
|
27600
27619
|
leaves.set(id, n);
|
|
27601
|
-
|
|
27602
|
-
};
|
|
27620
|
+
}
|
|
27603
27621
|
let total = 0;
|
|
27604
|
-
for (let root of t.roots) total +=
|
|
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 = (
|
|
27610
|
-
let
|
|
27611
|
-
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
id,
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
27620
|
-
|
|
27621
|
-
|
|
27622
|
-
|
|
27623
|
-
|
|
27624
|
-
|
|
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 = (
|
|
27676
|
-
let
|
|
27677
|
-
|
|
27678
|
-
|
|
27679
|
-
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
27683
|
-
|
|
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:
|
|
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 ? "
|
|
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
|
|
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 ? "
|
|
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 ? "
|
|
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"),
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -27365,7 +27365,7 @@ function fold_reveal(model, state, node_key) {
|
|
|
27365
27365
|
/***********************************************************************
|
|
27366
27366
|
* treedb_layout.js
|
|
27367
27367
|
*
|
|
27368
|
-
*
|
|
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`)
|
|
27393
|
-
* the SAME spanning tree, chosen
|
|
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
|
|
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
|
|
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
|
|
27520
|
-
sum +=
|
|
27521
|
-
if (
|
|
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
|
-
|
|
27524
|
-
|
|
27525
|
-
|
|
27526
|
-
|
|
27527
|
-
for (let root of t.roots)
|
|
27528
|
-
|
|
27529
|
-
|
|
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
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
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
|
|
27543
|
-
kids_h += height.get(kids[
|
|
27544
|
-
if (
|
|
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 =
|
|
27572
|
+
let cursor = own_top + (block - kids_h) / 2;
|
|
27547
27573
|
for (let kid of kids) {
|
|
27548
|
-
|
|
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
|
|
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
|
-
|
|
27610
|
+
continue;
|
|
27592
27611
|
}
|
|
27593
27612
|
let n = 0;
|
|
27594
|
-
for (let kid of kids) n +=
|
|
27613
|
+
for (let kid of kids) n += leaves.get(kid);
|
|
27595
27614
|
leaves.set(id, n);
|
|
27596
|
-
|
|
27597
|
-
};
|
|
27615
|
+
}
|
|
27598
27616
|
let total = 0;
|
|
27599
|
-
for (let root of t.roots) total +=
|
|
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 = (
|
|
27605
|
-
let
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
27609
|
-
|
|
27610
|
-
|
|
27611
|
-
id,
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
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 = (
|
|
27671
|
-
let
|
|
27672
|
-
|
|
27673
|
-
|
|
27674
|
-
|
|
27675
|
-
|
|
27676
|
-
|
|
27677
|
-
|
|
27678
|
-
|
|
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:
|
|
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 ? "
|
|
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
|
|
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 ? "
|
|
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 ? "
|
|
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
|
@@ -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:
|
|
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:
|
|
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? '
|
|
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
|
|
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? '
|
|
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? '
|
|
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'),
|
package/src/treedb_layout.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/***********************************************************************
|
|
2
2
|
* treedb_layout.js
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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`)
|
|
29
|
-
* the SAME spanning tree, chosen
|
|
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
|
|
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
|
|
202
|
+
/* Subtree heights: post-order, so children before parents. */
|
|
203
|
+
let order = walk_order(t);
|
|
171
204
|
let height = new Map();
|
|
172
|
-
let
|
|
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
|
|
181
|
-
sum +=
|
|
182
|
-
if(
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
|
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
|
-
|
|
204
|
-
return;
|
|
236
|
+
continue;
|
|
205
237
|
}
|
|
206
238
|
let kids_h = 0;
|
|
207
|
-
for(let
|
|
208
|
-
kids_h += height.get(kids[
|
|
209
|
-
if(
|
|
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 =
|
|
245
|
+
let cursor = own_top + (block - kids_h) / 2;
|
|
214
246
|
for(let kid of kids) {
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
290
|
+
continue;
|
|
263
291
|
}
|
|
264
292
|
let n = 0;
|
|
265
293
|
for(let kid of kids) {
|
|
266
|
-
n +=
|
|
294
|
+
n += leaves.get(kid);
|
|
267
295
|
}
|
|
268
296
|
leaves.set(id, n);
|
|
269
|
-
|
|
270
|
-
};
|
|
297
|
+
}
|
|
271
298
|
let total = 0;
|
|
272
299
|
for(let root of t.roots) {
|
|
273
|
-
total +=
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 = (
|
|
366
|
-
let
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
});
|