@ankhorage/zora 22.1.2 → 22.1.4
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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts +3 -3
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js +3 -3
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.d.ts +6 -5
- package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.d.ts.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.js +107 -171
- package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.js.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts +4 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js +8 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js +5 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.d.ts +9 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.d.ts.map +1 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.js +234 -0
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.js.map +1 -0
- package/dist/types/graph-view-spacing.d.ts +15 -0
- package/dist/types/graph-view-spacing.d.ts.map +1 -0
- package/dist/types/graph-view-spacing.js +2 -0
- package/dist/types/graph-view-spacing.js.map +1 -0
- package/dist/types/graph-view.d.ts +4 -1
- package/dist/types/graph-view.d.ts.map +1 -1
- package/dist/types/graph-view.js.map +1 -1
- package/package.json +5 -5
- package/src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx +3 -3
- package/src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.test.ts +195 -69
- package/src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts +162 -241
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.test.ts +2 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts +8 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.ts +5 -1
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.ts +338 -0
- package/src/types/graph-view-spacing.ts +16 -0
- package/src/types/graph-view.ts +4 -1
- package/web-dist/components/graph-view/GraphView.d.ts +3 -3
- package/web-dist/components/graph-view/GraphView.js +237 -126
- package/web-dist/components/graph-view/GraphView.types-0.d.ts +4 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "22.1.
|
|
4
|
+
"version": "22.1.4",
|
|
5
5
|
"description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/zora#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@ankhorage/color-theory": "^0.3.1",
|
|
44
|
-
"@ankhorage/contracts": "^22.8.
|
|
44
|
+
"@ankhorage/contracts": "^22.8.101",
|
|
45
45
|
"@ankhorage/game": "^0.1.2",
|
|
46
46
|
"@ankhorage/runtime": "^3.0.59",
|
|
47
47
|
"@ankhorage/surface": "^9.1.112",
|
|
48
|
-
"@ankhorage/utility": "^1.9.
|
|
48
|
+
"@ankhorage/utility": "^1.9.84",
|
|
49
49
|
"expo-document-picker": "~57.0.3",
|
|
50
50
|
"expo-image-picker": "~57.0.20",
|
|
51
51
|
"chess.js": "^1.4.0"
|
|
@@ -124,8 +124,8 @@
|
|
|
124
124
|
"version-packages": "ankhorage-changeset version"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@ankhorage/devtools": "^2.1.
|
|
128
|
-
"@ankhorage/paradox": "^0.2.
|
|
127
|
+
"@ankhorage/devtools": "^2.1.8",
|
|
128
|
+
"@ankhorage/paradox": "^0.2.28",
|
|
129
129
|
"@react-native-vector-icons/fontawesome": "^13.1.4",
|
|
130
130
|
"@react-native-vector-icons/fontawesome5": "^13.1.4",
|
|
131
131
|
"@react-native-vector-icons/fontawesome6": "^13.1.4",
|
|
@@ -21,9 +21,9 @@ type GraphContainer = NonNullable<CytoscapeOptions['container']>;
|
|
|
21
21
|
* minReadableLabelSize Expand the manual zoom ceiling until plain labels reach this CSS-pixel size.
|
|
22
22
|
* maxFitLabelSize Cap automatic fitting at this plain-label CSS-pixel size, leaving whitespace for tiny graphs.
|
|
23
23
|
* onSpacingFactorChange Receive the spacing accepted by explicit fit({ optimizeSpacing: true }).
|
|
24
|
-
* Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering,
|
|
25
|
+
* stops once effective fit reaches maxFitLabelSize, keeps labels and unrelated compound boundaries
|
|
26
|
+
* collision-safe, and permits only a small rendered background-overlap fallback for dense graphs.
|
|
27
27
|
*/
|
|
28
28
|
export function GraphView(props: GraphViewProps) {
|
|
29
29
|
const { containerRef, renderedNodes, runtimeRef } = useGraphViewRuntime(props);
|
|
@@ -1,113 +1,239 @@
|
|
|
1
1
|
import { expect, test } from 'bun:test';
|
|
2
|
-
import cytoscape, { type NodeSingular } from 'cytoscape';
|
|
2
|
+
import cytoscape, { type Core, type CytoscapeOptions, type NodeSingular } from 'cytoscape';
|
|
3
3
|
|
|
4
4
|
import { compactGraphSpacing } from './compactGraphSpacing';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const FIT_OPTIONS = { fitPadding: 50, maxFitZoom: 1, minReadableZoom: 1 } as const;
|
|
7
|
+
|
|
8
|
+
test('keeps an already readable graph unchanged once the fit-size target is reached', () => {
|
|
9
|
+
const cy = createGraph(
|
|
10
|
+
[
|
|
11
|
+
{ data: { id: 'a' }, position: { x: 0, y: 0 } },
|
|
12
|
+
{ data: { id: 'b' }, position: { x: 300, y: 0 } },
|
|
13
|
+
],
|
|
14
|
+
800,
|
|
15
|
+
600,
|
|
16
|
+
);
|
|
17
|
+
try {
|
|
18
|
+
const before = cy.getElementById('b').position();
|
|
19
|
+
const spacing = compactGraphSpacing(cy, 1, { ...FIT_OPTIONS, maxFitZoom: 1.5 });
|
|
20
|
+
expect(spacing).toBe(1);
|
|
21
|
+
expect(cy.getElementById('b').position()).toEqual(before);
|
|
22
|
+
} finally {
|
|
23
|
+
cy.destroy();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('compacts only until the effective fit reaches its cap while preserving node separation', () => {
|
|
28
|
+
const cy = createGraph(
|
|
29
|
+
[
|
|
12
30
|
{ data: { id: 'a' }, position: { x: 0, y: 0 } },
|
|
13
|
-
{ data: { id: 'b' }, position: { x:
|
|
31
|
+
{ data: { id: 'b' }, position: { x: 1000, y: 0 } },
|
|
14
32
|
{ data: { id: 'e', source: 'a', target: 'b', weight: 7 } },
|
|
15
33
|
],
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
800,
|
|
35
|
+
600,
|
|
36
|
+
);
|
|
18
37
|
try {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
expect(factor).toBeGreaterThan(0.17);
|
|
26
|
-
expect(factor).toBeLessThan(0.19);
|
|
27
|
-
expect(b.boundingBox().x1 - a.boundingBox().x2).toBeLessThan(0);
|
|
28
|
-
|
|
29
|
-
const labelA = readLabelBox(a);
|
|
30
|
-
const labelB = readLabelBox(b);
|
|
31
|
-
const overlap = Math.min(labelA.x2, labelB.x2) - Math.max(labelA.x1, labelB.x1);
|
|
32
|
-
expect(overlap).toBeGreaterThan(0);
|
|
33
|
-
expect(overlap).toBeLessThanOrEqual(40 * 0.12 + 0.2);
|
|
38
|
+
const spacing = compactGraphSpacing(cy, 1, FIT_OPTIONS);
|
|
39
|
+
const first = cy.getElementById('a').boundingBox();
|
|
40
|
+
const second = cy.getElementById('b').boundingBox();
|
|
41
|
+
expect(spacing).toBeGreaterThan(0.59);
|
|
42
|
+
expect(spacing).toBeLessThan(0.61);
|
|
43
|
+
expect(second.x1 - first.x2).toBeGreaterThan(0);
|
|
34
44
|
expect(cy.edges()[0].data('weight')).toBe(7);
|
|
35
|
-
expect(a.width()).toBe(100);
|
|
45
|
+
expect(cy.getElementById('a').width()).toBe(100);
|
|
36
46
|
} finally {
|
|
37
47
|
cy.destroy();
|
|
38
48
|
}
|
|
39
49
|
});
|
|
40
50
|
|
|
41
|
-
test('
|
|
42
|
-
const cy =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
test('keeps strict separation when it can preserve the readable-label threshold', () => {
|
|
52
|
+
const cy = createGraph(
|
|
53
|
+
[
|
|
54
|
+
{ data: { id: 'a' }, position: { x: 0, y: 0 } },
|
|
55
|
+
{ data: { id: 'b' }, position: { x: 120, y: 0 } },
|
|
56
|
+
],
|
|
57
|
+
340,
|
|
58
|
+
400,
|
|
59
|
+
);
|
|
60
|
+
installMeasuredLabelBox(cy.getElementById('a'), 40, 20);
|
|
61
|
+
installMeasuredLabelBox(cy.getElementById('b'), 40, 20);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const spacing = compactGraphSpacing(cy, 1, {
|
|
65
|
+
fitPadding: 50,
|
|
66
|
+
maxFitZoom: 1.5,
|
|
67
|
+
minReadableZoom: 1.1,
|
|
68
|
+
});
|
|
69
|
+
const first = cy.getElementById('a').boundingBox({ includeLabels: false });
|
|
70
|
+
const second = cy.getElementById('b').boundingBox({ includeLabels: false });
|
|
71
|
+
expect(spacing).toBeGreaterThan(0.8);
|
|
72
|
+
expect(spacing).toBeLessThan(0.9);
|
|
73
|
+
expect(Math.min(first.x2, second.x2) - Math.max(first.x1, second.x1)).toBeLessThanOrEqual(0);
|
|
74
|
+
} finally {
|
|
75
|
+
cy.destroy();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('uses a small rendered background-overlap budget only when strict separation cannot reach the fit target', () => {
|
|
80
|
+
const cy = createGraph(
|
|
81
|
+
[
|
|
82
|
+
{ data: { id: 'a' }, position: { x: 0, y: 0 } },
|
|
83
|
+
{ data: { id: 'b' }, position: { x: 120, y: 0 } },
|
|
84
|
+
],
|
|
85
|
+
296,
|
|
86
|
+
400,
|
|
87
|
+
);
|
|
88
|
+
installMeasuredLabelBox(cy.getElementById('a'), 40, 20);
|
|
89
|
+
installMeasuredLabelBox(cy.getElementById('b'), 40, 20);
|
|
90
|
+
try {
|
|
91
|
+
const spacing = compactGraphSpacing(cy, 1, FIT_OPTIONS);
|
|
92
|
+
const first = cy.getElementById('a').boundingBox({ includeLabels: false });
|
|
93
|
+
const second = cy.getElementById('b').boundingBox({ includeLabels: false });
|
|
94
|
+
const visualOverlap = Math.min(first.x2, second.x2) - Math.max(first.x1, second.x1);
|
|
95
|
+
const firstLabel = readLabelBox(cy.getElementById('a'));
|
|
96
|
+
const secondLabel = readLabelBox(cy.getElementById('b'));
|
|
97
|
+
const labelOverlap =
|
|
98
|
+
Math.min(firstLabel.x2, secondLabel.x2) - Math.max(firstLabel.x1, secondLabel.x1);
|
|
99
|
+
|
|
100
|
+
expect(spacing).toBeGreaterThan(0.79);
|
|
101
|
+
expect(spacing).toBeLessThan(0.84);
|
|
102
|
+
expect(visualOverlap).toBeGreaterThan(0);
|
|
103
|
+
expect(visualOverlap).toBeLessThanOrEqual(4.2);
|
|
104
|
+
expect(labelOverlap).toBeLessThanOrEqual(0);
|
|
105
|
+
} finally {
|
|
106
|
+
cy.destroy();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('expands cramped labels and nodes when the viewport can keep them separated at the fit target', () => {
|
|
111
|
+
const cy = createGraph(
|
|
112
|
+
[
|
|
47
113
|
{ data: { id: 'a' }, position: { x: 0, y: 0 } },
|
|
48
114
|
{ data: { id: 'b' }, position: { x: 20, y: 0 } },
|
|
49
115
|
],
|
|
50
|
-
|
|
51
|
-
|
|
116
|
+
800,
|
|
117
|
+
600,
|
|
118
|
+
);
|
|
119
|
+
installMeasuredLabelBox(cy.getElementById('a'), 40, 20);
|
|
120
|
+
installMeasuredLabelBox(cy.getElementById('b'), 40, 20);
|
|
52
121
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
expect(
|
|
58
|
-
expect(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
40 * 0.12 + 0.2,
|
|
63
|
-
);
|
|
122
|
+
const spacing = compactGraphSpacing(cy, 0.1, FIT_OPTIONS);
|
|
123
|
+
const first = cy.getElementById('a').boundingBox({ includeLabels: false });
|
|
124
|
+
const second = cy.getElementById('b').boundingBox({ includeLabels: false });
|
|
125
|
+
expect(spacing).toBeGreaterThanOrEqual(0.49);
|
|
126
|
+
expect(second.x1 - first.x2).toBeGreaterThanOrEqual(-0.2);
|
|
127
|
+
expect(
|
|
128
|
+
Math.min(readLabelBox(cy.getElementById('a')).x2, readLabelBox(cy.getElementById('b')).x2) -
|
|
129
|
+
Math.max(readLabelBox(cy.getElementById('a')).x1, readLabelBox(cy.getElementById('b')).x1),
|
|
130
|
+
).toBeLessThanOrEqual(0);
|
|
64
131
|
} finally {
|
|
65
132
|
cy.destroy();
|
|
66
133
|
}
|
|
67
134
|
});
|
|
68
135
|
|
|
69
|
-
test('
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
136
|
+
test('keeps mixed-width Atlas-like compound geometry readable while using available viewport', () => {
|
|
137
|
+
const cy = createGraph(
|
|
138
|
+
[
|
|
139
|
+
{ data: { id: 'group' } },
|
|
140
|
+
{ data: { id: 'theme', parent: 'group' }, position: { x: 0, y: 0 } },
|
|
141
|
+
{ data: { id: 'workspace', parent: 'group' }, position: { x: 360, y: 0 } },
|
|
142
|
+
{ data: { id: 'project-tree' }, position: { x: 760, y: 0 } },
|
|
143
|
+
{ data: { id: 'project-source' }, position: { x: 1180, y: 0 } },
|
|
144
|
+
],
|
|
145
|
+
1600,
|
|
146
|
+
700,
|
|
147
|
+
);
|
|
148
|
+
const widths = new Map([
|
|
149
|
+
['theme', 180],
|
|
150
|
+
['workspace', 220],
|
|
151
|
+
['project-tree', 260],
|
|
152
|
+
['project-source', 300],
|
|
153
|
+
]);
|
|
154
|
+
for (const [id, width] of widths) {
|
|
155
|
+
cy.getElementById(id).style({ width, height: 64 });
|
|
156
|
+
installMeasuredLabelBox(cy.getElementById(id), width - 32, 24);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const spacing = compactGraphSpacing(cy, 1, { fitPadding: 50, maxFitZoom: 1.4 });
|
|
161
|
+
expect(spacing).toBeLessThan(1);
|
|
162
|
+
expect(spacing).toBeGreaterThan(0.6);
|
|
163
|
+
|
|
164
|
+
const leaves = ['theme', 'workspace', 'project-tree', 'project-source'].map((id) =>
|
|
165
|
+
cy.getElementById(id).boundingBox({ includeLabels: false }),
|
|
166
|
+
);
|
|
167
|
+
for (const [index, first] of leaves.entries()) {
|
|
168
|
+
for (const second of leaves.slice(index + 1)) {
|
|
169
|
+
const overlapWidth = Math.min(first.x2, second.x2) - Math.max(first.x1, second.x1);
|
|
170
|
+
const overlapHeight = Math.min(first.y2, second.y2) - Math.max(first.y1, second.y1);
|
|
171
|
+
expect(overlapWidth > 0 && overlapHeight > 0).toBe(false);
|
|
172
|
+
}
|
|
84
173
|
}
|
|
174
|
+
expect(cy.getElementById('theme').data('parent')).toBe('group');
|
|
175
|
+
expect(cy.getElementById('workspace').data('parent')).toBe('group');
|
|
176
|
+
} finally {
|
|
177
|
+
cy.destroy();
|
|
85
178
|
}
|
|
86
179
|
});
|
|
87
180
|
|
|
88
|
-
test('
|
|
89
|
-
const cy =
|
|
90
|
-
|
|
91
|
-
styleEnabled: true,
|
|
92
|
-
layout: { name: 'preset' },
|
|
93
|
-
elements: [
|
|
181
|
+
test('preserves compound containment while keeping peer groups separated', () => {
|
|
182
|
+
const cy = createGraph(
|
|
183
|
+
[
|
|
94
184
|
{ data: { id: 'p' } },
|
|
95
185
|
{ data: { id: 'p.a', parent: 'p' }, position: { x: 0, y: 0 } },
|
|
96
186
|
{ data: { id: 'q' } },
|
|
97
187
|
{ data: { id: 'q.a', parent: 'q' }, position: { x: 800, y: 0 } },
|
|
98
188
|
],
|
|
99
|
-
|
|
189
|
+
1000,
|
|
190
|
+
600,
|
|
191
|
+
);
|
|
100
192
|
try {
|
|
101
|
-
expect(compactGraphSpacing(cy, 1)).toBeLessThan(1);
|
|
193
|
+
expect(compactGraphSpacing(cy, 1, { fitPadding: 50 })).toBeLessThan(1);
|
|
102
194
|
expect(
|
|
103
195
|
cy.getElementById('q').boundingBox().x1 - cy.getElementById('p').boundingBox().x2,
|
|
104
|
-
).toBeGreaterThanOrEqual(0);
|
|
196
|
+
).toBeGreaterThanOrEqual(-0.2);
|
|
105
197
|
expect(cy.getElementById('p.a').data('parent')).toBe('p');
|
|
106
198
|
} finally {
|
|
107
199
|
cy.destroy();
|
|
108
200
|
}
|
|
109
201
|
});
|
|
110
202
|
|
|
203
|
+
test('is idempotent after a capped optimized fit', () => {
|
|
204
|
+
const cy = createGraph(
|
|
205
|
+
[
|
|
206
|
+
{ data: { id: 'a' }, position: { x: 0, y: 0 } },
|
|
207
|
+
{ data: { id: 'b' }, position: { x: 1000, y: 0 } },
|
|
208
|
+
],
|
|
209
|
+
800,
|
|
210
|
+
600,
|
|
211
|
+
);
|
|
212
|
+
try {
|
|
213
|
+
const firstSpacing = compactGraphSpacing(cy, 1, FIT_OPTIONS);
|
|
214
|
+
const firstPosition = { ...cy.getElementById('b').position() };
|
|
215
|
+
const secondSpacing = compactGraphSpacing(cy, firstSpacing, FIT_OPTIONS);
|
|
216
|
+
expect(secondSpacing).toBe(firstSpacing);
|
|
217
|
+
expect(cy.getElementById('b').position()).toEqual(firstPosition);
|
|
218
|
+
} finally {
|
|
219
|
+
cy.destroy();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/*** Create one measured headless graph with stable viewport and visual node dimensions. */
|
|
224
|
+
function createGraph(elements: CytoscapeOptions['elements'], width: number, height: number): Core {
|
|
225
|
+
const cy = cytoscape({
|
|
226
|
+
headless: true,
|
|
227
|
+
styleEnabled: true,
|
|
228
|
+
layout: { name: 'preset' },
|
|
229
|
+
elements,
|
|
230
|
+
style: [{ selector: 'node', style: { width: 100, height: 40, padding: '0px' } }],
|
|
231
|
+
});
|
|
232
|
+
cy.width = () => width;
|
|
233
|
+
cy.height = () => height;
|
|
234
|
+
return cy;
|
|
235
|
+
}
|
|
236
|
+
|
|
111
237
|
/*** Install a dynamic renderer-style label box for headless Cytoscape tests. */
|
|
112
238
|
function installMeasuredLabelBox(node: NodeSingular, width: number, height: number): void {
|
|
113
239
|
const originalBoundingBox = node.boundingBox.bind(node);
|