backpack-viewer 0.2.16 → 0.2.19
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 +73 -1
- package/bin/serve.js +155 -0
- package/dist/api.d.ts +27 -0
- package/dist/api.js +71 -0
- package/dist/app/assets/index-CTM-vKgB.js +21 -0
- package/dist/app/assets/index-CjzMJjZ-.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +22 -1
- package/dist/canvas.js +143 -65
- package/dist/config.d.ts +4 -0
- package/dist/config.js +32 -0
- package/dist/default-config.json +66 -0
- package/dist/dialog.d.ts +9 -0
- package/dist/dialog.js +119 -0
- package/dist/info-panel.d.ts +4 -0
- package/dist/info-panel.js +66 -11
- package/dist/keybindings.d.ts +6 -0
- package/dist/keybindings.js +67 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +18 -8
- package/dist/main.js +175 -42
- package/dist/search.d.ts +5 -1
- package/dist/search.js +46 -85
- package/dist/shortcuts.d.ts +3 -1
- package/dist/shortcuts.js +51 -19
- package/dist/sidebar.d.ts +8 -0
- package/dist/sidebar.js +106 -1
- package/dist/style.css +328 -6
- package/dist/tools-pane.d.ts +10 -0
- package/dist/tools-pane.js +407 -148
- package/package.json +1 -1
- package/dist/app/assets/index-Mi0vDG5K.js +0 -21
- package/dist/app/assets/index-z15vEFEy.css +0 -1
package/dist/dialog.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Lightweight inline dialog system — replaces native alert/confirm/prompt. */
|
|
2
|
+
const DIALOG_CSS_CLASS = "bp-dialog-overlay";
|
|
3
|
+
function createOverlay() {
|
|
4
|
+
// Remove any existing dialog
|
|
5
|
+
document.querySelector(`.${DIALOG_CSS_CLASS}`)?.remove();
|
|
6
|
+
const overlay = document.createElement("div");
|
|
7
|
+
overlay.className = DIALOG_CSS_CLASS;
|
|
8
|
+
document.body.appendChild(overlay);
|
|
9
|
+
return overlay;
|
|
10
|
+
}
|
|
11
|
+
function createModal(overlay, title) {
|
|
12
|
+
const modal = document.createElement("div");
|
|
13
|
+
modal.className = "bp-dialog";
|
|
14
|
+
const heading = document.createElement("h4");
|
|
15
|
+
heading.className = "bp-dialog-title";
|
|
16
|
+
heading.textContent = title;
|
|
17
|
+
modal.appendChild(heading);
|
|
18
|
+
overlay.appendChild(modal);
|
|
19
|
+
overlay.addEventListener("click", (e) => {
|
|
20
|
+
if (e.target === overlay)
|
|
21
|
+
overlay.remove();
|
|
22
|
+
});
|
|
23
|
+
return modal;
|
|
24
|
+
}
|
|
25
|
+
function addButtons(modal, buttons) {
|
|
26
|
+
const row = document.createElement("div");
|
|
27
|
+
row.className = "bp-dialog-buttons";
|
|
28
|
+
for (const btn of buttons) {
|
|
29
|
+
const el = document.createElement("button");
|
|
30
|
+
el.className = "bp-dialog-btn";
|
|
31
|
+
if (btn.accent)
|
|
32
|
+
el.classList.add("bp-dialog-btn-accent");
|
|
33
|
+
if (btn.danger)
|
|
34
|
+
el.classList.add("bp-dialog-btn-danger");
|
|
35
|
+
el.textContent = btn.label;
|
|
36
|
+
el.addEventListener("click", btn.onClick);
|
|
37
|
+
row.appendChild(el);
|
|
38
|
+
}
|
|
39
|
+
modal.appendChild(row);
|
|
40
|
+
}
|
|
41
|
+
/** Show a confirmation dialog. Returns a promise that resolves to true/false. */
|
|
42
|
+
export function showConfirm(title, message) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const overlay = createOverlay();
|
|
45
|
+
const modal = createModal(overlay, title);
|
|
46
|
+
const msg = document.createElement("p");
|
|
47
|
+
msg.className = "bp-dialog-message";
|
|
48
|
+
msg.textContent = message;
|
|
49
|
+
modal.appendChild(msg);
|
|
50
|
+
addButtons(modal, [
|
|
51
|
+
{ label: "Cancel", onClick: () => { overlay.remove(); resolve(false); } },
|
|
52
|
+
{ label: "Confirm", accent: true, onClick: () => { overlay.remove(); resolve(true); } },
|
|
53
|
+
]);
|
|
54
|
+
// Focus the confirm button
|
|
55
|
+
modal.querySelector(".bp-dialog-btn-accent")?.focus();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Show a prompt dialog with an input field. Returns null if cancelled. */
|
|
59
|
+
export function showPrompt(title, placeholder, defaultValue) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const overlay = createOverlay();
|
|
62
|
+
const modal = createModal(overlay, title);
|
|
63
|
+
const input = document.createElement("input");
|
|
64
|
+
input.type = "text";
|
|
65
|
+
input.className = "bp-dialog-input";
|
|
66
|
+
input.placeholder = placeholder ?? "";
|
|
67
|
+
input.value = defaultValue ?? "";
|
|
68
|
+
modal.appendChild(input);
|
|
69
|
+
const submit = () => {
|
|
70
|
+
const val = input.value.trim();
|
|
71
|
+
overlay.remove();
|
|
72
|
+
resolve(val || null);
|
|
73
|
+
};
|
|
74
|
+
input.addEventListener("keydown", (e) => {
|
|
75
|
+
if (e.key === "Enter")
|
|
76
|
+
submit();
|
|
77
|
+
if (e.key === "Escape") {
|
|
78
|
+
overlay.remove();
|
|
79
|
+
resolve(null);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
addButtons(modal, [
|
|
83
|
+
{ label: "Cancel", onClick: () => { overlay.remove(); resolve(null); } },
|
|
84
|
+
{ label: "OK", accent: true, onClick: submit },
|
|
85
|
+
]);
|
|
86
|
+
input.focus();
|
|
87
|
+
input.select();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Show a danger confirmation (for destructive actions). */
|
|
91
|
+
export function showDangerConfirm(title, message) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const overlay = createOverlay();
|
|
94
|
+
const modal = createModal(overlay, title);
|
|
95
|
+
const msg = document.createElement("p");
|
|
96
|
+
msg.className = "bp-dialog-message";
|
|
97
|
+
msg.textContent = message;
|
|
98
|
+
modal.appendChild(msg);
|
|
99
|
+
addButtons(modal, [
|
|
100
|
+
{ label: "Cancel", onClick: () => { overlay.remove(); resolve(false); } },
|
|
101
|
+
{ label: "Delete", danger: true, onClick: () => { overlay.remove(); resolve(true); } },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Show a brief toast notification. */
|
|
106
|
+
export function showToast(message, durationMs = 3000) {
|
|
107
|
+
const existing = document.querySelector(".bp-toast");
|
|
108
|
+
if (existing)
|
|
109
|
+
existing.remove();
|
|
110
|
+
const toast = document.createElement("div");
|
|
111
|
+
toast.className = "bp-toast";
|
|
112
|
+
toast.textContent = message;
|
|
113
|
+
document.body.appendChild(toast);
|
|
114
|
+
setTimeout(() => toast.classList.add("bp-toast-visible"), 10);
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
toast.classList.remove("bp-toast-visible");
|
|
117
|
+
setTimeout(() => toast.remove(), 300);
|
|
118
|
+
}, durationMs);
|
|
119
|
+
}
|
package/dist/info-panel.d.ts
CHANGED
|
@@ -9,5 +9,9 @@ export interface EditCallbacks {
|
|
|
9
9
|
export declare function initInfoPanel(container: HTMLElement, callbacks?: EditCallbacks, onNavigateToNode?: (nodeId: string) => void, onFocus?: (nodeIds: string[]) => void): {
|
|
10
10
|
show(nodeIds: string[], data: LearningGraphData): void;
|
|
11
11
|
hide: () => void;
|
|
12
|
+
goBack: () => void;
|
|
13
|
+
goForward: () => void;
|
|
14
|
+
cycleConnection(direction: 1 | -1): string | null;
|
|
15
|
+
setFocusDisabled(disabled: boolean): void;
|
|
12
16
|
readonly visible: boolean;
|
|
13
17
|
};
|
package/dist/info-panel.js
CHANGED
|
@@ -20,6 +20,9 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
20
20
|
let navigatingHistory = false;
|
|
21
21
|
let lastData = null;
|
|
22
22
|
let currentNodeIds = [];
|
|
23
|
+
let focusDisabled = false;
|
|
24
|
+
let connectionNodeIds = []; // other-end node IDs for each connection
|
|
25
|
+
let activeConnectionIndex = -1;
|
|
23
26
|
function hide() {
|
|
24
27
|
panel.classList.add("hidden");
|
|
25
28
|
panel.classList.remove("info-panel-maximized");
|
|
@@ -43,19 +46,23 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
43
46
|
navigatingHistory = false;
|
|
44
47
|
}
|
|
45
48
|
function goBack() {
|
|
46
|
-
if (historyIndex <= 0 || !lastData
|
|
49
|
+
if (historyIndex <= 0 || !lastData)
|
|
47
50
|
return;
|
|
48
51
|
historyIndex--;
|
|
49
52
|
navigatingHistory = true;
|
|
50
|
-
|
|
53
|
+
const nodeId = history[historyIndex];
|
|
54
|
+
onNavigateToNode?.(nodeId);
|
|
55
|
+
showSingle(nodeId, lastData);
|
|
51
56
|
navigatingHistory = false;
|
|
52
57
|
}
|
|
53
58
|
function goForward() {
|
|
54
|
-
if (historyIndex >= history.length - 1 || !lastData
|
|
59
|
+
if (historyIndex >= history.length - 1 || !lastData)
|
|
55
60
|
return;
|
|
56
61
|
historyIndex++;
|
|
57
62
|
navigatingHistory = true;
|
|
58
|
-
|
|
63
|
+
const nodeId = history[historyIndex];
|
|
64
|
+
onNavigateToNode?.(nodeId);
|
|
65
|
+
showSingle(nodeId, lastData);
|
|
59
66
|
navigatingHistory = false;
|
|
60
67
|
}
|
|
61
68
|
function createToolbar() {
|
|
@@ -83,8 +90,12 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
83
90
|
focusBtn.className = "info-toolbar-btn info-focus-btn";
|
|
84
91
|
focusBtn.textContent = "\u25CE"; // bullseye
|
|
85
92
|
focusBtn.title = "Focus on neighborhood (F)";
|
|
93
|
+
focusBtn.disabled = focusDisabled;
|
|
94
|
+
if (focusDisabled)
|
|
95
|
+
focusBtn.style.opacity = "0.3";
|
|
86
96
|
focusBtn.addEventListener("click", () => {
|
|
87
|
-
|
|
97
|
+
if (!focusDisabled)
|
|
98
|
+
onFocus(currentNodeIds);
|
|
88
99
|
});
|
|
89
100
|
toolbar.appendChild(focusBtn);
|
|
90
101
|
}
|
|
@@ -114,12 +125,18 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
114
125
|
if (!node)
|
|
115
126
|
return;
|
|
116
127
|
const connectedEdges = data.edges.filter((e) => e.sourceId === nodeId || e.targetId === nodeId);
|
|
128
|
+
// Store connection targets for keyboard cycling
|
|
129
|
+
connectionNodeIds = connectedEdges.map((e) => e.sourceId === nodeId ? e.targetId : e.sourceId);
|
|
130
|
+
activeConnectionIndex = -1;
|
|
117
131
|
panel.innerHTML = "";
|
|
118
132
|
panel.classList.remove("hidden");
|
|
119
133
|
if (maximized)
|
|
120
134
|
panel.classList.add("info-panel-maximized");
|
|
135
|
+
// Pinned header area (toolbar + node identity)
|
|
136
|
+
const pinnedHeader = document.createElement("div");
|
|
137
|
+
pinnedHeader.className = "info-panel-header";
|
|
121
138
|
// Toolbar (back, forward, maximize, close)
|
|
122
|
-
|
|
139
|
+
pinnedHeader.appendChild(createToolbar());
|
|
123
140
|
// Header: type badge + label
|
|
124
141
|
const header = document.createElement("div");
|
|
125
142
|
header.className = "info-header";
|
|
@@ -173,7 +190,11 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
173
190
|
header.appendChild(typeBadge);
|
|
174
191
|
header.appendChild(label);
|
|
175
192
|
header.appendChild(nodeIdEl);
|
|
176
|
-
|
|
193
|
+
pinnedHeader.appendChild(header);
|
|
194
|
+
panel.appendChild(pinnedHeader);
|
|
195
|
+
// Scrollable body for properties, connections, timestamps
|
|
196
|
+
const body = document.createElement("div");
|
|
197
|
+
body.className = "info-panel-body";
|
|
177
198
|
// Properties section (editable)
|
|
178
199
|
const propKeys = Object.keys(node.properties);
|
|
179
200
|
const propSection = createSection("Properties");
|
|
@@ -258,7 +279,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
258
279
|
});
|
|
259
280
|
propSection.appendChild(addBtn);
|
|
260
281
|
}
|
|
261
|
-
|
|
282
|
+
body.appendChild(propSection);
|
|
262
283
|
// Connections section
|
|
263
284
|
if (connectedEdges.length > 0) {
|
|
264
285
|
const section = createSection(`Connections (${connectedEdges.length})`);
|
|
@@ -327,7 +348,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
327
348
|
list.appendChild(li);
|
|
328
349
|
}
|
|
329
350
|
section.appendChild(list);
|
|
330
|
-
|
|
351
|
+
body.appendChild(section);
|
|
331
352
|
}
|
|
332
353
|
// Timestamps
|
|
333
354
|
const tsSection = createSection("Timestamps");
|
|
@@ -346,7 +367,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
346
367
|
timestamps.appendChild(updatedDt);
|
|
347
368
|
timestamps.appendChild(updatedDd);
|
|
348
369
|
tsSection.appendChild(timestamps);
|
|
349
|
-
|
|
370
|
+
body.appendChild(tsSection);
|
|
350
371
|
// Delete node button
|
|
351
372
|
if (callbacks) {
|
|
352
373
|
const deleteSection = document.createElement("div");
|
|
@@ -359,8 +380,9 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
359
380
|
hide();
|
|
360
381
|
});
|
|
361
382
|
deleteSection.appendChild(deleteBtn);
|
|
362
|
-
|
|
383
|
+
body.appendChild(deleteSection);
|
|
363
384
|
}
|
|
385
|
+
panel.appendChild(body);
|
|
364
386
|
}
|
|
365
387
|
function showMulti(nodeIds, data) {
|
|
366
388
|
const selectedSet = new Set(nodeIds);
|
|
@@ -521,6 +543,39 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
521
543
|
}
|
|
522
544
|
},
|
|
523
545
|
hide,
|
|
546
|
+
goBack,
|
|
547
|
+
goForward,
|
|
548
|
+
cycleConnection(direction) {
|
|
549
|
+
if (connectionNodeIds.length === 0)
|
|
550
|
+
return null;
|
|
551
|
+
if (activeConnectionIndex === -1) {
|
|
552
|
+
activeConnectionIndex = direction === 1 ? 0 : connectionNodeIds.length - 1;
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
activeConnectionIndex += direction;
|
|
556
|
+
if (activeConnectionIndex >= connectionNodeIds.length)
|
|
557
|
+
activeConnectionIndex = 0;
|
|
558
|
+
if (activeConnectionIndex < 0)
|
|
559
|
+
activeConnectionIndex = connectionNodeIds.length - 1;
|
|
560
|
+
}
|
|
561
|
+
// Highlight active row in the panel
|
|
562
|
+
const items = panel.querySelectorAll(".info-connection");
|
|
563
|
+
items.forEach((el, i) => {
|
|
564
|
+
el.classList.toggle("info-connection-active", i === activeConnectionIndex);
|
|
565
|
+
});
|
|
566
|
+
if (activeConnectionIndex >= 0 && items[activeConnectionIndex]) {
|
|
567
|
+
items[activeConnectionIndex].scrollIntoView({ block: "nearest" });
|
|
568
|
+
}
|
|
569
|
+
return connectionNodeIds[activeConnectionIndex] ?? null;
|
|
570
|
+
},
|
|
571
|
+
setFocusDisabled(disabled) {
|
|
572
|
+
focusDisabled = disabled;
|
|
573
|
+
const btn = panel.querySelector(".info-focus-btn");
|
|
574
|
+
if (btn) {
|
|
575
|
+
btn.disabled = disabled;
|
|
576
|
+
btn.style.opacity = disabled ? "0.3" : "";
|
|
577
|
+
}
|
|
578
|
+
},
|
|
524
579
|
get visible() {
|
|
525
580
|
return !panel.classList.contains("hidden");
|
|
526
581
|
},
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type KeybindingAction = "search" | "searchAlt" | "undo" | "redo" | "help" | "escape" | "focus" | "toggleEdges" | "center" | "nextNode" | "prevNode" | "nextConnection" | "prevConnection" | "historyBack" | "historyForward" | "hopsIncrease" | "hopsDecrease" | "panLeft" | "panDown" | "panUp" | "panRight" | "panFastLeft" | "zoomOut" | "zoomIn" | "panFastRight" | "spacingDecrease" | "spacingIncrease" | "clusteringDecrease" | "clusteringIncrease" | "toggleSidebar";
|
|
2
|
+
export type KeybindingMap = Record<KeybindingAction, string>;
|
|
3
|
+
/** Parse a binding string like "ctrl+shift+z" and check if it matches a KeyboardEvent. */
|
|
4
|
+
export declare function matchKey(e: KeyboardEvent, binding: string): boolean;
|
|
5
|
+
/** Build a reverse map: for each action, store its binding string. Used by the help modal. */
|
|
6
|
+
export declare function actionDescriptions(): Record<KeybindingAction, string>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Parse a binding string like "ctrl+shift+z" and check if it matches a KeyboardEvent. */
|
|
2
|
+
export function matchKey(e, binding) {
|
|
3
|
+
const parts = binding.split("+");
|
|
4
|
+
const key = parts.pop();
|
|
5
|
+
const modifiers = parts.map((p) => p.toLowerCase());
|
|
6
|
+
const needCtrl = modifiers.includes("ctrl") || modifiers.includes("cmd") || modifiers.includes("meta");
|
|
7
|
+
const explicitShift = modifiers.includes("shift");
|
|
8
|
+
const needAlt = modifiers.includes("alt");
|
|
9
|
+
// Ctrl/meta check
|
|
10
|
+
if (needCtrl !== (e.ctrlKey || e.metaKey))
|
|
11
|
+
return false;
|
|
12
|
+
if (!needCtrl && (e.ctrlKey || e.metaKey))
|
|
13
|
+
return false;
|
|
14
|
+
// Only enforce shift when explicitly in the binding (e.g. "ctrl+shift+z").
|
|
15
|
+
// Plain chars like "K", ">", "?" implicitly require shift via their character.
|
|
16
|
+
if (explicitShift && !e.shiftKey)
|
|
17
|
+
return false;
|
|
18
|
+
// Alt check
|
|
19
|
+
if (needAlt !== e.altKey)
|
|
20
|
+
return false;
|
|
21
|
+
// Named keys
|
|
22
|
+
if (key.toLowerCase() === "escape")
|
|
23
|
+
return e.key === "Escape";
|
|
24
|
+
if (key.toLowerCase() === "tab")
|
|
25
|
+
return e.key === "Tab";
|
|
26
|
+
// For modified bindings (ctrl+z, ctrl+shift+z), compare case-insensitively
|
|
27
|
+
// because browsers vary on e.key casing when modifiers are held.
|
|
28
|
+
if (modifiers.length > 0)
|
|
29
|
+
return e.key.toLowerCase() === key.toLowerCase();
|
|
30
|
+
// For plain keys, compare exactly — "k" vs "K" distinguishes shift state.
|
|
31
|
+
return e.key === key;
|
|
32
|
+
}
|
|
33
|
+
/** Build a reverse map: for each action, store its binding string. Used by the help modal. */
|
|
34
|
+
export function actionDescriptions() {
|
|
35
|
+
return {
|
|
36
|
+
search: "Focus search",
|
|
37
|
+
searchAlt: "Focus search (alt)",
|
|
38
|
+
undo: "Undo",
|
|
39
|
+
redo: "Redo",
|
|
40
|
+
help: "Toggle help",
|
|
41
|
+
escape: "Exit focus / close panel",
|
|
42
|
+
focus: "Focus on selected / exit focus",
|
|
43
|
+
toggleEdges: "Toggle edges on/off",
|
|
44
|
+
center: "Center view on graph",
|
|
45
|
+
nextNode: "Next node in view",
|
|
46
|
+
prevNode: "Previous node in view",
|
|
47
|
+
nextConnection: "Next connection",
|
|
48
|
+
prevConnection: "Previous connection",
|
|
49
|
+
historyBack: "Node history back",
|
|
50
|
+
historyForward: "Node history forward",
|
|
51
|
+
hopsIncrease: "Increase hops",
|
|
52
|
+
hopsDecrease: "Decrease hops",
|
|
53
|
+
panLeft: "Pan left",
|
|
54
|
+
panDown: "Pan down",
|
|
55
|
+
panUp: "Pan up",
|
|
56
|
+
panRight: "Pan right",
|
|
57
|
+
panFastLeft: "Pan fast left",
|
|
58
|
+
zoomOut: "Zoom out",
|
|
59
|
+
zoomIn: "Zoom in",
|
|
60
|
+
panFastRight: "Pan fast right",
|
|
61
|
+
spacingDecrease: "Decrease spacing",
|
|
62
|
+
spacingIncrease: "Increase spacing",
|
|
63
|
+
clusteringDecrease: "Decrease clustering",
|
|
64
|
+
clusteringIncrease: "Increase clustering",
|
|
65
|
+
toggleSidebar: "Toggle sidebar",
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/layout.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface LayoutParams {
|
|
|
25
25
|
export declare const DEFAULT_LAYOUT_PARAMS: LayoutParams;
|
|
26
26
|
export declare function setLayoutParams(p: Partial<LayoutParams>): void;
|
|
27
27
|
export declare function getLayoutParams(): LayoutParams;
|
|
28
|
+
/** Compute sensible default layout params based on graph size. */
|
|
29
|
+
export declare function autoLayoutParams(nodeCount: number): LayoutParams;
|
|
28
30
|
/** Extract the N-hop neighborhood of seed nodes as a new subgraph. */
|
|
29
31
|
export declare function extractSubgraph(data: LearningGraphData, seedIds: string[], hops: number): LearningGraphData;
|
|
30
32
|
/** Create a layout state from ontology data. Nodes start grouped by type. */
|
package/dist/layout.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const DEFAULT_LAYOUT_PARAMS = {
|
|
2
|
-
clusterStrength: 0.
|
|
3
|
-
spacing: 1,
|
|
2
|
+
clusterStrength: 0.08,
|
|
3
|
+
spacing: 1.5,
|
|
4
4
|
};
|
|
5
|
-
const REPULSION =
|
|
6
|
-
const CROSS_TYPE_REPULSION_BASE =
|
|
7
|
-
const ATTRACTION = 0.
|
|
8
|
-
const REST_LENGTH_SAME_BASE =
|
|
9
|
-
const REST_LENGTH_CROSS_BASE =
|
|
5
|
+
const REPULSION = 6000;
|
|
6
|
+
const CROSS_TYPE_REPULSION_BASE = 12000;
|
|
7
|
+
const ATTRACTION = 0.004;
|
|
8
|
+
const REST_LENGTH_SAME_BASE = 140;
|
|
9
|
+
const REST_LENGTH_CROSS_BASE = 350;
|
|
10
10
|
const DAMPING = 0.9;
|
|
11
11
|
const CENTER_GRAVITY = 0.01;
|
|
12
12
|
const MIN_DISTANCE = 30;
|
|
@@ -22,6 +22,16 @@ export function setLayoutParams(p) {
|
|
|
22
22
|
export function getLayoutParams() {
|
|
23
23
|
return { ...params };
|
|
24
24
|
}
|
|
25
|
+
/** Compute sensible default layout params based on graph size. */
|
|
26
|
+
export function autoLayoutParams(nodeCount) {
|
|
27
|
+
if (nodeCount <= 30)
|
|
28
|
+
return { ...DEFAULT_LAYOUT_PARAMS };
|
|
29
|
+
const scale = Math.log2(nodeCount / 30);
|
|
30
|
+
return {
|
|
31
|
+
clusterStrength: Math.min(0.5, 0.08 + 0.06 * scale),
|
|
32
|
+
spacing: Math.min(15, 1.5 + 1.2 * scale),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
25
35
|
/** Extract a display label from a node — first string property value, fallback to id. */
|
|
26
36
|
function nodeLabel(properties, id) {
|
|
27
37
|
for (const value of Object.values(properties)) {
|
|
@@ -61,7 +71,7 @@ export function createLayout(data) {
|
|
|
61
71
|
const nodeMap = new Map();
|
|
62
72
|
// Group nodes by type for initial placement
|
|
63
73
|
const types = [...new Set(data.nodes.map((n) => n.type))];
|
|
64
|
-
const typeRadius = Math.sqrt(types.length) * REST_LENGTH_CROSS_BASE * 0.6;
|
|
74
|
+
const typeRadius = Math.sqrt(types.length) * REST_LENGTH_CROSS_BASE * 0.6 * Math.max(1, params.spacing);
|
|
65
75
|
const typeCounters = new Map();
|
|
66
76
|
const typeSizes = new Map();
|
|
67
77
|
for (const n of data.nodes) {
|