@yuneta/gobj-ui 7.23.73 → 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.
@@ -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. */
@@ -2348,6 +2415,18 @@ const LEGEND_GLYPH_STYLE = 'font-size:1.5em; line-height:1; color:inherit;';
2348
2415
  const LEGEND_NAME_STYLE = 'font-size:1.2em; line-height:1.2;';
2349
2416
  const LEGEND_COUNT_STYLE = 'margin-left:.4rem; font-size:1em;';
2350
2417
 
2418
+ /* The focused chip is HIGHLIGHTED, not pressed. The body of a chip is
2419
+ * the show/hide toggle, and that is what its `aria-pressed` says;
2420
+ * the focus belongs to the crosshair next to it, which is the button
2421
+ * that looks pressed. 7.23.73 painted the body with `pressed_state`
2422
+ * as well, so the eye and the screen reader read two different
2423
+ * states off one button -- and the count, `has-text-grey` by Bulma's
2424
+ * `!important`, sat grey on the near-black at 2.7:1. A ring inside
2425
+ * the border marks the chip without repainting anything in it.
2426
+ * Inset, because a chip lives in a `has-addons` group and a shadow
2427
+ * outside the box is clipped by its neighbours. */
2428
+ const LEGEND_FOCUSED_STYLE = 'box-shadow: inset 0 0 0 2px var(--bulma-text, #2e333d);';
2429
+
2351
2430
  function refresh_legend(gobj)
