@yuneta/gobj-ui 7.23.74 → 7.23.76

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.
@@ -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. */
@@ -508,8 +515,6 @@ function option_label(name)
508
515
  return t("manual");
509
516
  case "treedb-tree":
510
517
  return t("treedb-tree");
511
- case "treedb-outline":
512
- return t("treedb-outline");
513
518
  case "radial":
514
519
  return t("radial");
515
520
  case "dagre":
@@ -529,6 +534,62 @@ function option_label(name)
529
534
  /************************************************************
530
535
  *
531
536
  ************************************************************/
537
+ /************************************************************
538
+ * The two toggles of the nodes' shape.
539
+ *
540
+ * Closed nodes: every record a rounded square of its topic's
541
+ * colour, no ports, no text -- the topology of a graph of many
542
+ * nodes, which the cards hide behind their own size. Labels:
543
+ * the name under each square, when the squares alone do not say
544
+ * enough. Both persist, both are forwarded to the G6 child, and
545
+ * one node can be opened against the rule with a double click or
546
+ * its context menu (the child's business).
547
+ ************************************************************/
548
+ function node_shape_items(gobj, wide)
549
+ {
550
+ let toggle = (cls, icon, event_name, label_key) => {
551
+ return ['button', {class: `${cls} button`, type: 'button',
552
+ style: {height: wide, width: '2.5em'},
553
+ title: t(label_key), 'data-i18n-title': label_key,
554
+ 'aria-label': t(label_key), 'data-i18n-aria-label': label_key,
555
+ 'aria-pressed': 'false'},
556
+ ['i', {style: 'font-size:1.5em; color:inherit;', class: icon}],
557
+ {
558
+ click: (evt) => {
559
+ evt.stopPropagation();
560
+ gobj_send_event(gobj, event_name, {}, gobj);
561
+ }
562
+ }
563
+ ];
564
+ };
565
+ return [
566
+ toggle('GRAPH_NODE_MODE', 'yi-hexagon-nodes', 'EV_TOGGLE_NODE_MODE', 'closed nodes'),
567
+ toggle('GRAPH_NODE_LABELS', 'yi-font', 'EV_TOGGLE_NODE_LABELS', 'node labels'),
568
+ ];
569
+ }
570
+
571
+ /* The pressed look and `aria-pressed` of the two, from the attrs.
572
+ * `$root` is whatever holds the toolbar: the toolbar itself before
573
+ * the view is mounted, the view's container after. */
574
+ function refresh_node_shape_buttons(gobj, $root)
575
+ {
576
+ if(!$root) {
577
+ return;
578
+ }
579
+ let compact = (gobj_read_str_attr(gobj, "node_mode") === "compact");
580
+ let labels = gobj_read_bool_attr(gobj, "node_labels");
581
+ set_pressed_state($root, '.GRAPH_NODE_MODE', compact);
582
+ set_pressed_state($root, '.GRAPH_NODE_LABELS', labels);
583
+ let $mode = $root.querySelector('.GRAPH_NODE_MODE');
584
+ if($mode) {
585
+ $mode.setAttribute('aria-pressed', compact? 'true' : 'false');
586
+ }
587
+ let $labels = $root.querySelector('.GRAPH_NODE_LABELS');
588
+ if($labels) {
589
+ $labels.setAttribute('aria-pressed', labels? 'true' : 'false');
590
+ }
591
+ }
592
+
532
593
  function make_toolbar(gobj)
533
594
  {
534
595
  let priv = gobj.priv;
@@ -593,6 +654,10 @@ function make_toolbar(gobj)
593
654
  * way back to the default depth. */
594
655
  ...yui_graph_fold_items(gobj, gobj_read_str_attr(gobj, "wide")),
595
656
 
657
+ /* The shape pair: closed squares or open cards, and the name
658
+ * under a square. Two STATES, so they look pressed. */
659
+ ...node_shape_items(gobj, gobj_read_str_attr(gobj, "wide")),
660
+
596
661
  /* Inspect the treedb's raw tranger json in the lazy tree viewer
597
662
  * (print-tranger on the C_NODE service). A treedb can be huge, so
598
663
  * the viewer drills in on demand — see open_json_viewer. */
@@ -2704,6 +2769,39 @@ function ac_collapse_all(gobj, event, kw, src)
2704
2769
  return 0;
2705
2770
  }
2706
2771
 
