@yuneta/gobj-ui 7.23.69 → 7.23.71
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 +49 -0
- package/dist/gobj-ui.cjs.js +470 -3
- package/dist/gobj-ui.es.js +470 -3
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +103 -2
- package/src/c_yui_form.js +8 -2
- package/src/c_yui_treedb_graph.js +6 -0
- package/src/form_field_readonly.js +61 -0
- package/src/form_field_readonly.test.js +50 -0
- package/src/treedb_layout.js +380 -0
- package/src/treedb_layout.test.js +172 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_layout.test.js
|
|
3
|
+
*
|
|
4
|
+
* The two treedb layouts, pinned: the spanning tree they share
|
|
5
|
+
* (roots, first parent wins, hook order then record order), the
|
|
6
|
+
* tidy tree's columns and centring, the outline's rows.
|
|
7
|
+
***********************************************************************/
|
|
8
|
+
import { describe, test, expect } from "vitest";
|
|
9
|
+
import { spanning_tree, layout_tree, layout_outline, layout_radial } from "./treedb_layout.js";
|
|
10
|
+
|
|
11
|
+
/* es -> (norte, sur); norte -> nave; nave -> d0, d1 (hook rank 1) and
|
|
12
|
+
* c1 (hook rank 2); c1 -> d0 as well (second parent). */
|
|
13
|
+
const N = (id, w, h) => ({id, w, h});
|
|
14
|
+
const nodes = [
|
|
15
|
+
N("es", 172, 96), N("norte", 172, 96), N("sur", 172, 96), N("nave", 172, 120),
|
|
16
|
+
N("d0", 116, 40), N("d1", 116, 40), N("c1", 172, 96),
|
|
17
|
+
];
|
|
18
|
+
const edges = [
|
|
19
|
+
{source: "es", target: "norte", rank: 0},
|
|
20
|
+
{source: "es", target: "sur", rank: 0},
|
|
21
|
+
{source: "norte", target: "nave", rank: 0},
|
|
22
|
+
{source: "nave", target: "c1", rank: 2},
|
|
23
|
+
{source: "nave", target: "d0", rank: 1},
|
|
24
|
+
{source: "nave", target: "d1", rank: 1},
|
|
25
|
+
{source: "c1", target: "d0", rank: 0},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
describe("the spanning tree", () => {
|
|
29
|
+
test("roots are the nodes with no incoming edge, in node order", () => {
|
|
30
|
+
let t = spanning_tree(nodes, edges);
|
|
31
|
+
expect(t.roots).toEqual(["es"]);
|
|
32
|
+
expect(t.depth.get("nave")).toBe(2);
|
|
33
|
+
expect(t.depth.get("d0")).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("children are ordered by hook rank, then by node order", () => {
|
|
37
|
+
let t = spanning_tree(nodes, edges);
|
|
38
|
+
expect(t.children.get("nave")).toEqual(["d0", "d1", "c1"]);
|
|
39
|
+
expect(t.children.get("es")).toEqual(["norte", "sur"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("a node with two parents belongs to the first that reaches it", () => {
|
|
43
|
+
let t = spanning_tree(nodes, edges);
|
|
44
|
+
expect(t.children.get("c1")).toEqual([]); /* d0 is nave's */
|
|
45
|
+
expect(t.depth.get("d0")).toBe(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a cycle nothing reaches gets a root; a self edge is ignored", () => {
|
|
49
|
+
let t = spanning_tree(
|
|
50
|
+
[N("a", 10, 10), N("b", 10, 10), N("c", 10, 10)],
|
|
51
|
+
[{source: "a", target: "b"}, {source: "b", target: "c"}, {source: "c", target: "a"},
|
|
52
|
+
{source: "b", target: "b"}]
|
|
53
|
+
);
|
|
54
|
+
expect(t.roots).toEqual(["a"]);
|
|
55
|
+
expect(t.children.get("a")).toEqual(["b"]);
|
|
56
|
+
expect(t.depth.get("c")).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("an edge to an unknown node is ignored", () => {
|
|
60
|
+
let t = spanning_tree([N("a", 10, 10)], [{source: "a", target: "ghost"}]);
|
|
61
|
+
expect(t.roots).toEqual(["a"]);
|
|
62
|
+
expect(t.children.get("a")).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("the tidy tree", () => {
|
|
67
|
+
test("depths are columns; a column is as wide as its widest card", () => {
|
|
68
|
+
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
69
|
+
expect(pos.get("es").x).toBe(86); /* 172/2 */
|
|
70
|
+
expect(pos.get("norte").x).toBe(172 + 100 + 86);
|
|
71
|
+
expect(pos.get("sur").x).toBe(pos.get("norte").x);
|
|
72
|
+
/* depth 3 holds chips (116) and c1 (172): column is 172 wide,
|
|
73
|
+
* the chips are centred in it */
|
|
74
|
+
let col3 = 3 * (172 + 100);
|
|
75
|
+
expect(pos.get("c1").x).toBe(col3 + 86);
|
|
76
|
+
expect(pos.get("d0").x).toBe(col3 + 86);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("siblings stack with the gap; a parent is centred on its children", () => {
|
|
80
|
+
let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
81
|
+
/* nave's children: d0 (40), d1 (40), c1 (96) = 196 tall */
|
|
82
|
+
expect(pos.get("d1").y - pos.get("d0").y).toBe(50);
|
|
83
|
+
expect(pos.get("c1").y - pos.get("d1").y).toBe(20 + 10 + 48);
|
|
84
|
+
let first = pos.get("d0").y - 20;
|
|
85
|
+
let last = pos.get("c1").y + 48;
|
|
86
|
+
expect(pos.get("nave").y).toBe((first + last) / 2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a parent taller than its children keeps its own height", () => {
|
|
90
|
+
let pos = layout_tree(
|
|
91
|
+
[N("p", 100, 200), N("k", 50, 20)],
|
|
92
|
+
[{source: "p", target: "k"}], {nodesep: 10, ranksep: 50}
|
|
93
|
+
);
|
|
94
|
+
expect(pos.get("p").y).toBe(100);
|
|
95
|
+
expect(pos.get("k").y).toBe(100); /* centred in the parent's block */
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("two trees are stacked, not overlapped", () => {
|
|
99
|
+
let pos = layout_tree(
|
|
100
|
+
[N("a", 100, 50), N("b", 100, 50)], [], {nodesep: 10}
|
|
101
|
+
);
|
|
102
|
+
expect(pos.get("a").y).toBe(25);
|
|
103
|
+
expect(pos.get("b").y).toBe(25 + 50 + 20);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
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});
|
|
108
|
+
let more = nodes.concat([N("d2", 116, 40)]);
|
|
109
|
+
let more_edges = edges.concat([{source: "nave", target: "d2", rank: 1}]);
|
|
110
|
+
let pos = layout_tree(more, more_edges, {nodesep: 10, ranksep: 100});
|
|
111
|
+
expect(pos.get("es").x).toBe(base.get("es").x);
|
|
112
|
+
expect(pos.get("d0").y).toBe(base.get("d0").y); /* above the new one */
|
|
113
|
+
expect(pos.get("d1").y).toBe(base.get("d1").y);
|
|
114
|
+
expect(pos.get("d2").y).toBe(base.get("d1").y + 50);
|
|
115
|
+
expect(pos.get("c1").y).toBe(base.get("c1").y + 50); /* pushed down, in order */
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("the radial tree", () => {
|
|
120
|
+
test("one root sits at the centre, its children on a ring around it", () => {
|
|
121
|
+
let pos = layout_radial(nodes, edges, {nodesep: 10, ranksep: 100});
|
|
122
|
+
expect(pos.get("es")).toEqual({x: 0, y: 0});
|
|
123
|
+
let r = Math.hypot(pos.get("norte").x, pos.get("norte").y);
|
|
124
|
+
expect(r).toBeGreaterThanOrEqual(100);
|
|
125
|
+
expect(Math.hypot(pos.get("sur").x, pos.get("sur").y)).toBeCloseTo(r, 6);
|
|
126
|
+
/* norte carries 3 of es's 4 leaves: three quarters of the
|
|
127
|
+
* circle, from the top (-90°) clockwise, so its middle is at
|
|
128
|
+
* +45°; sur gets the last quarter, from 180° to 270°, centred
|
|
129
|
+
* at 225° (-135°) */
|
|
130
|
+
let ang = (id) => Math.atan2(pos.get(id).y, pos.get(id).x);
|
|
131
|
+
expect(ang("norte")).toBeCloseTo(Math.PI / 4, 6);
|
|
132
|
+
expect(ang("sur")).toBeCloseTo(-3 * Math.PI / 4, 6);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("a ring is long enough for its nodes: no two neighbours closer than a card", () => {
|
|
136
|
+
let many = [N("root", 172, 96)];
|
|
137
|
+
let e = [];
|
|
138
|
+
for(let i = 0; i < 40; i++) {
|
|
139
|
+
many.push(N(`k${i}`, 172, 96));
|
|
140
|
+
e.push({source: "root", target: `k${i}`, rank: 0});
|
|
141
|
+
}
|
|
142
|
+
let pos = layout_radial(many, e, {nodesep: 10, ranksep: 100});
|
|
143
|
+
let r = Math.hypot(pos.get("k0").x, pos.get("k0").y);
|
|
144
|
+
/* 40 diagonals of ~197 plus gaps on the circumference */
|
|
145
|
+
expect(2 * Math.PI * r).toBeGreaterThanOrEqual(40 * (Math.hypot(172, 96) + 10) - 1e-6);
|
|
146
|
+
let d = Math.hypot(pos.get("k1").x - pos.get("k0").x, pos.get("k1").y - pos.get("k0").y);
|
|
147
|
+
expect(d).toBeGreaterThan(190);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("several roots share the circle around an empty centre", () => {
|
|
151
|
+
let pos = layout_radial(
|
|
152
|
+
[N("a", 100, 50), N("b", 100, 50), N("c", 100, 50)], [], {ranksep: 100}
|
|
153
|
+
);
|
|
154
|
+
for(let id of ["a", "b", "c"]) {
|
|
155
|
+
expect(Math.hypot(pos.get(id).x, pos.get(id).y)).toBeCloseTo(100, 6);
|
|
156
|
+
}
|
|
157
|
+
expect(pos.get("a").x).not.toBe(pos.get("b").x);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
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
|
+
});
|