@yuneta/gobj-ui 7.23.147 → 7.23.149
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 +28 -3
- package/dist/gobj-ui.cjs.js +380 -52
- package/dist/gobj-ui.es.js +381 -53
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +96 -2
- package/src/c_yui_treedb_graph.js +50 -0
- package/src/treedb_layout.js +258 -0
- package/src/treedb_layout.test.js +102 -1
- package/src/yui_icons.css +6 -0
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -132,7 +132,7 @@ import {
|
|
|
132
132
|
fold_split_group_key,
|
|
133
133
|
fold_root_group_key,
|
|
134
134
|
} from "./treedb_fold_model.js";
|
|
135
|
-
import {layout_tree, layout_radial} from "./treedb_layout.js";
|
|
135
|
+
import {layout_tree, layout_radial, layout_compact} from "./treedb_layout.js";
|
|
136
136
|
|
|
137
137
|
import {
|
|
138
138
|
BaseLayout,
|
|
@@ -146,6 +146,7 @@ import {
|
|
|
146
146
|
HTML,
|
|
147
147
|
Toolbar,
|
|
148
148
|
register,
|
|
149
|
+
Polyline,
|
|
149
150
|
} from '@antv/g6';
|
|
150
151
|
|
|
151
152
|
import {Circle as CircleGeometry, Rect as RectGeometry} from '@antv/g';
|
|
@@ -350,6 +351,16 @@ const _layouts = {
|
|
|
350
351
|
nodesep: 18,
|
|
351
352
|
ranksep: 90,
|
|
352
353
|
},
|
|
354
|
+
/* The same tree packed by CONTOUR (Moen, as mxGraph drew it): a
|
|
355
|
+
* closed branch sits beside an open one at the depth they share,
|
|
356
|
+
* instead of beside the whole block of the open one. Same rows
|
|
357
|
+
* and order as `treedb-tree` (treedb_layout.js). */
|
|
358
|
+
"compact-tree": {
|
|
359
|
+
type: 'treedb-compact',
|
|
360
|
+
direction: 'TB',
|
|
361
|
+
nodesep: 18,
|
|
362
|
+
ranksep: 90,
|
|
363
|
+
},
|
|
353
364
|
"dagre": {
|
|
354
365
|
type: 'dagre',
|
|
355
366
|
rankdir: 'LR',
|
|
@@ -446,6 +457,8 @@ SDATA(data_type_t.DTP_LIST, "loose_topics", 0, "[]", "Topics whos
|
|
|
446
457
|
|
|
447
458
|
/*---------------- Node shape ----------------*/
|
|
448
459
|
SDATA(data_type_t.DTP_STRING, "node_mode", 0, "expanded", "How a record is drawn: `expanded` = the card with its pills and ports; `compact` = a one-line pill with the name and small ports; `shape` = a figure of the topic's colour (square, circle, diamond... per node or topic, from the node properties), no ports, no text -- the topology alone. One node can be toggled against it (EV_TOGGLE_NODE_MODE)"),
|
|
460
|
+
SDATA(data_type_t.DTP_STRING, "edge_shape", 0, "curved", "How an edge is drawn: `curved` (the default) or `elbow` -- straight out of its port to the channel between the two rows, along it, and straight into the other port: the bus of an org chart, as mxGraph drew its trees. Layouts with no rows (`rowless_layouts`) keep the curve"),
|
|
461
|
+
SDATA(data_type_t.DTP_LIST, "rowless_layouts", sdata_flag_t.SDF_RD, JSON.stringify(["radial", "d3-force", "force-atlas2"]), "Layouts with no rows for an elbow to run between: their edges stay curved whatever `edge_shape` says (read-only, for the parent to query)"),
|
|
449
462
|
SDATA(data_type_t.DTP_BOOLEAN, "node_labels", 0, true, "A `shape` node says its name under the figure. Off, it is a coloured figure and nothing else"),
|
|
450
463
|
SDATA(data_type_t.DTP_BOOLEAN, "confirm_delete_node", 0, true, "Ask confirmation before deleting a node"),
|
|
451
464
|
SDATA(data_type_t.DTP_BOOLEAN, "confirm_unlink_edge", 0, true, "Ask confirmation before unlinking an edge"),
|
|
@@ -925,6 +938,9 @@ function register_layouts(gobj)
|
|
|
925
938
|
_g6_extensions_registered = true;
|
|
926
939
|
register(ExtensionCategory.LAYOUT, 'manual', ManualLayout);
|
|
927
940
|
register(ExtensionCategory.LAYOUT, 'treedb-tree', TreedbTreeLayout);
|
|
941
|
+
register(ExtensionCategory.LAYOUT, 'treedb-compact', TreedbCompactLayout);
|
|
942
|
+
register(ExtensionCategory.EDGE, 'treedb-elbow-v', TreedbElbowV);
|
|
943
|
+
register(ExtensionCategory.EDGE, 'treedb-elbow-h', TreedbElbowH);
|
|
928
944
|
register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
|
|
929
945
|
register(ExtensionCategory.NODE, 'light', LightNode);
|
|
930
946
|
register(ExtensionCategory.NODE, 'treedb-card', TreedbCard);
|
|
@@ -2025,9 +2041,17 @@ function layout_direction(gobj)
|
|
|
2025
2041
|
return str_in_list(LR_LAYOUTS, gobj.priv.layout)? "LR" : "TB";
|
|
2026
2042
|
}
|
|
2027
2043
|
|
|
2044
|
+
/* A curve, or an elbow when the reader asked for one and the layout
|
|
2045
|
+
* has rows to run it between. */
|
|
2028
2046
|
function edge_type_for(gobj)
|
|
2029
2047
|
{
|
|
2030
|
-
|
|
2048
|
+
let lr = (layout_direction(gobj) === "LR");
|
|
2049
|
+
let rowless = gobj_read_attr(gobj, "rowless_layouts") || [];
|
|
2050
|
+
if(gobj_read_str_attr(gobj, "edge_shape") === "elbow" &&
|
|
2051
|
+
!str_in_list(rowless, gobj.priv.layout)) {
|
|
2052
|
+
return lr? "treedb-elbow-h" : "treedb-elbow-v";
|
|
2053
|
+
}
|
|
2054
|
+
return lr? "cubic-horizontal" : "cubic";
|
|
2031
2055
|
}
|
|
2032
2056
|
|
|
2033
2057
|
/************************************************************
|
|
@@ -4188,6 +4212,55 @@ class TreedbTreeLayout extends BaseLayout
|
|
|
4188
4212
|
}
|
|
4189
4213
|
}
|
|
4190
4214
|
|
|
4215
|
+
/************************************************************
|
|
4216
|
+
* An ELBOW edge, the way mxGraph drew a tree: straight out of
|
|
4217
|
+
* the parent's port to the channel half way to the child, along
|
|
4218
|
+
* it, and straight into the child's port. The children of one
|
|
4219
|
+
* hook sit on one row and leave by one port, so they share the
|
|
4220
|
+
* channel and the lines read as one bus.
|
|
4221
|
+
*
|
|
4222
|
+
* Computed on every draw from where the two ends ARE, so a
|
|
4223
|
+
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
4224
|
+
* between rows (top to bottom), `-h` between columns.
|
|
4225
|
+
************************************************************/
|
|
4226
|
+
const ELBOW_RADIUS = 6;
|
|
4227
|
+
|
|
4228
|
+
class TreedbElbowV extends Polyline
|
|
4229
|
+
{
|
|
4230
|
+
getControlPoints(attributes) {
|
|
4231
|
+
let [s, t] = this.getEndpoints(attributes, false);
|
|
4232
|
+
let mid = (s[1] + t[1]) / 2;
|
|
4233
|
+
return [[s[0], mid], [t[0], mid]];
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
getKeyPath(attributes) {
|
|
4237
|
+
return super.getKeyPath(Object.assign({}, attributes,
|
|
4238
|
+
{radius: attributes.radius || ELBOW_RADIUS}));
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
class TreedbElbowH extends Polyline
|
|
4243
|
+
{
|
|
4244
|
+
getControlPoints(attributes) {
|
|
4245
|
+
let [s, t] = this.getEndpoints(attributes, false);
|
|
4246
|
+
let mid = (s[0] + t[0]) / 2;
|
|
4247
|
+
return [[mid, s[1]], [mid, t[1]]];
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
getKeyPath(attributes) {
|
|
4251
|
+
return super.getKeyPath(Object.assign({}, attributes,
|
|
4252
|
+
{radius: attributes.radius || ELBOW_RADIUS}));
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
class TreedbCompactLayout extends BaseLayout
|
|
4257
|
+
{
|
|
4258
|
+
async execute(data, options) {
|
|
4259
|
+
let input = treedb_layout_input(data);
|
|
4260
|
+
return treedb_layout_output(layout_compact(input.nodes, input.edges, options));
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4191
4264
|
class TreedbRadialLayout extends BaseLayout
|
|
4192
4265
|
{
|
|
4193
4266
|
async execute(data, options) {
|
|
@@ -10856,6 +10929,25 @@ function ac_set_node_labels(gobj, event, kw, src)
|
|
|
10856
10929
|
return 0;
|
|
10857
10930
|
}
|
|
10858
10931
|
|
|
10932
|
+
/************************************************************
|
|
10933
|
+
* Curved or elbow edges (the host's toggle). Nothing moves: every
|
|
10934
|
+
* edge takes the type that goes with the shape and the layout,
|
|
10935
|
+
* and draws again.
|
|
10936
|
+
************************************************************/
|
|
10937
|
+
function ac_set_edge_shape(gobj, event, kw, src)
|
|
10938
|
+
{
|
|
10939
|
+
let priv = gobj.priv;
|
|
10940
|
+
|
|
10941
|
+
let shape = (kw && kw.edge_shape === "elbow")? "elbow" : "curved";
|
|
10942
|
+
gobj_write_str_attr(gobj, "edge_shape", shape);
|
|
10943
|
+
if(!priv.graph || !priv.graph_rendered) {
|
|
10944
|
+
return 0; /* every edge is typed as it is drawn */
|
|
10945
|
+
}
|
|
10946
|
+
apply_layout_direction(gobj);
|
|
10947
|
+
graph_draw(gobj);
|
|
10948
|
+
return 0;
|
|
10949
|
+
}
|
|
10950
|
+
|
|
10859
10951
|
/************************************************************
|
|
10860
10952
|
* ONE node opened or closed against the mode (a double click,
|
|
10861
10953
|
* or the context menu). It holds still on screen while the
|
|
@@ -11499,6 +11591,7 @@ function create_gclass(gclass_name)
|
|
|
11499
11591
|
["EV_SET_LOOSE_TOPICS", ac_set_loose_topics, null],
|
|
11500
11592
|
["EV_SET_NODE_MODE", ac_set_node_mode, null],
|
|
11501
11593
|
["EV_SET_NODE_LABELS", ac_set_node_labels, null],
|
|
11594
|
+
["EV_SET_EDGE_SHAPE", ac_set_edge_shape, null],
|
|
11502
11595
|
["EV_TOGGLE_NODE_MODE", ac_toggle_node_mode, null],
|
|
11503
11596
|
["EV_NODE_DBLCLICK", ac_node_dblclick, null],
|
|
11504
11597
|
["EV_CENTER", ac_center, null],
|
|
@@ -11566,6 +11659,7 @@ function create_gclass(gclass_name)
|
|
|
11566
11659
|
["EV_SET_LOOSE_TOPICS", 0],
|
|
11567
11660
|
["EV_SET_NODE_MODE", 0],
|
|
11568
11661
|
["EV_SET_NODE_LABELS", 0],
|
|
11662
|
+
["EV_SET_EDGE_SHAPE", 0],
|
|
11569
11663
|
["EV_TOGGLE_NODE_MODE", 0],
|
|
11570
11664
|
["EV_NODE_DBLCLICK", 0],
|
|
11571
11665
|
["EV_LEGEND_STATE", event_flag_t.EVF_OUTPUT_EVENT],
|
|
@@ -134,6 +134,7 @@ SDATA(data_type_t.DTP_LIST, "hidden_topics", sdata_flag_t.SDF_PERSIST, "[
|
|
|
134
134
|
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"),
|
|
135
135
|
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"),
|
|
136
136
|
SDATA(data_type_t.DTP_STRING, "node_mode", sdata_flag_t.SDF_PERSIST, "expanded", "How the records are drawn: `expanded` = the cards with their pills and ports, `compact` = one-line pills with the name, `shape` = coloured figures, the topology alone. User preference"),
|
|
137
|
+
SDATA(data_type_t.DTP_STRING, "edge_shape", sdata_flag_t.SDF_PERSIST, "curved", "How the edges are drawn: `curved` or `elbow` (the bus of an org chart). User preference"),
|
|
137
138
|
SDATA(data_type_t.DTP_BOOLEAN, "node_labels", sdata_flag_t.SDF_PERSIST, true, "A `shape` node says its name under its figure. User preference"),
|
|
138
139
|
SDATA(data_type_t.DTP_DICT, "camera", sdata_flag_t.SDF_PERSIST, "{}", "Where the reader left the viewport: {zoom, x, y}. Handed to the engine on creation and written back when a move settles, so the graph opens where it was left. User preference, per treedb"),
|
|
139
140
|
SDATA(data_type_t.DTP_INTEGER, "fold_level", sdata_flag_t.SDF_PERSIST, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = the main topic's roots, N = N-1 levels under them; 0 = never stepped, `expand_depth`. Handed to the engine on creation, so the graph opens at the level it was left. User preference, per treedb"),
|
|
@@ -283,6 +284,7 @@ function mt_create(gobj)
|
|
|
283
284
|
loose_topics: gobj_read_attr(gobj, "loose_topics") || [],
|
|
284
285
|
node_mode: gobj_read_str_attr(gobj, "node_mode") || "expanded",
|
|
285
286
|
node_labels: gobj_read_bool_attr(gobj, "node_labels"),
|
|
287
|
+
edge_shape: gobj_read_str_attr(gobj, "edge_shape") || "curved",
|
|
286
288
|
camera: gobj_read_attr(gobj, "camera") || {},
|
|
287
289
|
},
|
|
288
290
|
gobj
|
|
@@ -553,6 +555,8 @@ function option_label(name)
|
|
|
553
555
|
return t("manual");
|
|
554
556
|
case "treedb-tree":
|
|
555
557
|
return t("treedb-tree");
|
|
558
|
+
case "compact-tree":
|
|
559
|
+
return t("compact-tree");
|
|
556
560
|
case "radial":
|
|
557
561
|
return t("radial");
|
|
558
562
|
case "dagre":
|
|
@@ -623,6 +627,22 @@ function node_shape_items(gobj, wide)
|
|
|
623
627
|
}
|
|
624
628
|
}
|
|
625
629
|
],
|
|
630
|
+
/* How the lines are drawn: curves, or the elbows of an org
|
|
631
|
+
* chart. A toggle, so a pressed pill; disabled on a layout
|
|
632
|
+
* with no rows, where the edges stay curved anyway. */
|
|
633
|
+
['button', {class: 'GRAPH_EDGE_ELBOW button', type: 'button',
|
|
634
|
+
style: {height: wide, width: '2.5em'},
|
|
635
|
+
title: t('elbow edges'), 'data-i18n-title': 'elbow edges',
|
|
636
|
+
'aria-label': t('elbow edges'), 'data-i18n-aria-label': 'elbow edges',
|
|
637
|
+
'aria-pressed': 'false'},
|
|
638
|
+
yui_toolbar_icon('yi-diagram-project'),
|
|
639
|
+
{
|
|
640
|
+
click: (evt) => {
|
|
641
|
+
evt.stopPropagation();
|
|
642
|
+
gobj_send_event(gobj, "EV_TOGGLE_EDGE_SHAPE", {}, gobj);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
],
|
|
626
646
|
];
|
|
627
647
|
}
|
|
628
648
|
|
|
@@ -670,6 +690,17 @@ function refresh_node_shape_buttons(gobj, $root)
|
|
|
670
690
|
$labels.setAttribute('aria-pressed', labels? 'true' : 'false');
|
|
671
691
|
$labels.disabled = (mode !== 'shape');
|
|
672
692
|
}
|
|
693
|
+
|
|
694
|
+
let elbow = (gobj_read_str_attr(gobj, "edge_shape") === "elbow");
|
|
695
|
+
set_pressed_state($root, '.GRAPH_EDGE_ELBOW', elbow);
|
|
696
|
+
let $elbow = $root.querySelector('.GRAPH_EDGE_ELBOW');
|
|
697
|
+
if($elbow) {
|
|
698
|
+
$elbow.setAttribute('aria-pressed', elbow? 'true' : 'false');
|
|
699
|
+
let child = gobj.priv.gobj_nodes_tree;
|
|
700
|
+
let rowless = child? (gobj_read_attr(child, "rowless_layouts") || []) : [];
|
|
701
|
+
let layout = child? gobj_read_str_attr(child, "layout") : "";
|
|
702
|
+
$elbow.disabled = rowless.includes(layout);
|
|
703
|
+
}
|
|
673
704
|
}
|
|
674
705
|
|
|
675
706
|
/************************************************************
|
|
@@ -1317,6 +1348,7 @@ function populate_nodes_tree_options(gobj)
|
|
|
1317
1348
|
$layout_select.value = current_layout;
|
|
1318
1349
|
}
|
|
1319
1350
|
refresh_layout_stepper(gobj);
|
|
1351
|
+
refresh_node_shape_buttons(gobj, $container);
|
|
1320
1352
|
}
|
|
1321
1353
|
|
|
1322
1354
|
// Restore persisted operation_mode selection
|
|
@@ -2612,6 +2644,7 @@ function ac_set_layout(gobj, event, kw, src)
|
|
|
2612
2644
|
gobj
|
|
2613
2645
|
);
|
|
2614
2646
|
refresh_layout_stepper(gobj);
|
|
2647
|
+
refresh_node_shape_buttons(gobj, priv.$container);
|
|
2615
2648
|
|
|
2616
2649
|
return 0;
|
|
2617
2650
|
}
|
|
@@ -3114,6 +3147,7 @@ function ac_layout_autoset(gobj, event, kw, src)
|
|
|
3114
3147
|
}
|
|
3115
3148
|
$select.value = layout;
|
|
3116
3149
|
refresh_layout_stepper(gobj);
|
|
3150
|
+
refresh_node_shape_buttons(gobj, $container);
|
|
3117
3151
|
return 0;
|
|
3118
3152
|
}
|
|
3119
3153
|
|
|
@@ -3211,6 +3245,20 @@ function ac_set_node_mode(gobj, event, kw, src)
|
|
|
3211
3245
|
return 0;
|
|
3212
3246
|
}
|
|
3213
3247
|
|
|
3248
|
+
function ac_toggle_edge_shape(gobj, event, kw, src)
|
|
3249
|
+
{
|
|
3250
|
+
let priv = gobj.priv;
|
|
3251
|
+
let shape = (gobj_read_str_attr(gobj, "edge_shape") === "elbow")? "curved" : "elbow";
|
|
3252
|
+
|
|
3253
|
+
gobj_write_str_attr(gobj, "edge_shape", shape);
|
|
3254
|
+
gobj_save_persistent_attrs(gobj, "edge_shape");
|
|
3255
|
+
refresh_node_shape_buttons(gobj, priv.$container);
|
|
3256
|
+
if(priv.gobj_nodes_tree) {
|
|
3257
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_SET_EDGE_SHAPE", {edge_shape: shape}, gobj);
|
|
3258
|
+
}
|
|
3259
|
+
return 0;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3214
3262
|
function ac_toggle_node_labels(gobj, event, kw, src)
|
|
3215
3263
|
{
|
|
3216
3264
|
let priv = gobj.priv;
|
|
@@ -3393,6 +3441,7 @@ function create_gclass(gclass_name)
|
|
|
3393
3441
|
["EV_COLLAPSE_LEVEL", ac_fold_level_step, null],
|
|
3394
3442
|
["EV_SET_NODE_MODE", ac_set_node_mode, null],
|
|
3395
3443
|
["EV_TOGGLE_NODE_LABELS", ac_toggle_node_labels, null],
|
|
3444
|
+
["EV_TOGGLE_EDGE_SHAPE", ac_toggle_edge_shape, null],
|
|
3396
3445
|
["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
|
|
3397
3446
|
["EV_CAMERA_CHANGED", ac_camera_changed, null],
|
|
3398
3447
|
["EV_LEGEND_TOPIC", ac_legend_topic, null],
|
|
@@ -3439,6 +3488,7 @@ function create_gclass(gclass_name)
|
|
|
3439
3488
|
["EV_COLLAPSE_LEVEL", 0],
|
|
3440
3489
|
["EV_SET_NODE_MODE", 0],
|
|
3441
3490
|
["EV_TOGGLE_NODE_LABELS", 0],
|
|
3491
|
+
["EV_TOGGLE_EDGE_SHAPE", 0],
|
|
3442
3492
|
["EV_FIND_RESULT", 0],
|
|
3443
3493
|
["EV_LAYOUT_AUTOSET", 0],
|
|
3444
3494
|
["EV_CAMERA_CHANGED", 0],
|
package/src/treedb_layout.js
CHANGED
|
@@ -274,6 +274,264 @@ function tidy_tree_lr(nodes, edges, o)
|
|
|
274
274
|
return pos;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/************************************************************
|
|
278
|
+
* The compact tree.
|
|
279
|
+
*
|
|
280
|
+
* The tidy tree above gives every subtree its whole BLOCK: open a
|
|
281
|
+
* hall with twenty-four devices and its block widens, and with it
|
|
282
|
+
* every block above it, so the siblings of its ancestors move apart
|
|
283
|
+
* at depths where nothing of theirs touches. This one keeps the
|
|
284
|
+
* CONTOUR of each subtree instead -- its outline on each side, depth
|
|
285
|
+
* by depth -- and slides every sibling against the outline of the
|
|
286
|
+
* ones before it. A folded region then sits as close to an open one
|
|
287
|
+
* as their cards allow at each depth, which is what a folded treedb
|
|
288
|
+
* is: one branch open among closed ones.
|
|
289
|
+
*
|
|
290
|
+
* Same spanning tree, same order, same rows as the tidy tree: only
|
|
291
|
+
* the packing across the depths differs, so switching between the
|
|
292
|
+
* two moves cards sideways and never reorders them.
|
|
293
|
+
*
|
|
294
|
+
* The contour arithmetic is Moen's ("Drawing dynamic trees", IEEE
|
|
295
|
+
* Software, 1990) in the form mxGraph gave it in mxCompactTreeLayout
|
|
296
|
+
* -- Copyright (c) 2006-2015 JGraph Ltd, Apache License 2.0 -- read in
|
|
297
|
+
* maxGraph's CompactTreeLayout.ts: leaf, join, merge, offset and
|
|
298
|
+
* bridge below follow that form. What is ours: it runs on the
|
|
299
|
+
* spanning tree of a FOREST, every depth is a row as wide as its
|
|
300
|
+
* widest card (mxGraph's `alignRanks`), several roots sit side by
|
|
301
|
+
* side, and there is no recursion.
|
|
302
|
+
************************************************************/
|
|
303
|
+
export function layout_compact(nodes, edges, opts)
|
|
304
|
+
{
|
|
305
|
+
let o = Object.assign({}, DEFAULTS, opts || {});
|
|
306
|
+
if(o.direction === "LR") {
|
|
307
|
+
return compact_tree_lr(nodes, edges, o);
|
|
308
|
+
}
|
|
309
|
+
let transposed = nodes.map((n) => Object.assign({}, n, {w: n.h, h: n.w}));
|
|
310
|
+
let lr = compact_tree_lr(transposed, edges, o);
|
|
311
|
+
let pos = new Map();
|
|
312
|
+
for(let [id, p] of lr) {
|
|
313
|
+
pos.set(id, {x: p.y, y: p.x});
|
|
314
|
+
}
|
|
315
|
+
return pos;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* One segment of a contour: a step of `dx` along the depth and `dy`
|
|
319
|
+
* across it, then the next segment. */
|
|
320
|
+
function contour_line(dx, dy, next)
|
|
321
|
+
{
|
|
322
|
+
return {dx: dx, dy: dy, next: next || null};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* How far the segment (a1, a2) at (p1, p2) has to move across to
|
|
326
|
+
* clear the segment (b1, b2) at the origin. */
|
|
327
|
+
function contour_offset(p1, p2, a1, a2, b1, b2)
|
|
328
|
+
{
|
|
329
|
+
if(b1 <= p1 || p1 + a1 <= 0) {
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
let d;
|
|
333
|
+
let t = b1 * a2 - a1 * b2;
|
|
334
|
+
if(t > 0) {
|
|
335
|
+
if(p1 < 0) {
|
|
336
|
+
d = (p1 * a2) / a1 - p2;
|
|
337
|
+
} else if(p1 > 0) {
|
|
338
|
+
d = (p1 * b2) / b1 - p2;
|
|
339
|
+
} else {
|
|
340
|
+
d = -p2;
|
|
341
|
+
}
|
|
342
|
+
} else if(b1 < p1 + a1) {
|
|
343
|
+
d = b2 - (p2 + ((b1 - p1) * a2) / a1);
|
|
344
|
+
} else if(b1 > p1 + a1) {
|
|
345
|
+
d = ((a1 + p1) * b2) / b1 - (p2 + a2);
|
|
346
|
+
} else {
|
|
347
|
+
d = b2 - (p2 + a2);
|
|
348
|
+
}
|
|
349
|
+
return (d > 0)? d : 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function contour_bridge(line1, x1, y1, line2, x2, y2)
|
|
353
|
+
{
|
|
354
|
+
let dx = x2 + line2.dx - x1;
|
|
355
|
+
let dy = (line2.dx === 0)? line2.dy : (dx * line2.dy) / line2.dx;
|
|
356
|
+
let r = contour_line(dx, dy, line2.next);
|
|
357
|
+
line1.next = contour_line(0, y2 + line2.dy - dy - y1, r);
|
|
358
|
+
return r;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Slide contour `p2` against `p1` until they clear; returns how far,
|
|
362
|
+
* and leaves in `p1` the outline of the two together. */
|
|
363
|
+
function contour_merge(p1, p2)
|
|
364
|
+
{
|
|
365
|
+
let x = 0;
|
|
366
|
+
let y = 0;
|
|
367
|
+
let total = 0;
|
|
368
|
+
let upper = p1.lower_head;
|
|
369
|
+
let lower = p2.upper_head;
|
|
370
|
+
|
|
371
|
+
while(lower && upper) {
|
|
372
|
+
let d = contour_offset(x, y, lower.dx, lower.dy, upper.dx, upper.dy);
|
|
373
|
+
y += d;
|
|
374
|
+
total += d;
|
|
375
|
+
if(x + lower.dx <= upper.dx) {
|
|
376
|
+
x += lower.dx;
|
|
377
|
+
y += lower.dy;
|
|
378
|
+
lower = lower.next;
|
|
379
|
+
} else {
|
|
380
|
+
x -= upper.dx;
|
|
381
|
+
y -= upper.dy;
|
|
382
|
+
upper = upper.next;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if(lower) {
|
|
387
|
+
let b = contour_bridge(p1.upper_tail, 0, 0, lower, x, y);
|
|
388
|
+
p1.upper_tail = b.next? p2.upper_tail : b;
|
|
389
|
+
p1.lower_tail = p2.lower_tail;
|
|
390
|
+
} else {
|
|
391
|
+
let b = contour_bridge(p2.lower_tail, x, y, upper, 0, 0);
|
|
392
|
+
if(!b.next) {
|
|
393
|
+
p1.lower_tail = b;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
p1.lower_head = p2.lower_head;
|
|
397
|
+
return total;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function compact_tree_lr(nodes, edges, o)
|
|
401
|
+
{
|
|
402
|
+
let t = spanning_tree(nodes, edges);
|
|
403
|
+
let order = walk_order(t);
|
|
404
|
+
let pos = new Map();
|
|
405
|
+
|
|
406
|
+
/* Columns, as in the tidy tree: a depth is as wide as its widest
|
|
407
|
+
* card, and a column's gap is `ranksep`. */
|
|
408
|
+
let col_w = [];
|
|
409
|
+
for(let [id, d] of t.depth) {
|
|
410
|
+
col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
|
|
411
|
+
}
|
|
412
|
+
let col_x = [];
|
|
413
|
+
let x = 0;
|
|
414
|
+
for(let d = 0; d < col_w.length; d++) {
|
|
415
|
+
col_x[d] = x;
|
|
416
|
+
x += (col_w[d] || 0) + o.ranksep;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* mxGraph's nodeDistance is padding on EACH side of a card, so two
|
|
420
|
+
* neighbours end `nodesep` apart; its levelDistance is what is
|
|
421
|
+
* left of the column gap after that padding. */
|
|
422
|
+
let pad = o.nodesep / 2;
|
|
423
|
+
let dist = o.nodesep;
|
|
424
|
+
let level_distance = o.ranksep - pad;
|
|
425
|
+
|
|
426
|
+
/* `len` along the depth (the column's width: the rows are aligned),
|
|
427
|
+
* `ext` across it (the card's own height). */
|
|
428
|
+
let tn = new Map();
|
|
429
|
+
for(let id of order) {
|
|
430
|
+
tn.set(id, {
|
|
431
|
+
len: col_w[t.depth.get(id)],
|
|
432
|
+
ext: t.by_id.get(id).h,
|
|
433
|
+
off_x: 0,
|
|
434
|
+
off_y: 0,
|
|
435
|
+
y: 0,
|
|
436
|
+
contour: null,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Contours, children before parents. */
|
|
441
|
+
for(let i = order.length - 1; i >= 0; i--) {
|
|
442
|
+
let id = order[i];
|
|
443
|
+
let n = tn.get(id);
|
|
444
|
+
let kids = t.children.get(id) || [];
|
|
445
|
+
|
|
446
|
+
if(!kids.length) {
|
|
447
|
+
let upper = contour_line(n.len + dist, 0);
|
|
448
|
+
let lower_tail = contour_line(0, -n.ext - dist);
|
|
449
|
+
n.contour = {
|
|
450
|
+
upper_head: upper,
|
|
451
|
+
upper_tail: upper,
|
|
452
|
+
lower_tail: lower_tail,
|
|
453
|
+
lower_head: contour_line(n.len + dist, 0, lower_tail),
|
|
454
|
+
};
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* join: the children side by side, each slid against the
|
|
459
|
+
* outline of the ones before it. */
|
|
460
|
+
let first = tn.get(kids[0]);
|
|
461
|
+
n.contour = first.contour;
|
|
462
|
+
let h = first.ext + dist;
|
|
463
|
+
let span = h;
|
|
464
|
+
for(let k = 1; k < kids.length; k++) {
|
|
465
|
+
let kid = tn.get(kids[k]);
|
|
466
|
+
let d = contour_merge(n.contour, kid.contour);
|
|
467
|
+
kid.off_y = d + h;
|
|
468
|
+
kid.off_x = 0;
|
|
469
|
+
h = kid.ext + dist;
|
|
470
|
+
span += d + h;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* attach: the parent centred across the span of its children,
|
|
474
|
+
* one column before them. */
|
|
475
|
+
let gap = pad + level_distance;
|
|
476
|
+
let y2 = (span - n.ext) / 2 - pad;
|
|
477
|
+
let y1 = y2 + n.ext + 2 * pad - span;
|
|
478
|
+
first.off_x = gap + n.len;
|
|
479
|
+
first.off_y = y1;
|
|
480
|
+
n.contour.upper_head = contour_line(n.len, 0,
|
|
481
|
+
contour_line(gap, y1, n.contour.upper_head));
|
|
482
|
+
n.contour.lower_head = contour_line(n.len, 0,
|
|
483
|
+
contour_line(gap, y2, n.contour.lower_head));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* Positions across, parents before children: the first child at
|
|
487
|
+
* its offset from the parent, each next one at its offset from
|
|
488
|
+
* the one before. */
|
|
489
|
+
let root_of = new Map();
|
|
490
|
+
for(let id of order) {
|
|
491
|
+
let n = tn.get(id);
|
|
492
|
+
if(!root_of.has(id)) {
|
|
493
|
+
root_of.set(id, id);
|
|
494
|
+
}
|
|
495
|
+
let kids = t.children.get(id) || [];
|
|
496
|
+
if(!kids.length) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
let running = n.y + tn.get(kids[0]).off_y;
|
|
500
|
+
tn.get(kids[0]).y = running;
|
|
501
|
+
root_of.set(kids[0], root_of.get(id));
|
|
502
|
+
for(let k = 1; k < kids.length; k++) {
|
|
503
|
+
let kid = tn.get(kids[k]);
|
|
504
|
+
kid.y = running + kid.off_y;
|
|
505
|
+
running = kid.y;
|
|
506
|
+
root_of.set(kids[k], root_of.get(id));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* Several roots: each tree after the one before, by its extent. */
|
|
511
|
+
let lo = new Map();
|
|
512
|
+
let hi = new Map();
|
|
513
|
+
for(let id of order) {
|
|
514
|
+
let r = root_of.get(id);
|
|
515
|
+
let n = tn.get(id);
|
|
516
|
+
lo.set(r, Math.min(lo.has(r)? lo.get(r) : Infinity, n.y));
|
|
517
|
+
hi.set(r, Math.max(hi.has(r)? hi.get(r) : -Infinity, n.y + n.ext));
|
|
518
|
+
}
|
|
519
|
+
let shift = new Map();
|
|
520
|
+
let top = 0;
|
|
521
|
+
for(let r of t.roots) {
|
|
522
|
+
shift.set(r, top - lo.get(r));
|
|
523
|
+
top += hi.get(r) - lo.get(r) + o.nodesep * 2;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for(let id of order) {
|
|
527
|
+
let d = t.depth.get(id);
|
|
528
|
+
let n = tn.get(id);
|
|
529
|
+
let y = n.y + shift.get(root_of.get(id));
|
|
530
|
+
pos.set(id, {x: col_x[d] + col_w[d] / 2, y: y + n.ext / 2});
|
|
531
|
+
}
|
|
532
|
+
return pos;
|
|
533
|
+
}
|
|
534
|
+
|
|
277
535
|
/* How much of a card lies along the direction `a`: the projection
|
|
278
536
|
* of its rectangle on that direction. Full width when the card is
|
|
279
537
|
* read along its width, full height across it, and the honest
|
|
@@ -6,7 +6,7 @@
|
|
|
6
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_radial } from "./treedb_layout.js";
|
|
9
|
+
import { spanning_tree, layout_tree, layout_radial, layout_compact } 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). */
|
|
@@ -97,6 +97,107 @@ describe("the tidy tree, read down (the default)", () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
/* Every two cards of one row stand at least `gap` apart. */
|
|
101
|
+
function expect_no_overlap_in_rows(list, pos, gap)
|
|
102
|
+
{
|
|
103
|
+
for(let i = 0; i < list.length; i++) {
|
|
104
|
+
for(let j = i + 1; j < list.length; j++) {
|
|
105
|
+
let a = list[i];
|
|
106
|
+
let b = list[j];
|
|
107
|
+
let pa = pos.get(a.id);
|
|
108
|
+
let pb = pos.get(b.id);
|
|
109
|
+
if(Math.abs(pa.y - pb.y) > 1e-6) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
expect(Math.abs(pa.x - pb.x) + 1e-6).toBeGreaterThanOrEqual((a.w + b.w) / 2 + gap);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function width_of(list, pos)
|
|
118
|
+
{
|
|
119
|
+
let lo = Infinity;
|
|
120
|
+
let hi = -Infinity;
|
|
121
|
+
for(let n of list) {
|
|
122
|
+
let p = pos.get(n.id);
|
|
123
|
+
lo = Math.min(lo, p.x - n.w / 2);
|
|
124
|
+
hi = Math.max(hi, p.x + n.w / 2);
|
|
125
|
+
}
|
|
126
|
+
return hi - lo;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* r -> a, b; a -> hall -> six devices; b is a closed sibling. */
|
|
130
|
+
const wide = [N("r", 172, 96), N("a", 172, 96), N("b", 172, 96), N("hall", 172, 96)]
|
|
131
|
+
.concat([0, 1, 2, 3, 4, 5].map((i) => N(`d${i}`, 116, 40)));
|
|
132
|
+
const wide_edges = [
|
|
133
|
+
{source: "r", target: "a"}, {source: "r", target: "b"}, {source: "a", target: "hall"},
|
|
134
|
+
].concat([0, 1, 2, 3, 4, 5].map((i) => ({source: "hall", target: `d${i}`})));
|
|
135
|
+
|
|
136
|
+
describe("the compact tree", () => {
|
|
137
|
+
test("the rows are the tidy tree's rows", () => {
|
|
138
|
+
let tidy = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
139
|
+
let pos = layout_compact(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
140
|
+
for(let n of nodes) {
|
|
141
|
+
expect(pos.get(n.id).y).toBe(tidy.get(n.id).y);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("no two cards of a row are closer than the gap", () => {
|
|
146
|
+
expect_no_overlap_in_rows(nodes, layout_compact(nodes, edges, {nodesep: 10, ranksep: 100}), 10);
|
|
147
|
+
expect_no_overlap_in_rows(wide, layout_compact(wide, wide_edges, {nodesep: 18, ranksep: 90}), 18);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("a parent is centred over its children, which keep their order", () => {
|
|
151
|
+
let pos = layout_compact(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
152
|
+
expect(pos.get("d0").x).toBeLessThan(pos.get("d1").x);
|
|
153
|
+
expect(pos.get("d1").x).toBeLessThan(pos.get("c1").x);
|
|
154
|
+
let first = pos.get("d0").x - 58;
|
|
155
|
+
let last = pos.get("c1").x + 86;
|
|
156
|
+
expect(pos.get("nave").x).toBeCloseTo((first + last) / 2, 6);
|
|
157
|
+
expect(pos.get("norte").x).toBeLessThan(pos.get("sur").x);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("a closed sibling sits beside an open branch, not beside its block", () => {
|
|
161
|
+
let opts = {nodesep: 18, ranksep: 90};
|
|
162
|
+
let tidy = layout_tree(wide, wide_edges, opts);
|
|
163
|
+
let pos = layout_compact(wide, wide_edges, opts);
|
|
164
|
+
/* In the tidy tree `b` waits for the whole fan of devices; here it
|
|
165
|
+
* only has to clear `a`, the card beside it. */
|
|
166
|
+
expect(pos.get("b").x - pos.get("a").x).toBeLessThan(tidy.get("b").x - tidy.get("a").x);
|
|
167
|
+
expect(pos.get("b").x - pos.get("a").x).toBeGreaterThanOrEqual(172 + 18 - 1e-6);
|
|
168
|
+
expect(width_of(wide, pos)).toBeLessThanOrEqual(width_of(wide, tidy));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("two trees sit side by side, not overlapped", () => {
|
|
172
|
+
let pos = layout_compact([N("a", 100, 50), N("b", 100, 50)], [], {nodesep: 10});
|
|
173
|
+
expect(pos.get("b").x - pos.get("a").x).toBe(100 + 20);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("read right, it is the same tree with the axes swapped", () => {
|
|
177
|
+
let down = layout_compact(wide, wide_edges, {nodesep: 18, ranksep: 90});
|
|
178
|
+
let swapped = wide.map((n) => N(n.id, n.h, n.w));
|
|
179
|
+
let right = layout_compact(swapped, wide_edges, {nodesep: 18, ranksep: 90, direction: "LR"});
|
|
180
|
+
for(let n of wide) {
|
|
181
|
+
expect(right.get(n.id).x).toBeCloseTo(down.get(n.id).y, 6);
|
|
182
|
+
expect(right.get(n.id).y).toBeCloseTo(down.get(n.id).x, 6);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a tree as deep as the data places every node, without recursion", () => {
|
|
187
|
+
let chain = [];
|
|
188
|
+
let links = [];
|
|
189
|
+
for(let i = 0; i < 5000; i++) {
|
|
190
|
+
chain.push(N(`n${i}`, 40, 20));
|
|
191
|
+
if(i > 0) {
|
|
192
|
+
links.push({source: `n${i - 1}`, target: `n${i}`});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let pos = layout_compact(chain, links, {});
|
|
196
|
+
expect(pos.size).toBe(5000);
|
|
197
|
+
expect(Number.isFinite(pos.get("n4999").x)).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
100
201
|
describe("the tidy tree, read right", () => {
|
|
101
202
|
test("depths are columns; a column is as wide as its widest card", () => {
|
|
102
203
|
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100, direction: "LR"});
|
package/src/yui_icons.css
CHANGED
|
@@ -444,3 +444,9 @@
|
|
|
444
444
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zm11.3-387.3l104 104c4.6 4.6 5.9 11.5 3.5 17.4S366.5 256 360 256l-56 0 0 96c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-96-56 0c-6.5 0-12.3-3.9-14.8-9.9s-1.1-12.9 3.5-17.4l104-104c6.2-6.2 16.4-6.2 22.6 0z'/%3E%3C/svg%3E");
|
|
445
445
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zm11.3-387.3l104 104c4.6 4.6 5.9 11.5 3.5 17.4S366.5 256 360 256l-56 0 0 96c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-96-56 0c-6.5 0-12.3-3.9-14.8-9.9s-1.1-12.9 3.5-17.4l104-104c6.2-6.2 16.4-6.2 22.6 0z'/%3E%3C/svg%3E");
|
|
446
446
|
}
|
|
447
|
+
|
|
448
|
+
/* Elbow edges: boxes joined by right-angled lines (FontAwesome Free 7.3.1). */
|
|
449
|
+
.yi-diagram-project::before {
|
|
450
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M0 80C0 53.5 21.5 32 48 32l96 0c26.5 0 48 21.5 48 48l0 16 128 0 0-16c0-26.5 21.5-48 48-48l96 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-16-128 0 0 16c0 7.3-1.7 14.3-4.6 20.5l68.6 91.5 80 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-96c0-7.3 1.7-14.3 4.6-20.5L128 224 48 224c-26.5 0-48-21.5-48-48L0 80z'/%3E%3C/svg%3E");
|
|
451
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M0 80C0 53.5 21.5 32 48 32l96 0c26.5 0 48 21.5 48 48l0 16 128 0 0-16c0-26.5 21.5-48 48-48l96 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-16-128 0 0 16c0 7.3-1.7 14.3-4.6 20.5l68.6 91.5 80 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-96c0-7.3 1.7-14.3 4.6-20.5L128 224 48 224c-26.5 0-48-21.5-48-48L0 80z'/%3E%3C/svg%3E");
|
|
452
|
+
}
|