Skip to content

Commit 32f8362

Browse files
Merge pull request #62 from zazuko/layout-box-size
Layout box size
2 parents be846c6 + 4028189 commit 32f8362

File tree

19 files changed

+791
-706
lines changed

19 files changed

+791
-706
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ coverage
3333

3434
src-vscode/media/assets/main.js
3535
src-vscode/media/
36-
src-vscode/*.vsix
36+
*.vsix

package-lock.json

Lines changed: 485 additions & 481 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rdf-sketch",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"private": true,
55
"type": "module",
66
"workspaces": [
@@ -21,11 +21,11 @@
2121
"@vue-flow/core": "^1.48.2",
2222
"@zazuko/env": "^3.0.1",
2323
"@zazuko/prefixes": "^2.6.1",
24-
"elkjs": "^0.11.0",
24+
"elkjs": "^0.11.1",
2525
"primeicons": "^7.0.0",
2626
"primevue": "^4.5.4",
2727
"util": "^0.12.5",
28-
"vue": "^3.5.29"
28+
"vue": "^3.5.31"
2929
},
3030
"devDependencies": {
3131
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
@@ -35,17 +35,17 @@
3535
"@tsconfig/node20": "^20.1.9",
3636
"@types/n3": "^1.26.1",
3737
"@types/node": "^25.3.3",
38-
"@vitejs/plugin-vue": "^6.0.4",
38+
"@vitejs/plugin-vue": "^6.0.5",
3939
"@vue/eslint-config-typescript": "^14.7.0",
40-
"@vue/tsconfig": "^0.9.0",
41-
"eslint": "^10.0.2",
40+
"@vue/tsconfig": "^0.9.1",
41+
"eslint": "^10.1.0",
4242
"eslint-plugin-vue": "^10.8.0",
4343
"npm-run-all2": "^8.0.4",
4444
"rollup-plugin-node-polyfills": "^0.2.1",
4545
"typescript": "~5.9.3",
4646
"vite": "^7.3.1",
4747
"vite-plugin-css-injected-by-js": "^4.0.1",
4848
"vite-plugin-pwa": "^1.2.0",
49-
"vue-tsc": "^3.2.5"
49+
"vue-tsc": "^3.2.6"
5050
}
5151
}

packages/core-ui/src/components/GraphView.vue

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
<template>
2-
<div style="height: 100%; width:100%">
2+
<div style="height: 100%; width:100%; position: relative;">
3+
<div v-if="isLoading" class="loading-overlay">
4+
<div class="loader"></div>
5+
<div style="margin-top: 1rem; font-family: sans-serif;">Rendering graph layout...</div>
6+
</div>
37
<VueFlow
48
:nodes="nodes"
59
:edges="edges"
610
:min-zoom="0.00005"
711
:max-zoom="10"
812
:fit-view-on-init="true"
13+
@nodes-initialized="onNodesInitialized"
914
@node-drag="onNodeDrag"
1015
@edge-click="zoomToNode"
1116
>
@@ -63,37 +68,74 @@ const links = computed(() => {
6368
6469
const nodes = ref<CustomNode[]>([])
6570
const edges = ref<CustomEdge[]>([])
71+
const isLoading = ref<boolean>(true)
6672
67-
watch(links, async (newLinks) => {
68-
const nodesWithoutLayout = resources.value.map(resource => ({
73+
watch(links, (newLinks) => {
74+
isLoading.value = true;
75+
nodes.value = resources.value.map(resource => ({
6976
id: resource.id,
7077
type: 'custom',
7178
position: { x: 0, y: 0 },
72-
data: {
73-
resource
74-
},
75-
}));
79+
data: { resource },
80+
style: { opacity: 0 } // Hide nodes until layout is calculated
81+
})) as CustomNode[];
7682
77-
const newEdges = newLinks.map(link => ({
83+
edges.value = newLinks.map(link => ({
7884
id: `${link.source}-${link.sourceProperty}-${link.target}`,
7985
source: link.source,
8086
target: link.target,
8187
sourceHandle: `${link.source}-${link.sourceProperty}-right`,
8288
animated: false,
8389
data: link,
8490
type: 'custom',
85-
markerEnd: MarkerType.ArrowClosed
86-
}));
87-
88-
const nodesWithLayout = await elkLayout(nodesWithoutLayout, newEdges);
89-
nodes.value = (nodesWithLayout as any).nodes as unknown as CustomNode[];
90-
edges.value = (nodesWithLayout as any).edges as unknown as CustomEdge[];
91-
92-
setTimeout(() => {
93-
fitView({ padding: 0.1, duration: 800 })
94-
}, 200);
91+
markerEnd: MarkerType.ArrowClosed
92+
})) as CustomEdge[];
9593
},{ immediate: true });
9694
95+
async function onNodesInitialized() {
96+
// Wait a moment for Vue Flow to fully apply CSS and typography rendering to DOM
97+
// so that node.dimensions gets accurately populated by the internal ResizeObserver.
98+
setTimeout(async () => {
99+
// Vue Flow stores actual DOM dimensions in its internal nodeLookup state!
100+
// We must merge these dimensions into our node array before layout.
101+
const nodesWithDimensions = nodes.value.map(n => {
102+
const internalNode = nodeLookup.value.get(n.id)
103+
return {
104+
...n,
105+
dimensions: {
106+
width: internalNode?.dimensions?.width || 500,
107+
height: internalNode?.dimensions?.height || 200
108+
}
109+
}
110+
});
111+
112+
const nodesWithLayout = await elkLayout(nodesWithDimensions, edges.value);
113+
114+
nodes.value = (nodesWithLayout as any).nodes.map((n: CustomNode) => ({
115+
...n,
116+
style: { opacity: 1 } // Show nodes once layout is applied
117+
}));
118+
edges.value = (nodesWithLayout as any).edges as CustomEdge[];
119+
120+
// Update edge handles based on computed positions
121+
edges.value.forEach(e => {
122+
const sourceNode = nodes.value.find(n => n.id === e.source);
123+
const targetNode = nodes.value.find(n => n.id === e.target);
124+
if (sourceNode && targetNode) {
125+
const isTargetLeft = targetNode.position.x < sourceNode.position.x;
126+
e.sourceHandle = `${sourceNode.id}-${e.data?.sourceProperty}-${isTargetLeft ? 'left' : 'right'}`;
127+
}
128+
});
129+
130+
setTimeout(() => {
131+
fitView({ padding: 0.1, duration: 800 })
132+
setTimeout(() => {
133+
isLoading.value = false;
134+
}, 800);
135+
}, 200);
136+
}, 50);
137+
}
138+
97139
function onNodeDrag(nodeDragEvent: NodeDragEvent) {
98140
const focusNode = nodeDragEvent.node;
99141
@@ -201,5 +243,34 @@ function onNodeDrag(nodeDragEvent: NodeDragEvent) {
201243
color: #8a9ba1;
202244
}
203245
246+
.loading-overlay {
247+
position: absolute;
248+
top: 0;
249+
left: 0;
250+
right: 0;
251+
bottom: 0;
252+
display: flex;
253+
flex-direction: column;
254+
align-items: center;
255+
justify-content: center;
256+
background-color: var(--vscode-editor-background, rgba(255, 255, 255, 0.7));
257+
z-index: 1000;
258+
backdrop-filter: blur(2px);
259+
}
260+
261+
.loader {
262+
border: 4px solid var(--vscode-dropdown-border, #f3f3f3);
263+
border-top: 4px solid var(--vscode-button-background, #3498db);
264+
border-radius: 50%;
265+
width: 40px;
266+
height: 40px;
267+
animation: spin 1s linear infinite;
268+
}
269+
270+
@keyframes spin {
271+
0% { transform: rotate(0deg); }
272+
100% { transform: rotate(360deg); }
273+
}
274+
204275
</style>
205276

packages/core-ui/src/components/RdfTerm.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,15 @@ function avoidNodeDrag(event: MouseEvent) {
8686
}
8787
.named-node:hover .node-text {
8888
text-decoration: underline;
89-
color: blue;
89+
color: var(--p-primary-color, #007acc);
9090
}
9191
92-
9392
.blank-node:hover .arrow {
9493
opacity: 1;
9594
}
9695
.blank-node:hover .node-text {
9796
text-decoration: underline;
98-
color: blue;
97+
color: var(--p-primary-color, #007acc);
9998
}
10099
101100
.data-type {

packages/core-ui/src/components/SPOSearch.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ v-if="spo"
77
:value="spo"
88
:itemSize="itemSizeWithPadding"
99
scrollable
10-
:scrollHeight="props.isVscode ? 'calc(40vh - 0px)' : 'calc(40vh - 42px)'" :virtualScrollerOptions="{ itemSize: itemSizeWithPadding }"
10+
scrollHeight="flex"
11+
:virtualScrollerOptions="{ itemSize: itemSizeWithPadding }"
1112
class="spo-table"
13+
style="height: 100%; display: flex; flex-direction: column;"
1214
tableStyle="table-layout: fixed">
1315
<Column field="subject" header="Subject" :style="{ width: hasContext ? '25%' : '33.33%' }">
1416
<template #filter="{ filterModel, filterCallback }">
@@ -155,6 +157,11 @@ span {
155157
156158
.node-link {
157159
cursor: pointer;
160+
text-decoration: none;
161+
color: inherit;
162+
}
163+
.node-link:hover {
164+
color: var(--p-primary-color, #007acc);
158165
text-decoration: underline;
159166
}
160167

packages/core-ui/src/components/graph/resource-node/ResourceNode.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ function zoomToNode(term: Term) {
8484
background-color: white;
8585
box-shadow: 0 4px 6px rgba(107, 107, 107, 0.8);
8686
min-width: 500px;
87-
88-
87+
width: max-content;
88+
max-width: max-content;
8989
}
9090
9191
.resource-card-header {

packages/core-ui/src/layout/use-layout.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,58 @@ const rowHeight = 52;
1111
*/
1212
export function useLayout() {
1313

14-
async function elkLayout(nodes: Node[], edges: Edge[]) {
14+
async function elkLayout(nodes: any[], edges: any[]) {
1515

1616
const options = {
17-
"algorithm": "mrtree", // MRTREE algorithm for tree layout
18-
"org.eclipse.elk.direction": "RIGHT", // Layout direction (can be UP, DOWN, LEFT, RIGHT)
19-
"org.eclipse.elk.spacing.nodeNode": "400", // Space between nodes
20-
"org.eclipse.elk.spacing.edgeEdge": "20", // Space between edges
21-
"org.eclipse.elk.spacing.edgeNode": "30", // Space between nodes and edges
22-
"org.eclipse.elk.mrtree.spacing.level": "500", // Space between levels in the tree
23-
"org.eclipse.elk.mrtree.compaction.strategy": "DOWN", // Compaction strategy
24-
"org.eclipse.elk.mrtree.nodePlacement.strategy": "SIMPLE", // Simple node placement strategy
25-
"org.eclipse.elk.mrtree.nodePlacement.bk.fixedAlignment": "BALANCED", // Balanced alignment for nodes
17+
"algorithm": "layered", // Best for directed graphs (RDF)
18+
"org.eclipse.elk.direction": "RIGHT", // Subject on left -> Object on right
19+
20+
// Layering strategies
21+
"org.eclipse.elk.layered.layering.strategy": "NETWORK_SIMPLEX", // Groups nodes efficiently
22+
"org.eclipse.elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
23+
24+
// Edge routing
25+
"org.eclipse.elk.edgeRouting": "ORTHOGONAL", // Use clean right-angle lines
26+
"org.eclipse.elk.layered.crossingMinimization.strategy": "LAYER_SWEEP", // Actively prevents tangled lines
27+
28+
// Spacing
29+
"org.eclipse.elk.spacing.nodeNode": "900", // Vertical space between nodes in the same layer
30+
"org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "1200", // Horizontal space between layers
31+
"org.eclipse.elk.spacing.edgeNode": "150", // Space between edges and nodes
32+
"org.eclipse.elk.spacing.edgeEdge": "75", // Space between parallel edges
2633
};
2734

2835
const elkNodes: any[] = nodes.map((node) => {
36+
const elkW = node.dimensions?.width || 1000;
37+
const elkH = node.dimensions?.height || (headerHeight + rowHeight * node.data.resource.properties.length);
2938
return {
3039
id: node.id,
31-
width: 1000,
32-
height: headerHeight + rowHeight * node.data.resource.properties.length,
40+
width: elkW,
41+
height: elkH,
3342
labels: [{ text: node.data.resource.name }],
34-
properties: node.data.resource.properties.map((property) => {
43+
// Map properties to ELK Ports so the routing algorithm knows the exact vertical origin
44+
ports: node.data.resource.properties.map((property: any, index: number) => {
3545
return {
36-
id: property.id,
37-
width: 1000,
38-
height: rowHeight,
39-
labels: [{ text: property.name }],
46+
id: `${node.id}-${property.id}`, // Unique port ID per node
47+
width: 1,
48+
height: 1,
49+
// Tell ELK this port is roughly at this vertical offset on the right side
50+
properties: {
51+
"org.eclipse.elk.port.side": "EAST",
52+
"org.eclipse.elk.port.index": index,
53+
}
4054
}
4155
}),
4256
}
4357
});
4458

4559
const elkEdges = edges.map((edge) => {
4660
return {
47-
id: `${edge.source}-${edge.target}`,
61+
id: edge.id, // e.g. "source-prop-target"
4862
sources: [edge.source],
4963
targets: [edge.target],
64+
// Tell elk exactly which port (property) on the source node this edge comes from
65+
sourcePort: `${edge.source}-${edge.data?.sourceProperty}`
5066
}
5167
});
5268

packages/vscode-ext/media/assets/main.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/vscode-ext/media/assets/main.js

Lines changed: 115 additions & 115 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)