2772
+ /************************************************************
2773
+ * The shape pair of the toolbar: the attr flips, persists,
2774
+ * shows on the button, and reaches the G6 child, which owns
2775
+ * the nodes.
2776
+ ************************************************************/
2777
+ function ac_toggle_node_mode(gobj, event, kw, src)
2778
+ {
2779
+ let priv = gobj.priv;
2780
+ let mode = (gobj_read_str_attr(gobj, "node_mode") === "compact")? "expanded" : "compact";
2781
+
2782
+ gobj_write_str_attr(gobj, "node_mode", mode);
2783
+ gobj_save_persistent_attrs(gobj, "node_mode");
2784
+ refresh_node_shape_buttons(gobj, priv.$container);
2785
+ if(priv.gobj_nodes_tree) {
2786
+ gobj_send_event(priv.gobj_nodes_tree, "EV_SET_NODE_MODE", {node_mode: mode}, gobj);
2787
+ }
2788
+ return 0;
2789
+ }
2790
+
2791
+ function ac_toggle_node_labels(gobj, event, kw, src)
2792
+ {
2793
+ let priv = gobj.priv;
2794
+ let labels = !gobj_read_bool_attr(gobj, "node_labels");
2795
+
2796
+ gobj_write_bool_attr(gobj, "node_labels", labels);
2797
+ gobj_save_persistent_attrs(gobj, "node_labels");
2798
+ refresh_node_shape_buttons(gobj, priv.$container);
2799
+ if(priv.gobj_nodes_tree) {
2800
+ gobj_send_event(priv.gobj_nodes_tree, "EV_SET_NODE_LABELS", {node_labels: labels}, gobj);
2801
+ }
2802
+ return 0;
2803
+ }
2804
+
2707
2805
  /************************************************************
2708
2806
  * How many nodes the term matched, from the graph child. Zero with a
2709
2807
  * term typed is an ANSWER and is shown; an empty box shows nothing.
@@ -2857,6 +2955,8 @@ function create_gclass(gclass_name)
2857
2955
  ["EV_FIND_NODES", ac_find_nodes, null],
2858
2956
  ["EV_EXPAND_ALL", ac_expand_all, null],
2859
2957
  ["EV_COLLAPSE_ALL", ac_collapse_all, null],
2958
+ ["EV_TOGGLE_NODE_MODE", ac_toggle_node_mode, null],
2959
+ ["EV_TOGGLE_NODE_LABELS", ac_toggle_node_labels, null],
2860
2960
  ["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
2861
2961
  ["EV_LEGEND_TOPIC", ac_legend_topic, null],
2862
2962
  ["EV_LEGEND_STATE", ac_legend_state, null],
@@ -2898,6 +2998,8 @@ function create_gclass(gclass_name)
2898
2998
  ["EV_FIND_NODES", 0],
2899
2999
  ["EV_EXPAND_ALL", 0],
2900
3000
  ["EV_COLLAPSE_ALL", 0],
3001
+ ["EV_TOGGLE_NODE_MODE", 0],
3002
+ ["EV_TOGGLE_NODE_LABELS", 0],
2901
3003
  ["EV_FIND_RESULT", 0],
2902
3004
  ["EV_LAYOUT_AUTOSET", 0],
2903
3005
  ["EV_LEGEND_TOPIC", 0],
@@ -1,10 +1,10 @@
1
1
  /***********************************************************************
2
2
  * treedb_layout.js
3
3
  *
4
- * Three layouts made for a treedb, and nothing else.
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 left to right. A main topic is a tree (places:
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`), an outline (`outline`) and a radial
29
- * tree (`radial`), all three fed by the SAME spanning tree, chosen
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, left to right. Each depth is a
43
- * column as wide as its widest card; a node is centred on the
44
- * block of its children; siblings stack with `nodesep` between
45
- * them. O(n), and opening a hook moves NOTHING that is not below
46
- * or beside it -- the cards keep their order, the eye keeps its
47
- * place.
48
- *
49
- * `outline`: one node per row, indented by depth -- what a JSON
50
- * viewer draws, and what a treedb is when read as one. Tall, but
51
- * nothing is ever beside anything: the reader scrolls a list.
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 three thin adapters over it.
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) / rows (outline) */
67
- ranksep: 110, /* gap between a column and the next (tree) */
68
- indent: 48, /* what a depth adds to the x of a row (outline) */
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, left to right.
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 columns and centring, the outline's rows.
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, layout_outline, layout_radial } from "./treedb_layout.js";
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 columns; a column is as wide as its widest card", () => {
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 three layouts place every node of it", () => {
197
- for(let layout of [layout_tree, layout_outline, layout_radial]) {
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
+ }