@yuneta/gobj-ui 7.23.74 → 7.23.75
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 +66 -12
- package/dist/gobj-ui.cjs.js +493 -135
- package/dist/gobj-ui.es.js +493 -135
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +433 -94
- package/src/c_yui_treedb_graph.js +104 -0
- package/src/treedb_layout.js +40 -56
- package/src/treedb_layout.test.js +45 -24
- package/src/yui_icons.css +6 -0
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
treedb_get_field_desc,
|
|
50
50
|
treedb_decoder_fkey,
|
|
51
51
|
gobj_write_str_attr,
|
|
52
|
+
gobj_write_bool_attr,
|
|
52
53
|
gobj_command,
|
|
53
54
|
msg_iev_get_stack,
|
|
54
55
|
kw_get_dict,
|
|
@@ -88,6 +89,7 @@ import {
|
|
|
88
89
|
enableElements,
|
|
89
90
|
set_submit_state,
|
|
90
91
|
set_active_state,
|
|
92
|
+
set_pressed_state,
|
|
91
93
|
} from "./lib_graph.js";
|
|
92
94
|
import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
|
|
93
95
|
import {yui_shell_of, yui_shell_set_sub_routes} from "./c_yui_shell.js";
|
|
@@ -118,6 +120,8 @@ SDATA(data_type_t.DTP_STRING, "layout", sdata_flag_t.SDF_PERSIST, ""
|
|
|
118
120
|
SDATA(data_type_t.DTP_LIST, "hidden_topics", sdata_flag_t.SDF_PERSIST, "[]", "Topics hidden from the graph (legend). User preference, per treedb"),
|
|
119
121
|
SDATA(data_type_t.DTP_STRING, "main_topic", sdata_flag_t.SDF_PERSIST, "", "The topic the tree hangs from; empty = deduced by the graph. User preference, per treedb"),
|
|
120
122
|
SDATA(data_type_t.DTP_LIST, "loose_topics", sdata_flag_t.SDF_PERSIST, "[]", "Topics whose LOOSE records (no parent) are shown. User preference, per treedb"),
|
|
123
|
+
SDATA(data_type_t.DTP_STRING, "node_mode", sdata_flag_t.SDF_PERSIST, "expanded", "How the records are drawn: `expanded` = the cards with their ports, `compact` = coloured squares, the topology alone. User preference"),
|
|
124
|
+
SDATA(data_type_t.DTP_BOOLEAN, "node_labels", sdata_flag_t.SDF_PERSIST, true, "A compact node says its name under its square. User preference"),
|
|
121
125
|
|
|
122
126
|
/*---------------- Remote Connection ----------------*/
|
|
123
127
|
SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote Yuno to request data"),
|
|
@@ -253,6 +257,8 @@ function mt_create(gobj)
|
|
|
253
257
|
hidden_topics: gobj_read_attr(gobj, "hidden_topics") || [],
|
|
254
258
|
main_topic: gobj_read_str_attr(gobj, "main_topic") || "",
|
|
255
259
|
loose_topics: gobj_read_attr(gobj, "loose_topics") || [],
|
|
260
|
+
node_mode: gobj_read_str_attr(gobj, "node_mode") || "expanded",
|
|
261
|
+
node_labels: gobj_read_bool_attr(gobj, "node_labels"),
|
|
256
262
|
},
|
|
257
263
|
gobj
|
|
258
264
|
);
|
|
@@ -360,6 +366,7 @@ function build_ui(gobj)
|
|
|
360
366
|
*----------------------------------------------*/
|
|
361
367
|
let padding = gobj_read_attr(gobj, "padding");
|
|
362
368
|
let $toolbar = make_toolbar(gobj);
|
|
369
|
+
refresh_node_shape_buttons(gobj, $toolbar);
|
|
363
370
|
|
|
364
371
|
/* The back link is pinned in the row, BEFORE the toolbar, so the
|
|
365
372
|
* toolbar's horizontal scroll never carries it off-screen. */
|
|
@@ -529,6 +536,62 @@ function option_label(name)
|
|
|
529
536
|
/************************************************************
|
|
530
537
|
*
|
|
531
538
|
************************************************************/
|
|
539
|
+
/************************************************************
|
|
540
|
+
* The two toggles of the nodes' shape.
|
|
541
|
+
*
|
|
542
|
+
* Closed nodes: every record a rounded square of its topic's
|
|
543
|
+
* colour, no ports, no text -- the topology of a graph of many
|
|
544
|
+
* nodes, which the cards hide behind their own size. Labels:
|
|
545
|
+
* the name under each square, when the squares alone do not say
|
|
546
|
+
* enough. Both persist, both are forwarded to the G6 child, and
|
|
547
|
+
* one node can be opened against the rule with a double click or
|
|
548
|
+
* its context menu (the child's business).
|
|
549
|
+
************************************************************/
|
|
550
|
+
function node_shape_items(gobj, wide)
|
|
551
|
+
{
|
|
552
|
+
let toggle = (cls, icon, event_name, label_key) => {
|
|
553
|
+
return ['button', {class: `${cls} button`, type: 'button',
|
|
554
|
+
style: {height: wide, width: '2.5em'},
|
|
555
|
+
title: t(label_key), 'data-i18n-title': label_key,
|
|
556
|
+
'aria-label': t(label_key), 'data-i18n-aria-label': label_key,
|
|
557
|
+
'aria-pressed': 'false'},
|
|
558
|
+
['i', {style: 'font-size:1.5em; color:inherit;', class: icon}],
|
|
559
|
+
{
|
|
560
|
+
click: (evt) => {
|
|
561
|
+
evt.stopPropagation();
|
|
562
|
+
gobj_send_event(gobj, event_name, {}, gobj);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
];
|
|
566
|
+
};
|
|
567
|
+
return [
|
|
568
|
+
toggle('GRAPH_NODE_MODE', 'yi-hexagon-nodes', 'EV_TOGGLE_NODE_MODE', 'closed nodes'),
|
|
569
|
+
toggle('GRAPH_NODE_LABELS', 'yi-font', 'EV_TOGGLE_NODE_LABELS', 'node labels'),
|
|
570
|
+
];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* The pressed look and `aria-pressed` of the two, from the attrs.
|
|
574
|
+
* `$root` is whatever holds the toolbar: the toolbar itself before
|
|
575
|
+
* the view is mounted, the view's container after. */
|
|
576
|
+
function refresh_node_shape_buttons(gobj, $root)
|
|
577
|
+
{
|
|
578
|
+
if(!$root) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
let compact = (gobj_read_str_attr(gobj, "node_mode") === "compact");
|
|
582
|
+
let labels = gobj_read_bool_attr(gobj, "node_labels");
|
|
583
|
+
set_pressed_state($root, '.GRAPH_NODE_MODE', compact);
|
|
584
|
+
set_pressed_state($root, '.GRAPH_NODE_LABELS', labels);
|
|
585
|
+
let $mode = $root.querySelector('.GRAPH_NODE_MODE');
|
|
586
|
+
if($mode) {
|
|
587
|
+
$mode.setAttribute('aria-pressed', compact? 'true' : 'false');
|
|
588
|
+
}
|
|
589
|
+
let $labels = $root.querySelector('.GRAPH_NODE_LABELS');
|
|
590
|
+
if($labels) {
|
|
591
|
+
$labels.setAttribute('aria-pressed', labels? 'true' : 'false');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
532
595
|
function make_toolbar(gobj)
|
|
533
596
|
{
|
|
534
597
|
let priv = gobj.priv;
|
|
@@ -593,6 +656,10 @@ function make_toolbar(gobj)
|
|
|
593
656
|
* way back to the default depth. */
|
|
594
657
|
...yui_graph_fold_items(gobj, gobj_read_str_attr(gobj, "wide")),
|
|
595
658
|
|
|
659
|
+
/* The shape pair: closed squares or open cards, and the name
|
|
660
|
+
* under a square. Two STATES, so they look pressed. */
|
|
661
|
+
...node_shape_items(gobj, gobj_read_str_attr(gobj, "wide")),
|
|
662
|
+
|
|
596
663
|
/* Inspect the treedb's raw tranger json in the lazy tree viewer
|
|
597
664
|
* (print-tranger on the C_NODE service). A treedb can be huge, so
|
|
598
665
|
* the viewer drills in on demand — see open_json_viewer. */
|
|
@@ -2704,6 +2771,39 @@ function ac_collapse_all(gobj, event, kw, src)
|
|
|
2704
2771
|
return 0;
|
|
2705
2772
|
}
|
|
2706
2773
|
|
|
2774
|
+
/************************************************************
|
|
2775
|
+
* The shape pair of the toolbar: the attr flips, persists,
|
|
2776
|
+
* shows on the button, and reaches the G6 child, which owns
|
|
2777
|
+
* the nodes.
|
|
2778
|
+
************************************************************/
|
|
2779
|
+
function ac_toggle_node_mode(gobj, event, kw, src)
|
|
2780
|
+
{
|
|
2781
|
+
let priv = gobj.priv;
|
|
2782
|
+
let mode = (gobj_read_str_attr(gobj, "node_mode") === "compact")? "expanded" : "compact";
|
|
2783
|
+
|
|
2784
|
+
gobj_write_str_attr(gobj, "node_mode", mode);
|
|
2785
|
+
gobj_save_persistent_attrs(gobj, "node_mode");
|
|
2786
|
+
refresh_node_shape_buttons(gobj, priv.$container);
|
|
2787
|
+
if(priv.gobj_nodes_tree) {
|
|
2788
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_SET_NODE_MODE", {node_mode: mode}, gobj);
|
|
2789
|
+
}
|
|
2790
|
+
return 0;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
function ac_toggle_node_labels(gobj, event, kw, src)
|
|
2794
|
+
{
|
|
2795
|
+
let priv = gobj.priv;
|
|
2796
|
+
let labels = !gobj_read_bool_attr(gobj, "node_labels");
|
|
2797
|
+
|
|
2798
|
+
gobj_write_bool_attr(gobj, "node_labels", labels);
|
|
2799
|
+
gobj_save_persistent_attrs(gobj, "node_labels");
|
|
2800
|
+
refresh_node_shape_buttons(gobj, priv.$container);
|
|
2801
|
+
if(priv.gobj_nodes_tree) {
|
|
2802
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_SET_NODE_LABELS", {node_labels: labels}, gobj);
|
|
2803
|
+
}
|
|
2804
|
+
return 0;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2707
2807
|
/************************************************************
|
|
2708
2808
|
* How many nodes the term matched, from the graph child. Zero with a
|
|
2709
2809
|
* term typed is an ANSWER and is shown; an empty box shows nothing.
|
|
@@ -2857,6 +2957,8 @@ function create_gclass(gclass_name)
|
|
|
2857
2957
|
["EV_FIND_NODES", ac_find_nodes, null],
|
|
2858
2958
|
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
2859
2959
|
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
2960
|
+
["EV_TOGGLE_NODE_MODE", ac_toggle_node_mode, null],
|
|
2961
|
+
["EV_TOGGLE_NODE_LABELS", ac_toggle_node_labels, null],
|
|
2860
2962
|
["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
|
|
2861
2963
|
["EV_LEGEND_TOPIC", ac_legend_topic, null],
|
|
2862
2964
|
["EV_LEGEND_STATE", ac_legend_state, null],
|
|
@@ -2898,6 +3000,8 @@ function create_gclass(gclass_name)
|
|
|
2898
3000
|
["EV_FIND_NODES", 0],
|
|
2899
3001
|
["EV_EXPAND_ALL", 0],
|
|
2900
3002
|
["EV_COLLAPSE_ALL", 0],
|
|
3003
|
+
["EV_TOGGLE_NODE_MODE", 0],
|
|
3004
|
+
["EV_TOGGLE_NODE_LABELS", 0],
|
|
2901
3005
|
["EV_FIND_RESULT", 0],
|
|
2902
3006
|
["EV_LAYOUT_AUTOSET", 0],
|
|
2903
3007
|
["EV_LEGEND_TOPIC", 0],
|
package/src/treedb_layout.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/***********************************************************************
|
|
2
2
|
* treedb_layout.js
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Two 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
|
-
* a forest read
|
|
7
|
+
* a forest read top to bottom. A main topic is a tree (places:
|
|
8
8
|
* country, region, site, hall), the other topics hang from its
|
|
9
9
|
* nodes through hooks (devices, users, controllers), a record may
|
|
10
10
|
* hang from two parents (a device from its place AND its
|
|
@@ -25,9 +25,8 @@
|
|
|
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
|
-
*
|
|
30
|
-
* deterministically:
|
|
28
|
+
* So: a tidy tree (`tree`) and a radial tree (`radial`), both fed
|
|
29
|
+
* by the SAME spanning tree, chosen deterministically:
|
|
31
30
|
*
|
|
32
31
|
* - roots are the nodes with no incoming edge, in node order;
|
|
33
32
|
* - a node belongs to the FIRST parent that reaches it in a
|
|
@@ -39,16 +38,16 @@
|
|
|
39
38
|
* and then by node order (record order, the pages' order);
|
|
40
39
|
* - a node nothing reaches (a cycle) is a root too.
|
|
41
40
|
*
|
|
42
|
-
* `tree`: the classic tidy tree,
|
|
43
|
-
*
|
|
44
|
-
* block of its children; siblings
|
|
45
|
-
* them. O(n), and opening a hook moves NOTHING that is
|
|
46
|
-
* or beside it -- the cards keep their order, the eye
|
|
47
|
-
* place.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
41
|
+
* `tree`: the classic tidy tree, top to bottom. Each depth is a
|
|
42
|
+
* row as tall as its tallest card; a node is centred over the
|
|
43
|
+
* block of its children; siblings sit side by side with `nodesep`
|
|
44
|
+
* between them. O(n), and opening a hook moves NOTHING that is
|
|
45
|
+
* not below or beside it -- the cards keep their order, the eye
|
|
46
|
+
* keeps its place. It reads DOWN because that is the direction a
|
|
47
|
+
* tree with many children has room in: a hall with a hundred
|
|
48
|
+
* devices is a wide row, not a tall column beside a card. (The
|
|
49
|
+
* left-to-right reading is `direction: "LR"`, kept for a host
|
|
50
|
+
* that wants it; `dagre` reads that way in the graph.)
|
|
52
51
|
*
|
|
53
52
|
* `radial`: the root in the middle and a ring per depth, each
|
|
54
53
|
* subtree owning a sector proportional to its leaves; the radius
|
|
@@ -56,16 +55,16 @@
|
|
|
56
55
|
* nothing overlaps by construction (see `layout_radial`).
|
|
57
56
|
*
|
|
58
57
|
* Pure: takes plain nodes and edges, returns positions. The G6
|
|
59
|
-
* classes in c_g6_nodes_tree.js are
|
|
58
|
+
* classes in c_g6_nodes_tree.js are two thin adapters over it.
|
|
60
59
|
*
|
|
61
60
|
* Copyright (c) 2026, ArtGins.
|
|
62
61
|
* All Rights Reserved.
|
|
63
62
|
***********************************************************************/
|
|
64
63
|
|
|
65
64
|
const DEFAULTS = {
|
|
66
|
-
nodesep: 18, /* gap between siblings (tree) /
|
|
67
|
-
ranksep:
|
|
68
|
-
|
|
65
|
+
nodesep: 18, /* gap between siblings (tree) / neighbours (radial) */
|
|
66
|
+
ranksep: 90, /* gap between a depth and the next */
|
|
67
|
+
direction: "TB", /* tree: "TB" reads down, "LR" reads right */
|
|
69
68
|
};
|
|
70
69
|
|
|
71
70
|
/************************************************************
|
|
@@ -175,14 +174,35 @@ function walk_order(t)
|
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
/************************************************************
|
|
178
|
-
* The tidy tree
|
|
177
|
+
* The tidy tree.
|
|
179
178
|
*
|
|
180
179
|
* Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
|
|
181
180
|
* positions a node.
|
|
181
|
+
*
|
|
182
|
+
* The algorithm is written ONCE, reading left to right (depths are
|
|
183
|
+
* columns, siblings stack down). Top to bottom -- the default -- is
|
|
184
|
+
* the same tree with the axes swapped: every card is fed in
|
|
185
|
+
* transposed (its height as its width), and every position comes
|
|
186
|
+
* back transposed. That is the whole difference, and it is the
|
|
187
|
+
* reason the code is not duplicated for a second direction.
|
|
182
188
|
************************************************************/
|
|
183
189
|
export function layout_tree(nodes, edges, opts)
|
|
184
190
|
{
|
|
185
191
|
let o = Object.assign({}, DEFAULTS, opts || {});
|
|
192
|
+
if(o.direction === "LR") {
|
|
193
|
+
return tidy_tree_lr(nodes, edges, o);
|
|
194
|
+
}
|
|
195
|
+
let transposed = nodes.map((n) => Object.assign({}, n, {w: n.h, h: n.w}));
|
|
196
|
+
let lr = tidy_tree_lr(transposed, edges, o);
|
|
197
|
+
let pos = new Map();
|
|
198
|
+
for(let [id, p] of lr) {
|
|
199
|
+
pos.set(id, {x: p.y, y: p.x});
|
|
200
|
+
}
|
|
201
|
+
return pos;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function tidy_tree_lr(nodes, edges, o)
|
|
205
|
+
{
|
|
186
206
|
let t = spanning_tree(nodes, edges);
|
|
187
207
|
let pos = new Map();
|
|
188
208
|
|
|
@@ -383,39 +403,3 @@ export function layout_radial(nodes, edges, opts)
|
|
|
383
403
|
|
|
384
404
|
return pos;
|
|
385
405
|
}
|
|
386
|
-
|
|
387
|
-
/************************************************************
|
|
388
|
-
* The outline: one node per row, indented by depth, in the
|
|
389
|
-
* pre-order of the spanning tree. Rows are left-aligned on
|
|
390
|
-
* their indent, so a column of chips reads as a list.
|
|
391
|
-
************************************************************/
|
|
392
|
-
export function layout_outline(nodes, edges, opts)
|
|
393
|
-
{
|
|
394
|
-
let o = Object.assign({}, DEFAULTS, opts || {});
|
|
395
|
-
let t = spanning_tree(nodes, edges);
|
|
396
|
-
let pos = new Map();
|
|
397
|
-
|
|
398
|
-
let y = 0;
|
|
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
|
-
}
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
for(let root of t.roots) {
|
|
416
|
-
visit(root);
|
|
417
|
-
y += o.nodesep; /* a breath between two trees */
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return pos;
|
|
421
|
-
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The two treedb layouts, pinned: the spanning tree they share
|
|
5
5
|
* (roots, first parent wins, hook order then record order), the
|
|
6
|
-
* tidy tree's
|
|
6
|
+
* tidy tree's rows and centring, the radial tree's rings.
|
|
7
7
|
***********************************************************************/
|
|
8
8
|
import { describe, test, expect } from "vitest";
|
|
9
|
-
import { spanning_tree, layout_tree,
|
|
9
|
+
import { spanning_tree, layout_tree, layout_radial } from "./treedb_layout.js";
|
|
10
10
|
|
|
11
11
|
/* es -> (norte, sur); norte -> nave; nave -> d0, d1 (hook rank 1) and
|
|
12
12
|
* c1 (hook rank 2); c1 -> d0 as well (second parent). */
|
|
@@ -63,9 +63,43 @@ describe("the spanning tree", () => {
|
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
describe("the tidy tree", () => {
|
|
67
|
-
test("depths are
|
|
66
|
+
describe("the tidy tree, read down (the default)", () => {
|
|
67
|
+
test("depths are rows; a row is as tall as its tallest card", () => {
|
|
68
|
+
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
69
|
+
expect(pos.get("es").y).toBe(48); /* 96/2 */
|
|
70
|
+
expect(pos.get("norte").y).toBe(96 + 100 + 48);
|
|
71
|
+
expect(pos.get("sur").y).toBe(pos.get("norte").y);
|
|
72
|
+
/* depth 2 holds nave (120 tall): the row is 120 tall */
|
|
73
|
+
expect(pos.get("nave").y).toBe(2 * (96 + 100) + 60);
|
|
74
|
+
/* depth 3 holds chips (40) and c1 (96): row is 96 tall, the
|
|
75
|
+
* chips are centred in it */
|
|
76
|
+
let row3 = 2 * (96 + 100) + 120 + 100;
|
|
77
|
+
expect(pos.get("c1").y).toBe(row3 + 48);
|
|
78
|
+
expect(pos.get("d0").y).toBe(row3 + 48);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("siblings sit side by side; a parent is centred over its children", () => {
|
|
68
82
|
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
83
|
+
/* nave's children: d0 (116), d1 (116), c1 (172) = 424 wide */
|
|
84
|
+
expect(pos.get("d1").x - pos.get("d0").x).toBe(126);
|
|
85
|
+
expect(pos.get("c1").x - pos.get("d1").x).toBe(58 + 10 + 86);
|
|
86
|
+
let first = pos.get("d0").x - 58;
|
|
87
|
+
let last = pos.get("c1").x + 86;
|
|
88
|
+
expect(pos.get("nave").x).toBe((first + last) / 2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("two trees sit side by side, not overlapped", () => {
|
|
92
|
+
let pos = layout_tree(
|
|
93
|
+
[N("a", 100, 50), N("b", 100, 50)], [], {nodesep: 10}
|
|
94
|
+
);
|
|
95
|
+
expect(pos.get("a").x).toBe(50);
|
|
96
|
+
expect(pos.get("b").x).toBe(50 + 100 + 20);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("the tidy tree, read right", () => {
|
|
101
|
+
test("depths are columns; a column is as wide as its widest card", () => {
|
|
102
|
+
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100, direction: "LR"});
|
|
69
103
|
expect(pos.get("es").x).toBe(86); /* 172/2 */
|
|
70
104
|
expect(pos.get("norte").x).toBe(172 + 100 + 86);
|
|
71
105
|
expect(pos.get("sur").x).toBe(pos.get("norte").x);
|
|
@@ -77,7 +111,7 @@ describe("the tidy tree", () => {
|
|
|
77
111
|
});
|
|
78
112
|
|
|
79
113
|
test("siblings stack with the gap; a parent is centred on its children", () => {
|
|
80
|
-
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
114
|
+
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100, direction: "LR"});
|
|
81
115
|
/* nave's children: d0 (40), d1 (40), c1 (96) = 196 tall */
|
|
82
116
|
expect(pos.get("d1").y - pos.get("d0").y).toBe(50);
|
|
83
117
|
expect(pos.get("c1").y - pos.get("d1").y).toBe(20 + 10 + 48);
|
|
@@ -89,7 +123,7 @@ describe("the tidy tree", () => {
|
|
|
89
123
|
test("a parent taller than its children keeps its own height", () => {
|
|
90
124
|
let pos = layout_tree(
|
|
91
125
|
[N("p", 100, 200), N("k", 50, 20)],
|
|
92
|
-
[{source: "p", target: "k"}], {nodesep: 10, ranksep: 50}
|
|
126
|
+
[{source: "p", target: "k"}], {nodesep: 10, ranksep: 50, direction: "LR"}
|
|
93
127
|
);
|
|
94
128
|
expect(pos.get("p").y).toBe(100);
|
|
95
129
|
expect(pos.get("k").y).toBe(100); /* centred in the parent's block */
|
|
@@ -97,17 +131,17 @@ describe("the tidy tree", () => {
|
|
|
97
131
|
|
|
98
132
|
test("two trees are stacked, not overlapped", () => {
|
|
99
133
|
let pos = layout_tree(
|
|
100
|
-
[N("a", 100, 50), N("b", 100, 50)], [], {nodesep: 10}
|
|
134
|
+
[N("a", 100, 50), N("b", 100, 50)], [], {nodesep: 10, direction: "LR"}
|
|
101
135
|
);
|
|
102
136
|
expect(pos.get("a").y).toBe(25);
|
|
103
137
|
expect(pos.get("b").y).toBe(25 + 50 + 20);
|
|
104
138
|
});
|
|
105
139
|
|
|
106
140
|
test("opening a hook does not move what is not under or beside it", () => {
|
|
107
|
-
let base = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
141
|
+
let base = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100, direction: "LR"});
|
|
108
142
|
let more = nodes.concat([N("d2", 116, 40)]);
|
|
109
143
|
let more_edges = edges.concat([{source: "nave", target: "d2", rank: 1}]);
|
|
110
|
-
let pos = layout_tree(more, more_edges, {nodesep: 10, ranksep: 100});
|
|
144
|
+
let pos = layout_tree(more, more_edges, {nodesep: 10, ranksep: 100, direction: "LR"});
|
|
111
145
|
expect(pos.get("es").x).toBe(base.get("es").x);
|
|
112
146
|
expect(pos.get("d0").y).toBe(base.get("d0").y); /* above the new one */
|
|
113
147
|
expect(pos.get("d1").y).toBe(base.get("d1").y);
|
|
@@ -158,19 +192,6 @@ describe("the radial tree", () => {
|
|
|
158
192
|
});
|
|
159
193
|
});
|
|
160
194
|
|
|
161
|
-
describe("the outline", () => {
|
|
162
|
-
test("one node per row, pre-order, indented by depth", () => {
|
|
163
|
-
let pos = layout_outline(nodes, edges, {nodesep: 10, indent: 40});
|
|
164
|
-
let rows = [...pos.entries()].sort((a, b) => a[1].y - b[1].y).map((e) => e[0]);
|
|
165
|
-
expect(rows).toEqual(["es", "norte", "nave", "d0", "d1", "c1", "sur"]);
|
|
166
|
-
expect(pos.get("es").x).toBe(86);
|
|
167
|
-
expect(pos.get("nave").x).toBe(2 * 40 + 86);
|
|
168
|
-
expect(pos.get("d0").x).toBe(3 * 40 + 58);
|
|
169
|
-
expect(pos.get("es").y).toBe(48);
|
|
170
|
-
expect(pos.get("norte").y).toBe(96 + 10 + 48);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
195
|
describe("a deep tree", () => {
|
|
175
196
|
/* A self-referent hook -- a place inside a place inside a place --
|
|
176
197
|
* is as deep as the data says. The walks are iterative for exactly
|
|
@@ -193,8 +214,8 @@ describe("a deep tree", () => {
|
|
|
193
214
|
expect(t.depth.get(`n${DEEP - 1}`)).toBe(DEEP - 1);
|
|
194
215
|
});
|
|
195
216
|
|
|
196
|
-
test("the
|
|
197
|
-
for(let layout of [layout_tree,
|
|
217
|
+
test("the two layouts place every node of it", () => {
|
|
218
|
+
for(let layout of [layout_tree, layout_radial]) {
|
|
198
219
|
let pos = layout(deep_nodes, deep_edges);
|
|
199
220
|
expect(pos.size).toBe(DEEP);
|
|
200
221
|
expect(Number.isFinite(pos.get(`n${DEEP - 1}`).x)).toBe(true);
|
package/src/yui_icons.css
CHANGED
|
@@ -386,3 +386,9 @@
|
|
|
386
386
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z'/%3E%3C/svg%3E");
|
|
387
387
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z'/%3E%3C/svg%3E");
|
|
388
388
|
}
|
|
389
|
+
|
|
390
|
+
/* A letter, for "labels": the name under a closed graph node. */
|
|
391
|
+
.yi-font::before {
|
|
392
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath fill-rule='evenodd' d='M190 32h68l150 448h-72l-40-120H152l-40 120H40L190 32zM172 300h104l-52-158-52 158z'/%3E%3C/svg%3E");
|
|
393
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath fill-rule='evenodd' d='M190 32h68l150 448h-72l-40-120H152l-40 120H40L190 32zM172 300h104l-52-158-52 158z'/%3E%3C/svg%3E");
|
|
394
|
+
}
|