@matdata/yasgui-graph-plugin 1.4.2 → 1.6.0

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.
@@ -41,14 +41,18 @@ function truncateLabel(text, maxLength = 50) {
41
41
  }
42
42
 
43
43
  // src/networkConfig.ts
44
- function getDefaultNetworkOptions(themeColors) {
44
+ function getDefaultNetworkOptions(themeColors, settings) {
45
+ const curved = !settings || settings.edgeStyle !== "straight";
46
+ const nodeSizeMap = { small: 6, medium: 10, large: 16 };
47
+ const nodeSize = (settings == null ? void 0 : settings.nodeSize) ? nodeSizeMap[settings.nodeSize] : 10;
48
+ const showNodeLabels = (settings == null ? void 0 : settings.showNodeLabels) !== false;
45
49
  return {
46
50
  // Configure canvas background color based on theme
47
51
  configure: {
48
52
  enabled: false
49
53
  },
50
54
  physics: {
51
- enabled: true,
55
+ enabled: (settings == null ? void 0 : settings.physicsEnabled) !== false,
52
56
  stabilization: {
53
57
  enabled: true,
54
58
  iterations: 200,
@@ -68,14 +72,16 @@ function getDefaultNetworkOptions(themeColors) {
68
72
  dragView: true,
69
73
  zoomView: true,
70
74
  hover: true,
71
- tooltipDelay: 300
75
+ tooltipDelay: 300,
72
76
  // 300ms hover delay per spec
77
+ hideEdgesOnDrag: false,
78
+ hideEdgesOnZoom: false
73
79
  },
74
80
  nodes: {
75
81
  shape: "dot",
76
- size: 10,
82
+ size: nodeSize,
77
83
  font: {
78
- size: 12,
84
+ size: showNodeLabels ? 12 : 0,
79
85
  color: themeColors.text
80
86
  },
81
87
  borderWidth: 1,
@@ -90,7 +96,7 @@ function getDefaultNetworkOptions(themeColors) {
90
96
  }
91
97
  },
92
98
  smooth: {
93
- enabled: true,
99
+ enabled: curved,
94
100
  type: "dynamic",
95
101
  roundness: 0.5
96
102
  },
@@ -110,6 +116,40 @@ function getDefaultNetworkOptions(themeColors) {
110
116
  }
111
117
 
112
118
  // src/parsers.ts
119
+ async function parseBackgroundQueryResponse(response) {
120
+ var _a, _b;
121
+ if (!response) return [];
122
+ try {
123
+ let data2;
124
+ if (typeof response.json === "function") {
125
+ data2 = await response.json();
126
+ } else if (typeof response.data === "string") {
127
+ data2 = JSON.parse(response.data);
128
+ } else if (typeof response === "object") {
129
+ data2 = response;
130
+ } else {
131
+ return [];
132
+ }
133
+ const bindings = (_b = (_a = data2 == null ? void 0 : data2.results) == null ? void 0 : _a.bindings) != null ? _b : [];
134
+ const triples = [];
135
+ for (const binding of bindings) {
136
+ if (!binding.subject || !binding.predicate || !binding.object || typeof binding.subject.value !== "string" || typeof binding.predicate.value !== "string" || typeof binding.object.value !== "string") continue;
137
+ triples.push({
138
+ subject: binding.subject.value,
139
+ predicate: binding.predicate.value,
140
+ object: {
141
+ value: binding.object.value,
142
+ type: binding.object.type || "uri",
143
+ datatype: binding.object.datatype,
144
+ lang: binding.object["xml:lang"]
145
+ }
146
+ });
147
+ }
148
+ return triples;
149
+ } catch (e) {
150
+ return [];
151
+ }
152
+ }
113
153
  function parseConstructResults(yasrResults) {
114
154
  const triples = [];
115
155
  if (!yasrResults || !yasrResults.getBindings) {
@@ -152,10 +192,129 @@ function getNodeColor(node, triples, themeColors) {
152
192
  return themeColors.uri;
153
193
  }
154
194
 
195
+ // src/predicateIcons.ts
196
+ var PREDICATE_ICONS = {
197
+ // RDF core
198
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": "a",
199
+ // Turtle's "a" shorthand
200
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#value": "val",
201
+ // RDFS
202
+ "http://www.w3.org/2000/01/rdf-schema#label": "lbl",
203
+ "http://www.w3.org/2000/01/rdf-schema#comment": "cmt",
204
+ "http://www.w3.org/2000/01/rdf-schema#subClassOf": "\u2282",
205
+ // ⊂
206
+ "http://www.w3.org/2000/01/rdf-schema#subPropertyOf": "\u2286",
207
+ // ⊆
208
+ "http://www.w3.org/2000/01/rdf-schema#domain": "dom",
209
+ "http://www.w3.org/2000/01/rdf-schema#range": "rng",
210
+ "http://www.w3.org/2000/01/rdf-schema#seeAlso": "see",
211
+ "http://www.w3.org/2000/01/rdf-schema#isDefinedBy": "idb",
212
+ // OWL
213
+ "http://www.w3.org/2002/07/owl#sameAs": "\u2261",
214
+ // ≡
215
+ "http://www.w3.org/2002/07/owl#equivalentClass": "\u2245",
216
+ // ≅
217
+ "http://www.w3.org/2002/07/owl#inverseOf": "\u21C4",
218
+ // ⇄
219
+ "http://www.w3.org/2002/07/owl#disjointWith": "\u2260",
220
+ // ≠
221
+ // SKOS
222
+ "http://www.w3.org/2004/02/skos/core#prefLabel": "\u2605",
223
+ // ★
224
+ "http://www.w3.org/2004/02/skos/core#altLabel": "\u2606",
225
+ // ☆
226
+ "http://www.w3.org/2004/02/skos/core#definition": "def",
227
+ "http://www.w3.org/2004/02/skos/core#broader": "\u2191",
228
+ // ↑
229
+ "http://www.w3.org/2004/02/skos/core#narrower": "\u2193",
230
+ // ↓
231
+ "http://www.w3.org/2004/02/skos/core#related": "\u2194",
232
+ // ↔
233
+ "http://www.w3.org/2004/02/skos/core#note": "note",
234
+ "http://www.w3.org/2004/02/skos/core#exactMatch": "\u2261",
235
+ // ≡
236
+ "http://www.w3.org/2004/02/skos/core#closeMatch": "\u2248",
237
+ // ≈
238
+ // Dublin Core Terms
239
+ "http://purl.org/dc/terms/title": "ttl",
240
+ "http://purl.org/dc/terms/description": "dsc",
241
+ "http://purl.org/dc/terms/created": "crt",
242
+ "http://purl.org/dc/terms/modified": "mod",
243
+ "http://purl.org/dc/terms/creator": "by",
244
+ "http://purl.org/dc/terms/subject": "sbj",
245
+ // FOAF
246
+ "http://xmlns.com/foaf/0.1/name": "nm",
247
+ "http://xmlns.com/foaf/0.1/knows": "\u27F7",
248
+ // ⟷
249
+ "http://xmlns.com/foaf/0.1/member": "mbr",
250
+ // Schema.org
251
+ "http://schema.org/name": "nm",
252
+ "http://schema.org/description": "dsc"
253
+ };
254
+ function getPredicateIcon(predicateUri) {
255
+ return PREDICATE_ICONS[predicateUri];
256
+ }
257
+
155
258
  // src/transformers.ts
156
- function createNodeMap(triples, prefixMap, themeColors) {
259
+ var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
260
+ var SCHEMA_IMAGE = "https://schema.org/image";
261
+ var SCHEMA_ICON = "https://schema.org/icon";
262
+ var RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
263
+ var RDFS_SUBCLASSOF = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
264
+ var SUPPRESSED_PREDICATES = /* @__PURE__ */ new Set([SCHEMA_IMAGE, SCHEMA_ICON, RDFS_LABEL]);
265
+ function appendTooltipRows(title, rows) {
266
+ const closingTag = "</div>";
267
+ const idx = title.lastIndexOf(closingTag);
268
+ if (idx === -1) return title + rows;
269
+ return title.slice(0, idx) + rows + closingTag;
270
+ }
271
+ function buildVisualTooltipRow(key, value) {
272
+ return `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">${escapeHtml(key)}</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
273
+ }
274
+ function escapeHtml(str) {
275
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
276
+ }
277
+ function createCompactNodeTooltipHTML(uri, triples, prefixMap) {
278
+ const isBlankNode = uri.startsWith("_:");
279
+ let rows = `<div class="yasgui-tooltip-type">${isBlankNode ? "Blank Node" : "URI"}</div>`;
280
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">${isBlankNode ? "Identifier" : "Full URI"}</span><span class="yasgui-tooltip-val">${escapeHtml(uri)}</span></div>`;
281
+ triples.filter((t) => t.subject === uri && t.predicate === RDF_TYPE).forEach((t) => {
282
+ const typeLabel = applyPrefix(t.object.value, prefixMap);
283
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">rdf:type</span><span class="yasgui-tooltip-val">${escapeHtml(typeLabel)}</span></div>`;
284
+ });
285
+ triples.filter((t) => t.subject === uri && t.object.type === "literal").forEach((t) => {
286
+ const predLabel = applyPrefix(t.predicate, prefixMap);
287
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">${escapeHtml(predLabel)}</span><span class="yasgui-tooltip-val">${escapeHtml(t.object.value)}</span></div>`;
288
+ });
289
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
290
+ }
291
+ function createNodeTooltipHTML(nodeType, value, datatype, lang, prefixMap) {
292
+ const typeLabel = nodeType === "uri" ? "URI" : nodeType === "literal" ? "Literal" : "Blank Node";
293
+ let rows = `<div class="yasgui-tooltip-type">${typeLabel}</div>`;
294
+ if (nodeType === "literal") {
295
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Value</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
296
+ if (datatype) {
297
+ const dtLabel = prefixMap ? applyPrefix(datatype, prefixMap) : datatype;
298
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Datatype</span><span class="yasgui-tooltip-val">${escapeHtml(dtLabel)}</span></div>`;
299
+ }
300
+ if (lang) {
301
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Language</span><span class="yasgui-tooltip-val">${escapeHtml(lang)}</span></div>`;
302
+ }
303
+ } else if (nodeType === "uri") {
304
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Full URI</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
305
+ } else if (nodeType === "bnode") {
306
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Identifier</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
307
+ }
308
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
309
+ }
310
+ function createEdgeTooltipHTML(predicateUri) {
311
+ const rows = `<div class="yasgui-tooltip-type">Predicate</div><div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Full URI</span><span class="yasgui-tooltip-val">${escapeHtml(predicateUri)}</span></div>`;
312
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
313
+ }
314
+ function createNodeMap(triples, prefixMap, themeColors, settings) {
157
315
  const nodeMap = /* @__PURE__ */ new Map();
158
316
  let nodeId = 1;
317
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
159
318
  triples.forEach((triple) => {
160
319
  if (!nodeMap.has(triple.subject)) {
161
320
  const isBlankNode = triple.subject.startsWith("_:");
@@ -167,11 +326,18 @@ function createNodeMap(triples, prefixMap, themeColors) {
167
326
  color: getNodeColor({ uri: triple.subject, type: "uri" }, triples, themeColors),
168
327
  type: "uri",
169
328
  fullValue: triple.subject,
170
- title: isBlankNode ? triple.subject : applyPrefix(triple.subject, prefixMap)
329
+ size: 10 * sizeMultiplier,
330
+ title: createNodeTooltipHTML(
331
+ isBlankNode ? "bnode" : "uri",
332
+ triple.subject,
333
+ void 0,
334
+ void 0,
335
+ prefixMap
336
+ )
171
337
  });
172
338
  }
173
339
  const objValue = triple.object.value;
174
- if (!nodeMap.has(objValue)) {
340
+ if (!nodeMap.has(objValue) && !SUPPRESSED_PREDICATES.has(triple.predicate)) {
175
341
  const isLiteral = triple.object.type === "literal";
176
342
  const isBlankNode = !isLiteral && objValue.startsWith("_:");
177
343
  let label;
@@ -180,15 +346,21 @@ function createNodeMap(triples, prefixMap, themeColors) {
180
346
  if (isLiteral) {
181
347
  label = truncateLabel(objValue);
182
348
  fullValue = objValue;
183
- title = triple.object.datatype ? `"${objValue}"^^${applyPrefix(triple.object.datatype, prefixMap)}` : `"${objValue}"`;
349
+ title = createNodeTooltipHTML(
350
+ "literal",
351
+ objValue,
352
+ triple.object.datatype,
353
+ triple.object.lang,
354
+ prefixMap
355
+ );
184
356
  } else if (isBlankNode) {
185
357
  label = objValue;
186
358
  fullValue = objValue;
187
- title = objValue;
359
+ title = createNodeTooltipHTML("bnode", objValue);
188
360
  } else {
189
361
  label = truncateLabel(applyPrefix(objValue, prefixMap));
190
362
  fullValue = objValue;
191
- title = applyPrefix(objValue, prefixMap);
363
+ title = createNodeTooltipHTML("uri", objValue, void 0, void 0, prefixMap);
192
364
  }
193
365
  nodeMap.set(objValue, {
194
366
  id: nodeId++,
@@ -201,39 +373,157 @@ function createNodeMap(triples, prefixMap, themeColors) {
201
373
  ),
202
374
  type: isLiteral ? "literal" : "uri",
203
375
  fullValue,
376
+ size: (isLiteral ? 5 : 10) * sizeMultiplier,
204
377
  title
205
378
  });
206
379
  }
207
380
  });
208
381
  return nodeMap;
209
382
  }
210
- function createEdgesArray(triples, nodeMap, prefixMap) {
383
+ function createEdgesArray(triples, nodeMap, prefixMap, settings) {
211
384
  const edges = [];
212
385
  const edgeSet = /* @__PURE__ */ new Set();
213
386
  triples.forEach((triple) => {
387
+ var _a, _b;
388
+ if (SUPPRESSED_PREDICATES.has(triple.predicate)) return;
214
389
  const fromNode = nodeMap.get(triple.subject);
215
390
  const toNode = nodeMap.get(triple.object.value);
216
391
  if (!fromNode || !toNode) return;
217
392
  const edgeKey = `${fromNode.id}-${triple.predicate}-${toNode.id}`;
218
393
  if (!edgeSet.has(edgeKey)) {
219
394
  edgeSet.add(edgeKey);
395
+ let edgeLabel;
396
+ const predicateDisplay = (_a = settings == null ? void 0 : settings.predicateDisplay) != null ? _a : "label";
397
+ if (predicateDisplay === "none") {
398
+ edgeLabel = "";
399
+ } else if (predicateDisplay === "icon") {
400
+ edgeLabel = (_b = getPredicateIcon(triple.predicate)) != null ? _b : truncateLabel(applyPrefix(triple.predicate, prefixMap));
401
+ } else {
402
+ edgeLabel = truncateLabel(applyPrefix(triple.predicate, prefixMap));
403
+ }
220
404
  edges.push({
221
405
  id: `edge_${fromNode.id}_${toNode.id}_${edges.length}`,
222
406
  from: fromNode.id,
223
407
  to: toNode.id,
224
- label: truncateLabel(applyPrefix(triple.predicate, prefixMap)),
408
+ label: edgeLabel,
225
409
  predicate: triple.predicate,
226
- title: applyPrefix(triple.predicate, prefixMap),
410
+ title: createEdgeTooltipHTML(triple.predicate),
227
411
  arrows: "to"
228
412
  });
229
413
  }
230
414
  });
231
415
  return edges;
232
416
  }
233
- function triplesToGraph(triples, prefixMap, themeColors) {
234
- const nodeMap = createNodeMap(triples, prefixMap, themeColors);
235
- const edges = createEdgesArray(triples, nodeMap, prefixMap);
236
- const nodes = Array.from(nodeMap.values());
417
+ function isNodeVisible(node, triples, settings) {
418
+ if (node.uri && node.uri.startsWith("_:")) {
419
+ return true;
420
+ }
421
+ if (!settings.compactMode) {
422
+ return true;
423
+ }
424
+ if (node.type === "literal") {
425
+ return false;
426
+ }
427
+ const isClass = triples.some(
428
+ (t) => t.predicate === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" && t.object.value === node.uri
429
+ );
430
+ if (isClass) {
431
+ return false;
432
+ }
433
+ return true;
434
+ }
435
+ function triplesToGraph(triples, prefixMap, themeColors, settings) {
436
+ const nodeMap = createNodeMap(triples, prefixMap, themeColors, settings);
437
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
438
+ const tripleIndex = /* @__PURE__ */ new Map();
439
+ for (const t of triples) {
440
+ if (!tripleIndex.has(t.subject)) tripleIndex.set(t.subject, /* @__PURE__ */ new Map());
441
+ const sp = tripleIndex.get(t.subject);
442
+ if (!sp.has(t.predicate)) sp.set(t.predicate, []);
443
+ sp.get(t.predicate).push(t.object.value);
444
+ }
445
+ function getNodeVisualIdx(uri) {
446
+ const sp = tripleIndex.get(uri);
447
+ if (!sp) return {};
448
+ const icons = sp.get(SCHEMA_ICON);
449
+ if (icons == null ? void 0 : icons.length) return { icon: icons[0] };
450
+ const images = sp.get(SCHEMA_IMAGE);
451
+ if (images == null ? void 0 : images.length) return { image: images[0] };
452
+ return {};
453
+ }
454
+ function resolveCompactVisualIdx(uri) {
455
+ var _a, _b, _c, _d;
456
+ const own = getNodeVisualIdx(uri);
457
+ if (own.icon || own.image) return own;
458
+ const typeUris = (_b = (_a = tripleIndex.get(uri)) == null ? void 0 : _a.get(RDF_TYPE)) != null ? _b : [];
459
+ for (const typeUri of typeUris) {
460
+ const cv = getNodeVisualIdx(typeUri);
461
+ if (cv.icon || cv.image) return cv;
462
+ }
463
+ for (const typeUri of typeUris) {
464
+ const superUris = (_d = (_c = tripleIndex.get(typeUri)) == null ? void 0 : _c.get(RDFS_SUBCLASSOF)) != null ? _d : [];
465
+ for (const superUri of superUris) {
466
+ const sv = getNodeVisualIdx(superUri);
467
+ if (sv.icon || sv.image) return sv;
468
+ }
469
+ }
470
+ return {};
471
+ }
472
+ if (settings == null ? void 0 : settings.compactMode) {
473
+ const subjects = new Set(triples.map((t) => t.subject));
474
+ subjects.forEach((subjectUri) => {
475
+ const node = nodeMap.get(subjectUri);
476
+ if (node) {
477
+ node.title = createCompactNodeTooltipHTML(subjectUri, triples, prefixMap);
478
+ }
479
+ });
480
+ }
481
+ nodeMap.forEach((node) => {
482
+ var _a, _b;
483
+ if (!node.uri || node.type === "literal") return;
484
+ const visual = (settings == null ? void 0 : settings.compactMode) ? resolveCompactVisualIdx(node.uri) : getNodeVisualIdx(node.uri);
485
+ const rdfsLabel = (_b = (_a = tripleIndex.get(node.uri)) == null ? void 0 : _a.get(RDFS_LABEL)) == null ? void 0 : _b[0];
486
+ if (visual.icon) {
487
+ node.shape = "text";
488
+ node.label = rdfsLabel ? `${visual.icon}
489
+ ${rdfsLabel}` : visual.icon;
490
+ node.font = { size: 14 * sizeMultiplier };
491
+ if (!(settings == null ? void 0 : settings.compactMode)) {
492
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Icon", visual.icon));
493
+ }
494
+ } else if (visual.image) {
495
+ let imageAllowed = false;
496
+ try {
497
+ const parsed = new URL(visual.image);
498
+ imageAllowed = parsed.protocol === "http:" || parsed.protocol === "https:";
499
+ } catch (e) {
500
+ }
501
+ if (imageAllowed) {
502
+ node.shape = "circularImage";
503
+ node.image = visual.image;
504
+ }
505
+ if (rdfsLabel) {
506
+ node.label = rdfsLabel;
507
+ node.font = { size: 14 * sizeMultiplier };
508
+ }
509
+ if (!(settings == null ? void 0 : settings.compactMode)) {
510
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Image", visual.image));
511
+ }
512
+ } else if (rdfsLabel) {
513
+ node.label = rdfsLabel;
514
+ }
515
+ if (rdfsLabel && !(settings == null ? void 0 : settings.compactMode)) {
516
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("rdfs:label", rdfsLabel));
517
+ }
518
+ });
519
+ const visibleNodeIds = /* @__PURE__ */ new Set();
520
+ nodeMap.forEach((node) => {
521
+ if (!settings || isNodeVisible(node, triples, settings)) {
522
+ visibleNodeIds.add(node.id);
523
+ }
524
+ });
525
+ const edges = createEdgesArray(triples, nodeMap, prefixMap, settings).filter((e) => visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to));
526
+ const nodes = Array.from(nodeMap.values()).filter((n) => visibleNodeIds.has(n.id));
237
527
  return { nodes, edges };
238
528
  }
239
529
 
@@ -29704,9 +29994,44 @@ function watchThemeChanges(callback) {
29704
29994
  return observer;
29705
29995
  }
29706
29996
 
29997
+ // src/settings.ts
29998
+ var DEFAULT_SETTINGS = {
29999
+ edgeStyle: "curved",
30000
+ compactMode: false,
30001
+ predicateDisplay: "icon",
30002
+ showNodeLabels: true,
30003
+ physicsEnabled: true,
30004
+ nodeSize: "medium"
30005
+ };
30006
+ var STORAGE_KEY = "yasgui-graph-plugin-settings";
30007
+ function loadSettings() {
30008
+ try {
30009
+ const stored = localStorage.getItem(STORAGE_KEY);
30010
+ if (stored) {
30011
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
30012
+ }
30013
+ } catch (e) {
30014
+ }
30015
+ return { ...DEFAULT_SETTINGS };
30016
+ }
30017
+ function saveSettings(settings) {
30018
+ try {
30019
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
30020
+ } catch (e) {
30021
+ }
30022
+ }
30023
+
29707
30024
  // src/GraphPlugin.ts
30025
+ var LOADING_BORDER_WIDTH = 4;
30026
+ var LOADING_BORDER_COLOR = "#ffa500";
30027
+ var EXPANDED_BORDER_WIDTH = 3;
30028
+ var DEFAULT_BORDER_WIDTH = 2;
29708
30029
  var GraphPlugin = class {
29709
30030
  constructor(yasr) {
30031
+ this.settingsPanelOpen = false;
30032
+ this.clickOutsideHandler = null;
30033
+ this.expansionAbortController = null;
30034
+ this.uriToNodeId = /* @__PURE__ */ new Map();
29710
30035
  this.yasr = yasr;
29711
30036
  this.network = null;
29712
30037
  this.currentTheme = getCurrentTheme();
@@ -29716,6 +30041,7 @@ var GraphPlugin = class {
29716
30041
  this.edgesDataSet = null;
29717
30042
  this.triples = null;
29718
30043
  this.prefixMap = null;
30044
+ this.settings = loadSettings();
29719
30045
  }
29720
30046
  /**
29721
30047
  * Plugin priority (higher = shown first in tabs)
@@ -29729,6 +30055,12 @@ var GraphPlugin = class {
29729
30055
  static get label() {
29730
30056
  return "Graph";
29731
30057
  }
30058
+ /**
30059
+ * Help/documentation URL
30060
+ */
30061
+ static get helpReference() {
30062
+ return "https://yasgui-doc.matdata.eu/docs/user-guide#graph-plugin";
30063
+ }
29732
30064
  /**
29733
30065
  * Check if plugin can handle the current results
29734
30066
  * @returns True if results are from CONSTRUCT or DESCRIBE query
@@ -29749,6 +30081,13 @@ var GraphPlugin = class {
29749
30081
  * Render the graph visualization
29750
30082
  */
29751
30083
  draw() {
30084
+ const wasPanelOpen = this.settingsPanelOpen;
30085
+ if (this.expansionAbortController) {
30086
+ this.expansionAbortController.abort();
30087
+ this.expansionAbortController = null;
30088
+ }
30089
+ this.expandedNodes = /* @__PURE__ */ new Set();
30090
+ this.uriToNodeId = /* @__PURE__ */ new Map();
29752
30091
  this.yasr.resultsEl.innerHTML = "";
29753
30092
  try {
29754
30093
  this.triples = parseConstructResults(this.yasr.results);
@@ -29765,24 +30104,52 @@ var GraphPlugin = class {
29765
30104
  this.prefixMap = extractPrefixes(this.yasr);
29766
30105
  this.currentTheme = getCurrentTheme();
29767
30106
  const themeColors = getThemeNodeColors(this.currentTheme);
29768
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30107
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29769
30108
  const container = document.createElement("div");
29770
30109
  container.className = "yasgui-graph-plugin-container";
29771
30110
  container.id = "yasgui-graph-plugin-container";
29772
30111
  this.yasr.resultsEl.appendChild(container);
29773
30112
  this.nodesDataSet = new DataSet(nodes);
29774
30113
  this.edgesDataSet = new DataSet(edges);
29775
- const options = getDefaultNetworkOptions(themeColors);
30114
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29776
30115
  this.network = new Network(
29777
30116
  container,
29778
30117
  { nodes: this.nodesDataSet, edges: this.edgesDataSet },
29779
30118
  options
29780
30119
  );
30120
+ this.setupHtmlTooltips(container);
29781
30121
  this.applyCanvasBackground(themeColors.background);
29782
- this.network.on("stabilizationIterationsDone", () => {
29783
- this.network.setOptions({ physics: { enabled: true } });
29784
- this.network.fit({ maxZoomLevel: 3 });
29785
- this.setupContainerResize(container);
30122
+ this.setupContainerResize(container);
30123
+ if (this.settings.physicsEnabled) {
30124
+ this.network.on("stabilizationIterationsDone", () => {
30125
+ this.network.setOptions({ physics: { enabled: true } });
30126
+ this.network.fit({ maxZoomLevel: 3 });
30127
+ });
30128
+ } else {
30129
+ setTimeout(() => {
30130
+ if (this.network) {
30131
+ this.network.fit({ maxZoomLevel: 3 });
30132
+ }
30133
+ }, 100);
30134
+ }
30135
+ this.network.on("dragEnd", (params) => {
30136
+ if (params.nodes.length > 0) {
30137
+ const positions = this.network.getPositions(params.nodes);
30138
+ const updates = params.nodes.map((id2) => ({
30139
+ id: id2,
30140
+ x: positions[id2].x,
30141
+ y: positions[id2].y,
30142
+ fixed: { x: true, y: true }
30143
+ }));
30144
+ this.nodesDataSet.update(updates);
30145
+ }
30146
+ });
30147
+ this.setupNodeExpansion();
30148
+ this.uriToNodeId = /* @__PURE__ */ new Map();
30149
+ this.nodesDataSet.get().forEach((node) => {
30150
+ if (node.uri) {
30151
+ this.uriToNodeId.set(node.uri, node.id);
30152
+ }
29786
30153
  });
29787
30154
  if (!this.themeObserver) {
29788
30155
  this.themeObserver = watchThemeChanges((newTheme) => {
@@ -29801,6 +30168,20 @@ var GraphPlugin = class {
29801
30168
  }
29802
30169
  };
29803
30170
  controls.appendChild(fitButton);
30171
+ const settingsButton = document.createElement("button");
30172
+ settingsButton.className = "yasgui-graph-button yasgui-graph-settings-button";
30173
+ settingsButton.setAttribute("aria-label", "Graph settings");
30174
+ settingsButton.innerHTML = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
30175
+ <path d="M8 10.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
30176
+ <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.434.901-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.892 3.434-.901 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.892-1.64-.901-3.434-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.474l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
30177
+ </svg> Settings`;
30178
+ settingsButton.onclick = () => {
30179
+ this.toggleSettingsPanel(container);
30180
+ };
30181
+ controls.appendChild(settingsButton);
30182
+ if (wasPanelOpen) {
30183
+ this.toggleSettingsPanel(container);
30184
+ }
29804
30185
  } catch (error) {
29805
30186
  console.error("Error rendering graph:", error);
29806
30187
  const errorDiv = document.createElement("div");
@@ -29809,6 +30190,111 @@ var GraphPlugin = class {
29809
30190
  this.yasr.resultsEl.appendChild(errorDiv);
29810
30191
  }
29811
30192
  }
30193
+ /**
30194
+ * Setup custom HTML tooltip rendering for vis-network
30195
+ * @param container - The graph container element
30196
+ */
30197
+ setupHtmlTooltips(container) {
30198
+ if (!this.network) return;
30199
+ let hideTimeout = null;
30200
+ this.network.on("hoverNode", (params) => {
30201
+ if (hideTimeout) {
30202
+ clearTimeout(hideTimeout);
30203
+ hideTimeout = null;
30204
+ }
30205
+ const nodeId = params.node;
30206
+ const node = this.nodesDataSet.get(nodeId);
30207
+ if (node && node.title) {
30208
+ this.showHtmlTooltip(container, node.title, params.pointer.DOM);
30209
+ }
30210
+ });
30211
+ this.network.on("hoverEdge", (params) => {
30212
+ if (hideTimeout) {
30213
+ clearTimeout(hideTimeout);
30214
+ hideTimeout = null;
30215
+ }
30216
+ const edgeId = params.edge;
30217
+ const edge = this.edgesDataSet.get(edgeId);
30218
+ if (edge && edge.title) {
30219
+ this.showHtmlTooltip(container, edge.title, params.pointer.DOM);
30220
+ }
30221
+ });
30222
+ this.network.on("blurNode", () => {
30223
+ hideTimeout = window.setTimeout(() => {
30224
+ this.hideHtmlTooltipIfNotHovered(container);
30225
+ }, 200);
30226
+ });
30227
+ this.network.on("blurEdge", () => {
30228
+ hideTimeout = window.setTimeout(() => {
30229
+ this.hideHtmlTooltipIfNotHovered(container);
30230
+ }, 200);
30231
+ });
30232
+ this.network.on("dragStart", () => {
30233
+ if (hideTimeout) {
30234
+ clearTimeout(hideTimeout);
30235
+ hideTimeout = null;
30236
+ }
30237
+ this.hideHtmlTooltip(container);
30238
+ });
30239
+ this.network.on("zoom", () => {
30240
+ if (hideTimeout) {
30241
+ clearTimeout(hideTimeout);
30242
+ hideTimeout = null;
30243
+ }
30244
+ this.hideHtmlTooltip(container);
30245
+ });
30246
+ }
30247
+ /**
30248
+ * Show HTML tooltip at specified position
30249
+ * @param container - Container element
30250
+ * @param htmlContent - HTML content to display
30251
+ * @param position - Mouse position {x, y}
30252
+ */
30253
+ showHtmlTooltip(container, htmlContent, position) {
30254
+ this.hideHtmlTooltip(container);
30255
+ const tooltip = document.createElement("div");
30256
+ tooltip.className = "yasgui-graph-tooltip-container";
30257
+ tooltip.innerHTML = htmlContent;
30258
+ tooltip.style.position = "absolute";
30259
+ tooltip.style.left = `${position.x + 10}px`;
30260
+ tooltip.style.top = `${position.y + 10}px`;
30261
+ tooltip.style.zIndex = "1000";
30262
+ tooltip.addEventListener("mouseleave", () => {
30263
+ this.hideHtmlTooltip(container);
30264
+ });
30265
+ container.appendChild(tooltip);
30266
+ const rect = tooltip.getBoundingClientRect();
30267
+ const containerRect = container.getBoundingClientRect();
30268
+ if (rect.right > containerRect.right) {
30269
+ tooltip.style.left = `${position.x - rect.width - 10}px`;
30270
+ }
30271
+ if (rect.bottom > containerRect.bottom) {
30272
+ tooltip.style.top = `${position.y - rect.height - 10}px`;
30273
+ }
30274
+ }
30275
+ /**
30276
+ * Hide HTML tooltip
30277
+ * @param container - Container element
30278
+ */
30279
+ hideHtmlTooltip(container) {
30280
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30281
+ if (existingTooltip) {
30282
+ existingTooltip.remove();
30283
+ }
30284
+ }
30285
+ /**
30286
+ * Hide HTML tooltip only if mouse is not hovering over it
30287
+ * @param container - Container element
30288
+ */
30289
+ hideHtmlTooltipIfNotHovered(container) {
30290
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30291
+ if (existingTooltip) {
30292
+ const isHovered = existingTooltip.matches(":hover");
30293
+ if (!isHovered) {
30294
+ existingTooltip.remove();
30295
+ }
30296
+ }
30297
+ }
29812
30298
  /**
29813
30299
  * Apply theme to existing network
29814
30300
  * @param newTheme - 'light' or 'dark'
@@ -29819,12 +30305,30 @@ var GraphPlugin = class {
29819
30305
  }
29820
30306
  this.currentTheme = newTheme;
29821
30307
  const themeColors = getThemeNodeColors(newTheme);
29822
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30308
+ const fixedNodes = {};
30309
+ this.nodesDataSet.get().forEach((node) => {
30310
+ var _a, _b;
30311
+ const isFixed = node.fixed === true || typeof node.fixed === "object" && ((_a = node.fixed) == null ? void 0 : _a.x) && ((_b = node.fixed) == null ? void 0 : _b.y);
30312
+ if (isFixed && node.x !== void 0 && node.y !== void 0) {
30313
+ fixedNodes[node.id] = { x: node.x, y: node.y };
30314
+ }
30315
+ });
30316
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29823
30317
  this.nodesDataSet.clear();
29824
30318
  this.nodesDataSet.add(nodes);
29825
30319
  this.edgesDataSet.clear();
29826
30320
  this.edgesDataSet.add(edges);
29827
- const options = getDefaultNetworkOptions(themeColors);
30321
+ const fixedIds = Object.keys(fixedNodes);
30322
+ if (fixedIds.length > 0) {
30323
+ const updates = fixedIds.map((id2) => ({
30324
+ id: id2,
30325
+ x: fixedNodes[id2].x,
30326
+ y: fixedNodes[id2].y,
30327
+ fixed: { x: true, y: true }
30328
+ }));
30329
+ this.nodesDataSet.update(updates);
30330
+ }
30331
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29828
30332
  this.network.setOptions(options);
29829
30333
  this.applyCanvasBackground(themeColors.background);
29830
30334
  }
@@ -29861,7 +30365,287 @@ var GraphPlugin = class {
29861
30365
  this.resizeObserver.observe(parent2);
29862
30366
  }
29863
30367
  /**
29864
- * Get icon for plugin tab
30368
+ * Toggle the settings panel open/closed
30369
+ * @param container - The graph container element
30370
+ */
30371
+ toggleSettingsPanel(container) {
30372
+ const existing = container.querySelector(".yasgui-graph-settings-panel");
30373
+ if (existing) {
30374
+ existing.remove();
30375
+ this.settingsPanelOpen = false;
30376
+ this.removeClickOutsideHandler();
30377
+ } else {
30378
+ const panel = this.createSettingsPanel(container);
30379
+ container.appendChild(panel);
30380
+ this.settingsPanelOpen = true;
30381
+ this.setupClickOutsideHandler(container, panel);
30382
+ }
30383
+ }
30384
+ /**
30385
+ * Setup click-outside-to-close handler for settings panel
30386
+ * @param container - The graph container element
30387
+ * @param panel - The settings panel element
30388
+ */
30389
+ setupClickOutsideHandler(container, panel) {
30390
+ this.removeClickOutsideHandler();
30391
+ this.clickOutsideHandler = (event) => {
30392
+ const target = event.target;
30393
+ if (!panel.contains(target) && !this.isSettingsButton(target)) {
30394
+ this.toggleSettingsPanel(container);
30395
+ }
30396
+ };
30397
+ setTimeout(() => {
30398
+ document.addEventListener("click", this.clickOutsideHandler);
30399
+ }, 100);
30400
+ }
30401
+ /**
30402
+ * Remove the click-outside handler
30403
+ */
30404
+ removeClickOutsideHandler() {
30405
+ if (this.clickOutsideHandler) {
30406
+ document.removeEventListener("click", this.clickOutsideHandler);
30407
+ this.clickOutsideHandler = null;
30408
+ }
30409
+ }
30410
+ /**
30411
+ * Check if a node is the settings button or inside it
30412
+ * @param node - The node to check
30413
+ */
30414
+ isSettingsButton(node) {
30415
+ let current = node;
30416
+ while (current) {
30417
+ if (current instanceof Element && current.classList.contains("yasgui-graph-settings-button")) {
30418
+ return true;
30419
+ }
30420
+ current = current.parentNode;
30421
+ }
30422
+ return false;
30423
+ }
30424
+ /**
30425
+ * Build and return the settings panel element
30426
+ * @param container - The graph container element (used to re-draw on change)
30427
+ */
30428
+ createSettingsPanel(_container) {
30429
+ const panel = document.createElement("div");
30430
+ panel.className = "yasgui-graph-settings-panel";
30431
+ panel.setAttribute("role", "dialog");
30432
+ panel.setAttribute("aria-label", "Graph settings");
30433
+ const title = document.createElement("div");
30434
+ title.className = "yasgui-graph-settings-title";
30435
+ title.textContent = "Graph Settings";
30436
+ panel.appendChild(title);
30437
+ const addSection = (label) => {
30438
+ const h = document.createElement("div");
30439
+ h.className = "yasgui-graph-settings-section";
30440
+ h.textContent = label;
30441
+ panel.appendChild(h);
30442
+ };
30443
+ const addToggle = (label, checked, onChange) => {
30444
+ const row = document.createElement("label");
30445
+ row.className = "yasgui-graph-settings-row";
30446
+ const input = document.createElement("input");
30447
+ input.type = "checkbox";
30448
+ input.checked = checked;
30449
+ input.addEventListener("change", () => onChange(input.checked));
30450
+ const span = document.createElement("span");
30451
+ span.textContent = label;
30452
+ row.appendChild(input);
30453
+ row.appendChild(span);
30454
+ panel.appendChild(row);
30455
+ };
30456
+ const addSelect = (label, options, current, onChange) => {
30457
+ const row = document.createElement("div");
30458
+ row.className = "yasgui-graph-settings-row";
30459
+ const lbl = document.createElement("span");
30460
+ lbl.textContent = label;
30461
+ const sel = document.createElement("select");
30462
+ sel.className = "yasgui-graph-settings-select";
30463
+ options.forEach((o) => {
30464
+ const opt = document.createElement("option");
30465
+ opt.value = o.value;
30466
+ opt.textContent = o.label;
30467
+ if (o.value === current) opt.selected = true;
30468
+ sel.appendChild(opt);
30469
+ });
30470
+ sel.addEventListener("change", () => onChange(sel.value));
30471
+ row.appendChild(lbl);
30472
+ row.appendChild(sel);
30473
+ panel.appendChild(row);
30474
+ };
30475
+ const applyAndRedraw = () => {
30476
+ saveSettings(this.settings);
30477
+ this.draw();
30478
+ };
30479
+ addSection("Arrows");
30480
+ addSelect(
30481
+ "Style",
30482
+ [
30483
+ { value: "curved", label: "Curved" },
30484
+ { value: "straight", label: "Straight" }
30485
+ ],
30486
+ this.settings.edgeStyle,
30487
+ (v) => {
30488
+ this.settings.edgeStyle = v;
30489
+ applyAndRedraw();
30490
+ }
30491
+ );
30492
+ addSection("Predicate display");
30493
+ addSelect(
30494
+ "Display",
30495
+ [
30496
+ { value: "label", label: "Label (prefixed URI)" },
30497
+ { value: "icon", label: "Icon / symbol" },
30498
+ { value: "none", label: "Hidden" }
30499
+ ],
30500
+ this.settings.predicateDisplay,
30501
+ (v) => {
30502
+ this.settings.predicateDisplay = v;
30503
+ applyAndRedraw();
30504
+ }
30505
+ );
30506
+ addSection("Compact mode");
30507
+ addToggle("Compact mode", this.settings.compactMode, (v) => {
30508
+ this.settings.compactMode = v;
30509
+ applyAndRedraw();
30510
+ });
30511
+ addSection("Display");
30512
+ addToggle("Show node labels", this.settings.showNodeLabels, (v) => {
30513
+ this.settings.showNodeLabels = v;
30514
+ applyAndRedraw();
30515
+ });
30516
+ addToggle("Enable physics", this.settings.physicsEnabled, (v) => {
30517
+ this.settings.physicsEnabled = v;
30518
+ applyAndRedraw();
30519
+ });
30520
+ addSelect(
30521
+ "Node size",
30522
+ [
30523
+ { value: "small", label: "Small" },
30524
+ { value: "medium", label: "Medium" },
30525
+ { value: "large", label: "Large" }
30526
+ ],
30527
+ this.settings.nodeSize,
30528
+ (v) => {
30529
+ this.settings.nodeSize = v;
30530
+ applyAndRedraw();
30531
+ }
30532
+ );
30533
+ return panel;
30534
+ }
30535
+ /**
30536
+ * Setup double-click handler for node expansion
30537
+ */
30538
+ setupNodeExpansion() {
30539
+ if (!this.network) return;
30540
+ this.network.on("doubleClick", (params) => {
30541
+ if (params.nodes.length === 0) return;
30542
+ const nodeId = params.nodes[0];
30543
+ const node = this.nodesDataSet.get(nodeId);
30544
+ if (node && node.uri && !node.uri.startsWith("_:")) {
30545
+ this.expandNode(node.uri);
30546
+ }
30547
+ });
30548
+ }
30549
+ /**
30550
+ * Expand a node by executing a DESCRIBE query for the given URI and
30551
+ * merging the returned triples into the existing graph.
30552
+ * @param uri - URI of the node to expand
30553
+ */
30554
+ async expandNode(uri) {
30555
+ if (!this.yasr.executeQuery) {
30556
+ console.warn("yasgui-graph-plugin: background query execution not available (yasr.executeQuery is not configured)");
30557
+ return;
30558
+ }
30559
+ if (!this.triples || !this.prefixMap) return;
30560
+ if (this.expansionAbortController) {
30561
+ this.expansionAbortController.abort();
30562
+ }
30563
+ const controller = new AbortController();
30564
+ this.expansionAbortController = controller;
30565
+ const nodeId = this.uriToNodeId.get(uri);
30566
+ let originalColor = void 0;
30567
+ let originalBorderWidth = void 0;
30568
+ if (nodeId !== void 0) {
30569
+ const node = this.nodesDataSet.get(nodeId);
30570
+ if (node) {
30571
+ originalColor = node.color;
30572
+ originalBorderWidth = node.borderWidth;
30573
+ }
30574
+ this.nodesDataSet.update({
30575
+ id: nodeId,
30576
+ borderWidth: LOADING_BORDER_WIDTH,
30577
+ color: typeof originalColor === "object" && originalColor !== null ? { ...originalColor, border: LOADING_BORDER_COLOR } : { border: LOADING_BORDER_COLOR, background: originalColor != null ? originalColor : void 0 }
30578
+ });
30579
+ }
30580
+ const restoreNode = (borderWidth) => {
30581
+ if (nodeId !== void 0) {
30582
+ this.nodesDataSet.update({ id: nodeId, borderWidth, color: originalColor });
30583
+ }
30584
+ };
30585
+ try {
30586
+ const response = await this.yasr.executeQuery(`DESCRIBE <${uri}>`, {
30587
+ acceptHeader: "application/sparql-results+json",
30588
+ signal: controller.signal
30589
+ });
30590
+ if (controller.signal.aborted) {
30591
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30592
+ return;
30593
+ }
30594
+ const newTriples = await parseBackgroundQueryResponse(response);
30595
+ if (controller.signal.aborted) {
30596
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30597
+ return;
30598
+ }
30599
+ const existingKeys = new Set(
30600
+ this.triples.map((t) => `${t.subject}|${t.predicate}|${t.object.value}`)
30601
+ );
30602
+ const uniqueNew = newTriples.filter(
30603
+ (t) => !existingKeys.has(`${t.subject}|${t.predicate}|${t.object.value}`)
30604
+ );
30605
+ if (uniqueNew.length > 0) {
30606
+ this.triples = [...this.triples, ...uniqueNew];
30607
+ this.mergeNewTriples();
30608
+ }
30609
+ restoreNode(EXPANDED_BORDER_WIDTH);
30610
+ } catch (error) {
30611
+ if ((error == null ? void 0 : error.name) === "AbortError") {
30612
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30613
+ return;
30614
+ }
30615
+ console.error("yasgui-graph-plugin: error expanding node", uri, error);
30616
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30617
+ }
30618
+ }
30619
+ /**
30620
+ * Incrementally add new triples to the vis-network DataSets without a full redraw.
30621
+ * New nodes and edges are added; existing ones are left untouched.
30622
+ * Expects `this.triples` to already include the new triples.
30623
+ */
30624
+ mergeNewTriples() {
30625
+ if (!this.triples || !this.prefixMap || !this.nodesDataSet || !this.edgesDataSet) return;
30626
+ const themeColors = getThemeNodeColors(this.currentTheme);
30627
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
30628
+ const existingValues = new Set(this.nodesDataSet.get().map((n) => n.fullValue));
30629
+ const nodesToAdd = nodes.filter((n) => !existingValues.has(n.fullValue));
30630
+ const existingEdgeKeys = new Set(
30631
+ this.edgesDataSet.get().map((e) => `${e.from}|${e.predicate}|${e.to}`)
30632
+ );
30633
+ const edgesToAdd = edges.filter(
30634
+ (e) => !existingEdgeKeys.has(`${e.from}|${e.predicate}|${e.to}`)
30635
+ );
30636
+ if (nodesToAdd.length > 0) {
30637
+ this.nodesDataSet.add(nodesToAdd);
30638
+ nodesToAdd.forEach((n) => {
30639
+ if (n.uri != null) {
30640
+ this.uriToNodeId.set(n.uri, n.id);
30641
+ }
30642
+ });
30643
+ }
30644
+ if (edgesToAdd.length > 0) {
30645
+ this.edgesDataSet.add(edgesToAdd);
30646
+ }
30647
+ }
30648
+ /**
29865
30649
  * @returns Icon element
29866
30650
  */
29867
30651
  getIcon() {
@@ -29882,6 +30666,11 @@ var GraphPlugin = class {
29882
30666
  * Cleanup when plugin is destroyed
29883
30667
  */
29884
30668
  destroy() {
30669
+ this.removeClickOutsideHandler();
30670
+ if (this.expansionAbortController) {
30671
+ this.expansionAbortController.abort();
30672
+ this.expansionAbortController = null;
30673
+ }
29885
30674
  if (this.themeObserver) {
29886
30675
  this.themeObserver.disconnect();
29887
30676
  this.themeObserver = null;