2352
2431
  {
2353
2432
  let priv = gobj.priv;
@@ -2412,10 +2491,10 @@ function refresh_legend(gobj)
2412
2491
  * every chip was dead except its star. */
2413
2492
  let body_attrs = {
2414
2493
  class: 'GRAPH_LEGEND_ITEM button is-small' +
2415
- (focused? ' pressed_state' : '') +
2494
+ (focused? ' GRAPH_LEGEND_FOCUSED' : '') +
2416
2495
  (is_main? ' GRAPH_LEGEND_MAIN' : ''),
2417
2496
  type: 'button',
2418
- style: 'gap:0; flex:0 0 auto;',
2497
+ style: 'gap:0; flex:0 0 auto;' + (focused? ' ' + LEGEND_FOCUSED_STYLE : ''),
2419
2498
  title: is_main? t('main topic') : (hidden? t('show topic') : t('hide topic')),
2420
2499
  'aria-label': topic_name,
2421
2500
  'aria-pressed': hidden? 'false' : 'true',
@@ -2692,6 +2771,39 @@ function ac_collapse_all(gobj, event, kw, src)
2692
2771
  return 0;
2693
2772
  }
2694
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
+
2695
2807
  /************************************************************
2696
2808
  * How many nodes the term matched, from the graph child. Zero with a
2697
2809
  * term typed is an ANSWER and is shown; an empty box shows nothing.
@@ -2845,6 +2957,8 @@ function create_gclass(gclass_name)
2845
2957
  ["EV_FIND_NODES", ac_find_nodes, null],
2846
2958
  ["EV_EXPAND_ALL", ac_expand_all, null],
2847
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],
2848
2962
  ["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
2849
2963
  ["EV_LEGEND_TOPIC", ac_legend_topic, null],
2850
2964
  ["EV_LEGEND_STATE", ac_legend_state, null],
@@ -2886,6 +3000,8 @@ function create_gclass(gclass_name)
2886
3000
  ["EV_FIND_NODES", 0],
2887
3001
  ["EV_EXPAND_ALL", 0],
2888
3002
  ["EV_COLLAPSE_ALL", 0],
3003
+ ["EV_TOGGLE_NODE_MODE", 0],
3004
+ ["EV_TOGGLE_NODE_LABELS", 0],
2889
3005
  ["EV_FIND_RESULT", 0],
2890
3006
  ["EV_LAYOUT_AUTOSET", 0],
2891
3007
  ["EV_LEGEND_TOPIC", 0],
@@ -4,7 +4,7 @@
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 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,8 +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`) and an outline (`outline`), both fed by
29
- * the SAME spanning tree, chosen deterministically:
28
+ * So: a tidy tree (`tree`) and a radial tree (`radial`), both fed
29
+ * by the SAME spanning tree, chosen deterministically:
30
30
  *
31
31
  * - roots are the nodes with no incoming edge, in node order;
32
32
  * - a node belongs to the FIRST parent that reaches it in a
@@ -38,16 +38,21 @@
38
38
  * and then by node order (record order, the pages' order);
39
39
  * - a node nothing reaches (a cycle) is a root too.
40
40
  *
41
- * `tree`: the classic tidy tree, left to right. Each depth is a
42
- * column as wide as its widest card; a node is centred on the
43
- * block of its children; siblings stack with `nodesep` between
44
- * them. O(n), and opening a hook moves NOTHING that is not below
45
- * or beside it -- the cards keep their order, the eye keeps its
46
- * place.
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.)
47
51
  *
48
- * `outline`: one node per row, indented by depth -- what a JSON
49
- * viewer draws, and what a treedb is when read as one. Tall, but
50
- * nothing is ever beside anything: the reader scrolls a list.
52
+ * `radial`: the root in the middle and a ring per depth, each
53
+ * subtree owning a sector proportional to its leaves; the radius
54
+ * of a ring grows until its neighbours sit side by side, so
55
+ * nothing overlaps by construction (see `layout_radial`).
51
56
  *
52
57
  * Pure: takes plain nodes and edges, returns positions. The G6
53
58
  * classes in c_g6_nodes_tree.js are two thin adapters over it.
@@ -57,9 +62,9 @@
57
62
  ***********************************************************************/
58
63
 
59
64
  const DEFAULTS = {
60
- nodesep: 18, /* gap between siblings (tree) / rows (outline) */
61
- ranksep: 110, /* gap between a column and the next (tree) */
62
- 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 */
63
68
  };
64
69
 
65
70
  /************************************************************
@@ -169,14 +174,35 @@ function walk_order(t)
169
174
  }
170
175
 
171
176
  /************************************************************
172
- * The tidy tree, left to right.
177
+ * The tidy tree.
173
178
  *
174
179
  * Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
175
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.
176
188
  ************************************************************/
177
189
  export function layout_tree(nodes, edges, opts)
178
190
  {
179
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
+ {
180
206
  let t = spanning_tree(nodes, edges);
181
207
  let pos = new Map();
182
208
 
@@ -377,39 +403,3 @@ export function layout_radial(nodes, edges, opts)
377
403
 
378
404
  return pos;
379
405
  }
380
-
381
- /************************************************************
382
- * The outline: one node per row, indented by depth, in the
383
- * pre-order of the spanning tree. Rows are left-aligned on
384
- * their indent, so a column of chips reads as a list.
385
- ************************************************************/
386
- export function layout_outline(nodes, edges, opts)
387
- {
388
- let o = Object.assign({}, DEFAULTS, opts || {});
389
- let t = spanning_tree(nodes, edges);
390
- let pos = new Map();
391
-
392
- let y = 0;
393
- let visit = (root_id) => {
394
- let stack = [root_id];
395
- while(stack.length) {
396
- let id = stack.pop();
397
- let n = t.by_id.get(id);
398
- let d = t.depth.get(id);
399
- pos.set(id, {x: d * o.indent + n.w / 2, y: y + n.h / 2});
400
- y += n.h + o.nodesep;
401
- /* Reversed, so a sibling pops in the order it was given:
402
- * here the order of the walk IS the order of the rows. */
403
- let kids = t.children.get(id) || [];
404
- for(let i = kids.length - 1; i >= 0; i--) {
405
- stack.push(kids[i]);
406
- }
407
- }
408
- };
409
- for(let root of t.roots) {
410
- visit(root);
411
- y += o.nodesep; /* a breath between two trees */
412
- }
413
-
414
- return pos;
415
- }
@@ -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
+ }