@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.
@@ -67,14 +67,18 @@ function truncateLabel(text, maxLength = 50) {
67
67
  }
68
68
 
69
69
  // src/networkConfig.ts
70
- function getDefaultNetworkOptions(themeColors) {
70
+ function getDefaultNetworkOptions(themeColors, settings) {
71
+ const curved = !settings || settings.edgeStyle !== "straight";
72
+ const nodeSizeMap = { small: 6, medium: 10, large: 16 };
73
+ const nodeSize = (settings == null ? void 0 : settings.nodeSize) ? nodeSizeMap[settings.nodeSize] : 10;
74
+ const showNodeLabels = (settings == null ? void 0 : settings.showNodeLabels) !== false;
71
75
  return {
72
76
  // Configure canvas background color based on theme
73
77
  configure: {
74
78
  enabled: false
75
79
  },
76
80
  physics: {
77
- enabled: true,
81
+ enabled: (settings == null ? void 0 : settings.physicsEnabled) !== false,
78
82
  stabilization: {
79
83
  enabled: true,
80
84
  iterations: 200,
@@ -94,14 +98,16 @@ function getDefaultNetworkOptions(themeColors) {
94
98
  dragView: true,
95
99
  zoomView: true,
96
100
  hover: true,
97
- tooltipDelay: 300
101
+ tooltipDelay: 300,
98
102
  // 300ms hover delay per spec
103
+ hideEdgesOnDrag: false,
104
+ hideEdgesOnZoom: false
99
105
  },
100
106
  nodes: {
101
107
  shape: "dot",
102
- size: 10,
108
+ size: nodeSize,
103
109
  font: {
104
- size: 12,
110
+ size: showNodeLabels ? 12 : 0,
105
111
  color: themeColors.text
106
112
  },
107
113
  borderWidth: 1,
@@ -116,7 +122,7 @@ function getDefaultNetworkOptions(themeColors) {
116
122
  }
117
123
  },
118
124
  smooth: {
119
- enabled: true,
125
+ enabled: curved,
120
126
  type: "dynamic",
121
127
  roundness: 0.5
122
128
  },
@@ -136,6 +142,40 @@ function getDefaultNetworkOptions(themeColors) {
136
142
  }
137
143
 
138
144
  // src/parsers.ts
145
+ async function parseBackgroundQueryResponse(response) {
146
+ var _a, _b;
147
+ if (!response) return [];
148
+ try {
149
+ let data2;
150
+ if (typeof response.json === "function") {
151
+ data2 = await response.json();
152
+ } else if (typeof response.data === "string") {
153
+ data2 = JSON.parse(response.data);
154
+ } else if (typeof response === "object") {
155
+ data2 = response;
156
+ } else {
157
+ return [];
158
+ }
159
+ const bindings = (_b = (_a = data2 == null ? void 0 : data2.results) == null ? void 0 : _a.bindings) != null ? _b : [];
160
+ const triples = [];
161
+ for (const binding of bindings) {
162
+ if (!binding.subject || !binding.predicate || !binding.object || typeof binding.subject.value !== "string" || typeof binding.predicate.value !== "string" || typeof binding.object.value !== "string") continue;
163
+ triples.push({
164
+ subject: binding.subject.value,
165
+ predicate: binding.predicate.value,
166
+ object: {
167
+ value: binding.object.value,
168
+ type: binding.object.type || "uri",
169
+ datatype: binding.object.datatype,
170
+ lang: binding.object["xml:lang"]
171
+ }
172
+ });
173
+ }
174
+ return triples;
175
+ } catch (e) {
176
+ return [];
177
+ }
178
+ }
139
179
  function parseConstructResults(yasrResults) {
140
180
  const triples = [];
141
181
  if (!yasrResults || !yasrResults.getBindings) {
@@ -178,10 +218,129 @@ function getNodeColor(node, triples, themeColors) {
178
218
  return themeColors.uri;
179
219
  }
180
220
 
221
+ // src/predicateIcons.ts
222
+ var PREDICATE_ICONS = {
223
+ // RDF core
224
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": "a",
225
+ // Turtle's "a" shorthand
226
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#value": "val",
227
+ // RDFS
228
+ "http://www.w3.org/2000/01/rdf-schema#label": "lbl",
229
+ "http://www.w3.org/2000/01/rdf-schema#comment": "cmt",
230
+ "http://www.w3.org/2000/01/rdf-schema#subClassOf": "\u2282",
231
+ // ⊂
232
+ "http://www.w3.org/2000/01/rdf-schema#subPropertyOf": "\u2286",
233
+ // ⊆
234
+ "http://www.w3.org/2000/01/rdf-schema#domain": "dom",
235
+ "http://www.w3.org/2000/01/rdf-schema#range": "rng",
236
+ "http://www.w3.org/2000/01/rdf-schema#seeAlso": "see",
237
+ "http://www.w3.org/2000/01/rdf-schema#isDefinedBy": "idb",
238
+ // OWL
239
+ "http://www.w3.org/2002/07/owl#sameAs": "\u2261",
240
+ // ≡
241
+ "http://www.w3.org/2002/07/owl#equivalentClass": "\u2245",
242
+ // ≅
243
+ "http://www.w3.org/2002/07/owl#inverseOf": "\u21C4",
244
+ // ⇄
245
+ "http://www.w3.org/2002/07/owl#disjointWith": "\u2260",
246
+ // ≠
247
+ // SKOS
248
+ "http://www.w3.org/2004/02/skos/core#prefLabel": "\u2605",
249
+ // ★
250
+ "http://www.w3.org/2004/02/skos/core#altLabel": "\u2606",
251
+ // ☆
252
+ "http://www.w3.org/2004/02/skos/core#definition": "def",
253
+ "http://www.w3.org/2004/02/skos/core#broader": "\u2191",
254
+ // ↑
255
+ "http://www.w3.org/2004/02/skos/core#narrower": "\u2193",
256
+ // ↓
257
+ "http://www.w3.org/2004/02/skos/core#related": "\u2194",
258
+ // ↔
259
+ "http://www.w3.org/2004/02/skos/core#note": "note",
260
+ "http://www.w3.org/2004/02/skos/core#exactMatch": "\u2261",
261
+ // ≡
262
+ "http://www.w3.org/2004/02/skos/core#closeMatch": "\u2248",
263
+ // ≈
264
+ // Dublin Core Terms
265
+ "http://purl.org/dc/terms/title": "ttl",
266
+ "http://purl.org/dc/terms/description": "dsc",
267
+ "http://purl.org/dc/terms/created": "crt",
268
+ "http://purl.org/dc/terms/modified": "mod",
269
+ "http://purl.org/dc/terms/creator": "by",
270
+ "http://purl.org/dc/terms/subject": "sbj",
271
+ // FOAF
272
+ "http://xmlns.com/foaf/0.1/name": "nm",
273
+ "http://xmlns.com/foaf/0.1/knows": "\u27F7",
274
+ // ⟷
275
+ "http://xmlns.com/foaf/0.1/member": "mbr",
276
+ // Schema.org
277
+ "http://schema.org/name": "nm",
278
+ "http://schema.org/description": "dsc"
279
+ };
280
+ function getPredicateIcon(predicateUri) {
281
+ return PREDICATE_ICONS[predicateUri];
282
+ }
283
+
181
284
  // src/transformers.ts
182
- function createNodeMap(triples, prefixMap, themeColors) {
285
+ var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
286
+ var SCHEMA_IMAGE = "https://schema.org/image";
287
+ var SCHEMA_ICON = "https://schema.org/icon";
288
+ var RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
289
+ var RDFS_SUBCLASSOF = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
290
+ var SUPPRESSED_PREDICATES = /* @__PURE__ */ new Set([SCHEMA_IMAGE, SCHEMA_ICON, RDFS_LABEL]);
291
+ function appendTooltipRows(title, rows) {
292
+ const closingTag = "</div>";
293
+ const idx = title.lastIndexOf(closingTag);
294
+ if (idx === -1) return title + rows;
295
+ return title.slice(0, idx) + rows + closingTag;
296
+ }
297
+ function buildVisualTooltipRow(key, value) {
298
+ return `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">${escapeHtml(key)}</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
299
+ }
300
+ function escapeHtml(str) {
301
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
302
+ }
303
+ function createCompactNodeTooltipHTML(uri, triples, prefixMap) {
304
+ const isBlankNode = uri.startsWith("_:");
305
+ let rows = `<div class="yasgui-tooltip-type">${isBlankNode ? "Blank Node" : "URI"}</div>`;
306
+ 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>`;
307
+ triples.filter((t) => t.subject === uri && t.predicate === RDF_TYPE).forEach((t) => {
308
+ const typeLabel = applyPrefix(t.object.value, prefixMap);
309
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">rdf:type</span><span class="yasgui-tooltip-val">${escapeHtml(typeLabel)}</span></div>`;
310
+ });
311
+ triples.filter((t) => t.subject === uri && t.object.type === "literal").forEach((t) => {
312
+ const predLabel = applyPrefix(t.predicate, prefixMap);
313
+ 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>`;
314
+ });
315
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
316
+ }
317
+ function createNodeTooltipHTML(nodeType, value, datatype, lang, prefixMap) {
318
+ const typeLabel = nodeType === "uri" ? "URI" : nodeType === "literal" ? "Literal" : "Blank Node";
319
+ let rows = `<div class="yasgui-tooltip-type">${typeLabel}</div>`;
320
+ if (nodeType === "literal") {
321
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Value</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
322
+ if (datatype) {
323
+ const dtLabel = prefixMap ? applyPrefix(datatype, prefixMap) : datatype;
324
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Datatype</span><span class="yasgui-tooltip-val">${escapeHtml(dtLabel)}</span></div>`;
325
+ }
326
+ if (lang) {
327
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Language</span><span class="yasgui-tooltip-val">${escapeHtml(lang)}</span></div>`;
328
+ }
329
+ } else if (nodeType === "uri") {
330
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Full URI</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
331
+ } else if (nodeType === "bnode") {
332
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Identifier</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
333
+ }
334
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
335
+ }
336
+ function createEdgeTooltipHTML(predicateUri) {
337
+ 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>`;
338
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
339
+ }
340
+ function createNodeMap(triples, prefixMap, themeColors, settings) {
183
341
  const nodeMap = /* @__PURE__ */ new Map();
184
342
  let nodeId = 1;
343
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
185
344
  triples.forEach((triple) => {
186
345
  if (!nodeMap.has(triple.subject)) {
187
346
  const isBlankNode = triple.subject.startsWith("_:");
@@ -193,11 +352,18 @@ function createNodeMap(triples, prefixMap, themeColors) {
193
352
  color: getNodeColor({ uri: triple.subject, type: "uri" }, triples, themeColors),
194
353
  type: "uri",
195
354
  fullValue: triple.subject,
196
- title: isBlankNode ? triple.subject : applyPrefix(triple.subject, prefixMap)
355
+ size: 10 * sizeMultiplier,
356
+ title: createNodeTooltipHTML(
357
+ isBlankNode ? "bnode" : "uri",
358
+ triple.subject,
359
+ void 0,
360
+ void 0,
361
+ prefixMap
362
+ )
197
363
  });
198
364
  }
199
365
  const objValue = triple.object.value;
200
- if (!nodeMap.has(objValue)) {
366
+ if (!nodeMap.has(objValue) && !SUPPRESSED_PREDICATES.has(triple.predicate)) {
201
367
  const isLiteral = triple.object.type === "literal";
202
368
  const isBlankNode = !isLiteral && objValue.startsWith("_:");
203
369
  let label;
@@ -206,15 +372,21 @@ function createNodeMap(triples, prefixMap, themeColors) {
206
372
  if (isLiteral) {
207
373
  label = truncateLabel(objValue);
208
374
  fullValue = objValue;
209
- title = triple.object.datatype ? `"${objValue}"^^${applyPrefix(triple.object.datatype, prefixMap)}` : `"${objValue}"`;
375
+ title = createNodeTooltipHTML(
376
+ "literal",
377
+ objValue,
378
+ triple.object.datatype,
379
+ triple.object.lang,
380
+ prefixMap
381
+ );
210
382
  } else if (isBlankNode) {
211
383
  label = objValue;
212
384
  fullValue = objValue;
213
- title = objValue;
385
+ title = createNodeTooltipHTML("bnode", objValue);
214
386
  } else {
215
387
  label = truncateLabel(applyPrefix(objValue, prefixMap));
216
388
  fullValue = objValue;
217
- title = applyPrefix(objValue, prefixMap);
389
+ title = createNodeTooltipHTML("uri", objValue, void 0, void 0, prefixMap);
218
390
  }
219
391
  nodeMap.set(objValue, {
220
392
  id: nodeId++,
@@ -227,39 +399,157 @@ function createNodeMap(triples, prefixMap, themeColors) {
227
399
  ),
228
400
  type: isLiteral ? "literal" : "uri",
229
401
  fullValue,
402
+ size: (isLiteral ? 5 : 10) * sizeMultiplier,
230
403
  title
231
404
  });
232
405
  }
233
406
  });
234
407
  return nodeMap;
235
408
  }
236
- function createEdgesArray(triples, nodeMap, prefixMap) {
409
+ function createEdgesArray(triples, nodeMap, prefixMap, settings) {
237
410
  const edges = [];
238
411
  const edgeSet = /* @__PURE__ */ new Set();
239
412
  triples.forEach((triple) => {
413
+ var _a, _b;
414
+ if (SUPPRESSED_PREDICATES.has(triple.predicate)) return;
240
415
  const fromNode = nodeMap.get(triple.subject);
241
416
  const toNode = nodeMap.get(triple.object.value);
242
417
  if (!fromNode || !toNode) return;
243
418
  const edgeKey = `${fromNode.id}-${triple.predicate}-${toNode.id}`;
244
419
  if (!edgeSet.has(edgeKey)) {
245
420
  edgeSet.add(edgeKey);
421
+ let edgeLabel;
422
+ const predicateDisplay = (_a = settings == null ? void 0 : settings.predicateDisplay) != null ? _a : "label";
423
+ if (predicateDisplay === "none") {
424
+ edgeLabel = "";
425
+ } else if (predicateDisplay === "icon") {
426
+ edgeLabel = (_b = getPredicateIcon(triple.predicate)) != null ? _b : truncateLabel(applyPrefix(triple.predicate, prefixMap));
427
+ } else {
428
+ edgeLabel = truncateLabel(applyPrefix(triple.predicate, prefixMap));
429
+ }
246
430
  edges.push({
247
431
  id: `edge_${fromNode.id}_${toNode.id}_${edges.length}`,
248
432
  from: fromNode.id,
249
433
  to: toNode.id,
250
- label: truncateLabel(applyPrefix(triple.predicate, prefixMap)),
434
+ label: edgeLabel,
251
435
  predicate: triple.predicate,
252
- title: applyPrefix(triple.predicate, prefixMap),
436
+ title: createEdgeTooltipHTML(triple.predicate),
253
437
  arrows: "to"
254
438
  });
255
439
  }
256
440
  });
257
441
  return edges;
258
442
  }
259
- function triplesToGraph(triples, prefixMap, themeColors) {
260
- const nodeMap = createNodeMap(triples, prefixMap, themeColors);
261
- const edges = createEdgesArray(triples, nodeMap, prefixMap);
262
- const nodes = Array.from(nodeMap.values());
443
+ function isNodeVisible(node, triples, settings) {
444
+ if (node.uri && node.uri.startsWith("_:")) {
445
+ return true;
446
+ }
447
+ if (!settings.compactMode) {
448
+ return true;
449
+ }
450
+ if (node.type === "literal") {
451
+ return false;
452
+ }
453
+ const isClass = triples.some(
454
+ (t) => t.predicate === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" && t.object.value === node.uri
455
+ );
456
+ if (isClass) {
457
+ return false;
458
+ }
459
+ return true;
460
+ }
461
+ function triplesToGraph(triples, prefixMap, themeColors, settings) {
462
+ const nodeMap = createNodeMap(triples, prefixMap, themeColors, settings);
463
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
464
+ const tripleIndex = /* @__PURE__ */ new Map();
465
+ for (const t of triples) {
466
+ if (!tripleIndex.has(t.subject)) tripleIndex.set(t.subject, /* @__PURE__ */ new Map());
467
+ const sp = tripleIndex.get(t.subject);
468
+ if (!sp.has(t.predicate)) sp.set(t.predicate, []);
469
+ sp.get(t.predicate).push(t.object.value);
470
+ }
471
+ function getNodeVisualIdx(uri) {
472
+ const sp = tripleIndex.get(uri);
473
+ if (!sp) return {};
474
+ const icons = sp.get(SCHEMA_ICON);
475
+ if (icons == null ? void 0 : icons.length) return { icon: icons[0] };
476
+ const images = sp.get(SCHEMA_IMAGE);
477
+ if (images == null ? void 0 : images.length) return { image: images[0] };
478
+ return {};
479
+ }
480
+ function resolveCompactVisualIdx(uri) {
481
+ var _a, _b, _c, _d;
482
+ const own = getNodeVisualIdx(uri);
483
+ if (own.icon || own.image) return own;
484
+ const typeUris = (_b = (_a = tripleIndex.get(uri)) == null ? void 0 : _a.get(RDF_TYPE)) != null ? _b : [];
485
+ for (const typeUri of typeUris) {
486
+ const cv = getNodeVisualIdx(typeUri);
487
+ if (cv.icon || cv.image) return cv;
488
+ }
489
+ for (const typeUri of typeUris) {
490
+ const superUris = (_d = (_c = tripleIndex.get(typeUri)) == null ? void 0 : _c.get(RDFS_SUBCLASSOF)) != null ? _d : [];
491
+ for (const superUri of superUris) {
492
+ const sv = getNodeVisualIdx(superUri);
493
+ if (sv.icon || sv.image) return sv;
494
+ }
495
+ }
496
+ return {};
497
+ }
498
+ if (settings == null ? void 0 : settings.compactMode) {
499
+ const subjects = new Set(triples.map((t) => t.subject));
500
+ subjects.forEach((subjectUri) => {
501
+ const node = nodeMap.get(subjectUri);
502
+ if (node) {
503
+ node.title = createCompactNodeTooltipHTML(subjectUri, triples, prefixMap);
504
+ }
505
+ });
506
+ }
507
+ nodeMap.forEach((node) => {
508
+ var _a, _b;
509
+ if (!node.uri || node.type === "literal") return;
510
+ const visual = (settings == null ? void 0 : settings.compactMode) ? resolveCompactVisualIdx(node.uri) : getNodeVisualIdx(node.uri);
511
+ const rdfsLabel = (_b = (_a = tripleIndex.get(node.uri)) == null ? void 0 : _a.get(RDFS_LABEL)) == null ? void 0 : _b[0];
512
+ if (visual.icon) {
513
+ node.shape = "text";
514
+ node.label = rdfsLabel ? `${visual.icon}
515
+ ${rdfsLabel}` : visual.icon;
516
+ node.font = { size: 14 * sizeMultiplier };
517
+ if (!(settings == null ? void 0 : settings.compactMode)) {
518
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Icon", visual.icon));
519
+ }
520
+ } else if (visual.image) {
521
+ let imageAllowed = false;
522
+ try {
523
+ const parsed = new URL(visual.image);
524
+ imageAllowed = parsed.protocol === "http:" || parsed.protocol === "https:";
525
+ } catch (e) {
526
+ }
527
+ if (imageAllowed) {
528
+ node.shape = "circularImage";
529
+ node.image = visual.image;
530
+ }
531
+ if (rdfsLabel) {
532
+ node.label = rdfsLabel;
533
+ node.font = { size: 14 * sizeMultiplier };
534
+ }
535
+ if (!(settings == null ? void 0 : settings.compactMode)) {
536
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Image", visual.image));
537
+ }
538
+ } else if (rdfsLabel) {
539
+ node.label = rdfsLabel;
540
+ }
541
+ if (rdfsLabel && !(settings == null ? void 0 : settings.compactMode)) {
542
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("rdfs:label", rdfsLabel));
543
+ }
544
+ });
545
+ const visibleNodeIds = /* @__PURE__ */ new Set();
546
+ nodeMap.forEach((node) => {
547
+ if (!settings || isNodeVisible(node, triples, settings)) {
548
+ visibleNodeIds.add(node.id);
549
+ }
550
+ });
551
+ const edges = createEdgesArray(triples, nodeMap, prefixMap, settings).filter((e) => visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to));
552
+ const nodes = Array.from(nodeMap.values()).filter((n) => visibleNodeIds.has(n.id));
263
553
  return { nodes, edges };
264
554
  }
265
555
 
@@ -29730,9 +30020,44 @@ function watchThemeChanges(callback) {
29730
30020
  return observer;
29731
30021
  }
29732
30022
 
30023
+ // src/settings.ts
30024
+ var DEFAULT_SETTINGS = {
30025
+ edgeStyle: "curved",
30026
+ compactMode: false,
30027
+ predicateDisplay: "icon",
30028
+ showNodeLabels: true,
30029
+ physicsEnabled: true,
30030
+ nodeSize: "medium"
30031
+ };
30032
+ var STORAGE_KEY = "yasgui-graph-plugin-settings";
30033
+ function loadSettings() {
30034
+ try {
30035
+ const stored = localStorage.getItem(STORAGE_KEY);
30036
+ if (stored) {
30037
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
30038
+ }
30039
+ } catch (e) {
30040
+ }
30041
+ return { ...DEFAULT_SETTINGS };
30042
+ }
30043
+ function saveSettings(settings) {
30044
+ try {
30045
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
30046
+ } catch (e) {
30047
+ }
30048
+ }
30049
+
29733
30050
  // src/GraphPlugin.ts
30051
+ var LOADING_BORDER_WIDTH = 4;
30052
+ var LOADING_BORDER_COLOR = "#ffa500";
30053
+ var EXPANDED_BORDER_WIDTH = 3;
30054
+ var DEFAULT_BORDER_WIDTH = 2;
29734
30055
  var GraphPlugin = class {
29735
30056
  constructor(yasr) {
30057
+ this.settingsPanelOpen = false;
30058
+ this.clickOutsideHandler = null;
30059
+ this.expansionAbortController = null;
30060
+ this.uriToNodeId = /* @__PURE__ */ new Map();
29736
30061
  this.yasr = yasr;
29737
30062
  this.network = null;
29738
30063
  this.currentTheme = getCurrentTheme();
@@ -29742,6 +30067,7 @@ var GraphPlugin = class {
29742
30067
  this.edgesDataSet = null;
29743
30068
  this.triples = null;
29744
30069
  this.prefixMap = null;
30070
+ this.settings = loadSettings();
29745
30071
  }
29746
30072
  /**
29747
30073
  * Plugin priority (higher = shown first in tabs)
@@ -29755,6 +30081,12 @@ var GraphPlugin = class {
29755
30081
  static get label() {
29756
30082
  return "Graph";
29757
30083
  }
30084
+ /**
30085
+ * Help/documentation URL
30086
+ */
30087
+ static get helpReference() {
30088
+ return "https://yasgui-doc.matdata.eu/docs/user-guide#graph-plugin";
30089
+ }
29758
30090
  /**
29759
30091
  * Check if plugin can handle the current results
29760
30092
  * @returns True if results are from CONSTRUCT or DESCRIBE query
@@ -29775,6 +30107,13 @@ var GraphPlugin = class {
29775
30107
  * Render the graph visualization
29776
30108
  */
29777
30109
  draw() {
30110
+ const wasPanelOpen = this.settingsPanelOpen;
30111
+ if (this.expansionAbortController) {
30112
+ this.expansionAbortController.abort();
30113
+ this.expansionAbortController = null;
30114
+ }
30115
+ this.expandedNodes = /* @__PURE__ */ new Set();
30116
+ this.uriToNodeId = /* @__PURE__ */ new Map();
29778
30117
  this.yasr.resultsEl.innerHTML = "";
29779
30118
  try {
29780
30119
  this.triples = parseConstructResults(this.yasr.results);
@@ -29791,24 +30130,52 @@ var GraphPlugin = class {
29791
30130
  this.prefixMap = extractPrefixes(this.yasr);
29792
30131
  this.currentTheme = getCurrentTheme();
29793
30132
  const themeColors = getThemeNodeColors(this.currentTheme);
29794
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30133
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29795
30134
  const container = document.createElement("div");
29796
30135
  container.className = "yasgui-graph-plugin-container";
29797
30136
  container.id = "yasgui-graph-plugin-container";
29798
30137
  this.yasr.resultsEl.appendChild(container);
29799
30138
  this.nodesDataSet = new DataSet(nodes);
29800
30139
  this.edgesDataSet = new DataSet(edges);
29801
- const options = getDefaultNetworkOptions(themeColors);
30140
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29802
30141
  this.network = new Network(
29803
30142
  container,
29804
30143
  { nodes: this.nodesDataSet, edges: this.edgesDataSet },
29805
30144
  options
29806
30145
  );
30146
+ this.setupHtmlTooltips(container);
29807
30147
  this.applyCanvasBackground(themeColors.background);
29808
- this.network.on("stabilizationIterationsDone", () => {
29809
- this.network.setOptions({ physics: { enabled: true } });
29810
- this.network.fit({ maxZoomLevel: 3 });
29811
- this.setupContainerResize(container);
30148
+ this.setupContainerResize(container);
30149
+ if (this.settings.physicsEnabled) {
30150
+ this.network.on("stabilizationIterationsDone", () => {
30151
+ this.network.setOptions({ physics: { enabled: true } });
30152
+ this.network.fit({ maxZoomLevel: 3 });
30153
+ });
30154
+ } else {
30155
+ setTimeout(() => {
30156
+ if (this.network) {
30157
+ this.network.fit({ maxZoomLevel: 3 });
30158
+ }
30159
+ }, 100);
30160
+ }
30161
+ this.network.on("dragEnd", (params) => {
30162
+ if (params.nodes.length > 0) {
30163
+ const positions = this.network.getPositions(params.nodes);
30164
+ const updates = params.nodes.map((id2) => ({
30165
+ id: id2,
30166
+ x: positions[id2].x,
30167
+ y: positions[id2].y,
30168
+ fixed: { x: true, y: true }
30169
+ }));
30170
+ this.nodesDataSet.update(updates);
30171
+ }
30172
+ });
30173
+ this.setupNodeExpansion();
30174
+ this.uriToNodeId = /* @__PURE__ */ new Map();
30175
+ this.nodesDataSet.get().forEach((node) => {
30176
+ if (node.uri) {
30177
+ this.uriToNodeId.set(node.uri, node.id);
30178
+ }
29812
30179
  });
29813
30180
  if (!this.themeObserver) {
29814
30181
  this.themeObserver = watchThemeChanges((newTheme) => {
@@ -29827,6 +30194,20 @@ var GraphPlugin = class {
29827
30194
  }
29828
30195
  };
29829
30196
  controls.appendChild(fitButton);
30197
+ const settingsButton = document.createElement("button");
30198
+ settingsButton.className = "yasgui-graph-button yasgui-graph-settings-button";
30199
+ settingsButton.setAttribute("aria-label", "Graph settings");
30200
+ settingsButton.innerHTML = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
30201
+ <path d="M8 10.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
30202
+ <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"/>
30203
+ </svg> Settings`;
30204
+ settingsButton.onclick = () => {
30205
+ this.toggleSettingsPanel(container);
30206
+ };
30207
+ controls.appendChild(settingsButton);
30208
+ if (wasPanelOpen) {
30209
+ this.toggleSettingsPanel(container);
30210
+ }
29830
30211
  } catch (error) {
29831
30212
  console.error("Error rendering graph:", error);
29832
30213
  const errorDiv = document.createElement("div");
@@ -29835,6 +30216,111 @@ var GraphPlugin = class {
29835
30216
  this.yasr.resultsEl.appendChild(errorDiv);
29836
30217
  }
29837
30218
  }
30219
+ /**
30220
+ * Setup custom HTML tooltip rendering for vis-network
30221
+ * @param container - The graph container element
30222
+ */
30223
+ setupHtmlTooltips(container) {
30224
+ if (!this.network) return;
30225
+ let hideTimeout = null;
30226
+ this.network.on("hoverNode", (params) => {
30227
+ if (hideTimeout) {
30228
+ clearTimeout(hideTimeout);
30229
+ hideTimeout = null;
30230
+ }
30231
+ const nodeId = params.node;
30232
+ const node = this.nodesDataSet.get(nodeId);
30233
+ if (node && node.title) {
30234
+ this.showHtmlTooltip(container, node.title, params.pointer.DOM);
30235
+ }
30236
+ });
30237
+ this.network.on("hoverEdge", (params) => {
30238
+ if (hideTimeout) {
30239
+ clearTimeout(hideTimeout);
30240
+ hideTimeout = null;
30241
+ }
30242
+ const edgeId = params.edge;
30243
+ const edge = this.edgesDataSet.get(edgeId);
30244
+ if (edge && edge.title) {
30245
+ this.showHtmlTooltip(container, edge.title, params.pointer.DOM);
30246
+ }
30247
+ });
30248
+ this.network.on("blurNode", () => {
30249
+ hideTimeout = window.setTimeout(() => {
30250
+ this.hideHtmlTooltipIfNotHovered(container);
30251
+ }, 200);
30252
+ });
30253
+ this.network.on("blurEdge", () => {
30254
+ hideTimeout = window.setTimeout(() => {
30255
+ this.hideHtmlTooltipIfNotHovered(container);
30256
+ }, 200);
30257
+ });
30258
+ this.network.on("dragStart", () => {
30259
+ if (hideTimeout) {
30260
+ clearTimeout(hideTimeout);
30261
+ hideTimeout = null;
30262
+ }
30263
+ this.hideHtmlTooltip(container);
30264
+ });
30265
+ this.network.on("zoom", () => {
30266
+ if (hideTimeout) {
30267
+ clearTimeout(hideTimeout);
30268
+ hideTimeout = null;
30269
+ }
30270
+ this.hideHtmlTooltip(container);
30271
+ });
30272
+ }
30273
+ /**
30274
+ * Show HTML tooltip at specified position
30275
+ * @param container - Container element
30276
+ * @param htmlContent - HTML content to display
30277
+ * @param position - Mouse position {x, y}
30278
+ */
30279
+ showHtmlTooltip(container, htmlContent, position) {
30280
+ this.hideHtmlTooltip(container);
30281
+ const tooltip = document.createElement("div");
30282
+ tooltip.className = "yasgui-graph-tooltip-container";
30283
+ tooltip.innerHTML = htmlContent;
30284
+ tooltip.style.position = "absolute";
30285
+ tooltip.style.left = `${position.x + 10}px`;
30286
+ tooltip.style.top = `${position.y + 10}px`;
30287
+ tooltip.style.zIndex = "1000";
30288
+ tooltip.addEventListener("mouseleave", () => {
30289
+ this.hideHtmlTooltip(container);
30290
+ });
30291
+ container.appendChild(tooltip);
30292
+ const rect = tooltip.getBoundingClientRect();
30293
+ const containerRect = container.getBoundingClientRect();
30294
+ if (rect.right > containerRect.right) {
30295
+ tooltip.style.left = `${position.x - rect.width - 10}px`;
30296
+ }
30297
+ if (rect.bottom > containerRect.bottom) {
30298
+ tooltip.style.top = `${position.y - rect.height - 10}px`;
30299
+ }
30300
+ }
30301
+ /**
30302
+ * Hide HTML tooltip
30303
+ * @param container - Container element
30304
+ */
30305
+ hideHtmlTooltip(container) {
30306
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30307
+ if (existingTooltip) {
30308
+ existingTooltip.remove();
30309
+ }
30310
+ }
30311
+ /**
30312
+ * Hide HTML tooltip only if mouse is not hovering over it
30313
+ * @param container - Container element
30314
+ */
30315
+ hideHtmlTooltipIfNotHovered(container) {
30316
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30317
+ if (existingTooltip) {
30318
+ const isHovered = existingTooltip.matches(":hover");
30319
+ if (!isHovered) {
30320
+ existingTooltip.remove();
30321
+ }
30322
+ }
30323
+ }
29838
30324
  /**
29839
30325
  * Apply theme to existing network
29840
30326
  * @param newTheme - 'light' or 'dark'
@@ -29845,12 +30331,30 @@ var GraphPlugin = class {
29845
30331
  }
29846
30332
  this.currentTheme = newTheme;
29847
30333
  const themeColors = getThemeNodeColors(newTheme);
29848
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30334
+ const fixedNodes = {};
30335
+ this.nodesDataSet.get().forEach((node) => {
30336
+ var _a, _b;
30337
+ 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);
30338
+ if (isFixed && node.x !== void 0 && node.y !== void 0) {
30339
+ fixedNodes[node.id] = { x: node.x, y: node.y };
30340
+ }
30341
+ });
30342
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29849
30343
  this.nodesDataSet.clear();
29850
30344
  this.nodesDataSet.add(nodes);
29851
30345
  this.edgesDataSet.clear();
29852
30346
  this.edgesDataSet.add(edges);
29853
- const options = getDefaultNetworkOptions(themeColors);
30347
+ const fixedIds = Object.keys(fixedNodes);
30348
+ if (fixedIds.length > 0) {
30349
+ const updates = fixedIds.map((id2) => ({
30350
+ id: id2,
30351
+ x: fixedNodes[id2].x,
30352
+ y: fixedNodes[id2].y,
30353
+ fixed: { x: true, y: true }
30354
+ }));
30355
+ this.nodesDataSet.update(updates);
30356
+ }
30357
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29854
30358
  this.network.setOptions(options);
29855
30359
  this.applyCanvasBackground(themeColors.background);
29856
30360
  }
@@ -29887,7 +30391,287 @@ var GraphPlugin = class {
29887
30391
  this.resizeObserver.observe(parent2);
29888
30392
  }
29889
30393
  /**
29890
- * Get icon for plugin tab
30394
+ * Toggle the settings panel open/closed
30395
+ * @param container - The graph container element
30396
+ */
30397
+ toggleSettingsPanel(container) {
30398
+ const existing = container.querySelector(".yasgui-graph-settings-panel");
30399
+ if (existing) {
30400
+ existing.remove();
30401
+ this.settingsPanelOpen = false;
30402
+ this.removeClickOutsideHandler();
30403
+ } else {
30404
+ const panel = this.createSettingsPanel(container);
30405
+ container.appendChild(panel);
30406
+ this.settingsPanelOpen = true;
30407
+ this.setupClickOutsideHandler(container, panel);
30408
+ }
30409
+ }
30410
+ /**
30411
+ * Setup click-outside-to-close handler for settings panel
30412
+ * @param container - The graph container element
30413
+ * @param panel - The settings panel element
30414
+ */
30415
+ setupClickOutsideHandler(container, panel) {
30416
+ this.removeClickOutsideHandler();
30417
+ this.clickOutsideHandler = (event) => {
30418
+ const target = event.target;
30419
+ if (!panel.contains(target) && !this.isSettingsButton(target)) {
30420
+ this.toggleSettingsPanel(container);
30421
+ }
30422
+ };
30423
+ setTimeout(() => {
30424
+ document.addEventListener("click", this.clickOutsideHandler);
30425
+ }, 100);
30426
+ }
30427
+ /**
30428
+ * Remove the click-outside handler
30429
+ */
30430
+ removeClickOutsideHandler() {
30431
+ if (this.clickOutsideHandler) {
30432
+ document.removeEventListener("click", this.clickOutsideHandler);
30433
+ this.clickOutsideHandler = null;
30434
+ }
30435
+ }
30436
+ /**
30437
+ * Check if a node is the settings button or inside it
30438
+ * @param node - The node to check
30439
+ */
30440
+ isSettingsButton(node) {
30441
+ let current = node;
30442
+ while (current) {
30443
+ if (current instanceof Element && current.classList.contains("yasgui-graph-settings-button")) {
30444
+ return true;
30445
+ }
30446
+ current = current.parentNode;
30447
+ }
30448
+ return false;
30449
+ }
30450
+ /**
30451
+ * Build and return the settings panel element
30452
+ * @param container - The graph container element (used to re-draw on change)
30453
+ */
30454
+ createSettingsPanel(_container) {
30455
+ const panel = document.createElement("div");
30456
+ panel.className = "yasgui-graph-settings-panel";
30457
+ panel.setAttribute("role", "dialog");
30458
+ panel.setAttribute("aria-label", "Graph settings");
30459
+ const title = document.createElement("div");
30460
+ title.className = "yasgui-graph-settings-title";
30461
+ title.textContent = "Graph Settings";
30462
+ panel.appendChild(title);
30463
+ const addSection = (label) => {
30464
+ const h = document.createElement("div");
30465
+ h.className = "yasgui-graph-settings-section";
30466
+ h.textContent = label;
30467
+ panel.appendChild(h);
30468
+ };
30469
+ const addToggle = (label, checked, onChange) => {
30470
+ const row = document.createElement("label");
30471
+ row.className = "yasgui-graph-settings-row";
30472
+ const input = document.createElement("input");
30473
+ input.type = "checkbox";
30474
+ input.checked = checked;
30475
+ input.addEventListener("change", () => onChange(input.checked));
30476
+ const span = document.createElement("span");
30477
+ span.textContent = label;
30478
+ row.appendChild(input);
30479
+ row.appendChild(span);
30480
+ panel.appendChild(row);
30481
+ };
30482
+ const addSelect = (label, options, current, onChange) => {
30483
+ const row = document.createElement("div");
30484
+ row.className = "yasgui-graph-settings-row";
30485
+ const lbl = document.createElement("span");
30486
+ lbl.textContent = label;
30487
+ const sel = document.createElement("select");
30488
+ sel.className = "yasgui-graph-settings-select";
30489
+ options.forEach((o) => {
30490
+ const opt = document.createElement("option");
30491
+ opt.value = o.value;
30492
+ opt.textContent = o.label;
30493
+ if (o.value === current) opt.selected = true;
30494
+ sel.appendChild(opt);
30495
+ });
30496
+ sel.addEventListener("change", () => onChange(sel.value));
30497
+ row.appendChild(lbl);
30498
+ row.appendChild(sel);
30499
+ panel.appendChild(row);
30500
+ };
30501
+ const applyAndRedraw = () => {
30502
+ saveSettings(this.settings);
30503
+ this.draw();
30504
+ };
30505
+ addSection("Arrows");
30506
+ addSelect(
30507
+ "Style",
30508
+ [
30509
+ { value: "curved", label: "Curved" },
30510
+ { value: "straight", label: "Straight" }
30511
+ ],
30512
+ this.settings.edgeStyle,
30513
+ (v) => {
30514
+ this.settings.edgeStyle = v;
30515
+ applyAndRedraw();
30516
+ }
30517
+ );
30518
+ addSection("Predicate display");
30519
+ addSelect(
30520
+ "Display",
30521
+ [
30522
+ { value: "label", label: "Label (prefixed URI)" },
30523
+ { value: "icon", label: "Icon / symbol" },
30524
+ { value: "none", label: "Hidden" }
30525
+ ],
30526
+ this.settings.predicateDisplay,
30527
+ (v) => {
30528
+ this.settings.predicateDisplay = v;
30529
+ applyAndRedraw();
30530
+ }
30531
+ );
30532
+ addSection("Compact mode");
30533
+ addToggle("Compact mode", this.settings.compactMode, (v) => {
30534
+ this.settings.compactMode = v;
30535
+ applyAndRedraw();
30536
+ });
30537
+ addSection("Display");
30538
+ addToggle("Show node labels", this.settings.showNodeLabels, (v) => {
30539
+ this.settings.showNodeLabels = v;
30540
+ applyAndRedraw();
30541
+ });
30542
+ addToggle("Enable physics", this.settings.physicsEnabled, (v) => {
30543
+ this.settings.physicsEnabled = v;
30544
+ applyAndRedraw();
30545
+ });
30546
+ addSelect(
30547
+ "Node size",
30548
+ [
30549
+ { value: "small", label: "Small" },
30550
+ { value: "medium", label: "Medium" },
30551
+ { value: "large", label: "Large" }
30552
+ ],
30553
+ this.settings.nodeSize,
30554
+ (v) => {
30555
+ this.settings.nodeSize = v;
30556
+ applyAndRedraw();
30557
+ }
30558
+ );
30559
+ return panel;
30560
+ }
30561
+ /**
30562
+ * Setup double-click handler for node expansion
30563
+ */
30564
+ setupNodeExpansion() {
30565
+ if (!this.network) return;
30566
+ this.network.on("doubleClick", (params) => {
30567
+ if (params.nodes.length === 0) return;
30568
+ const nodeId = params.nodes[0];
30569
+ const node = this.nodesDataSet.get(nodeId);
30570
+ if (node && node.uri && !node.uri.startsWith("_:")) {
30571
+ this.expandNode(node.uri);
30572
+ }
30573
+ });
30574
+ }
30575
+ /**
30576
+ * Expand a node by executing a DESCRIBE query for the given URI and
30577
+ * merging the returned triples into the existing graph.
30578
+ * @param uri - URI of the node to expand
30579
+ */
30580
+ async expandNode(uri) {
30581
+ if (!this.yasr.executeQuery) {
30582
+ console.warn("yasgui-graph-plugin: background query execution not available (yasr.executeQuery is not configured)");
30583
+ return;
30584
+ }
30585
+ if (!this.triples || !this.prefixMap) return;
30586
+ if (this.expansionAbortController) {
30587
+ this.expansionAbortController.abort();
30588
+ }
30589
+ const controller = new AbortController();
30590
+ this.expansionAbortController = controller;
30591
+ const nodeId = this.uriToNodeId.get(uri);
30592
+ let originalColor = void 0;
30593
+ let originalBorderWidth = void 0;
30594
+ if (nodeId !== void 0) {
30595
+ const node = this.nodesDataSet.get(nodeId);
30596
+ if (node) {
30597
+ originalColor = node.color;
30598
+ originalBorderWidth = node.borderWidth;
30599
+ }
30600
+ this.nodesDataSet.update({
30601
+ id: nodeId,
30602
+ borderWidth: LOADING_BORDER_WIDTH,
30603
+ color: typeof originalColor === "object" && originalColor !== null ? { ...originalColor, border: LOADING_BORDER_COLOR } : { border: LOADING_BORDER_COLOR, background: originalColor != null ? originalColor : void 0 }
30604
+ });
30605
+ }
30606
+ const restoreNode = (borderWidth) => {
30607
+ if (nodeId !== void 0) {
30608
+ this.nodesDataSet.update({ id: nodeId, borderWidth, color: originalColor });
30609
+ }
30610
+ };
30611
+ try {
30612
+ const response = await this.yasr.executeQuery(`DESCRIBE <${uri}>`, {
30613
+ acceptHeader: "application/sparql-results+json",
30614
+ signal: controller.signal
30615
+ });
30616
+ if (controller.signal.aborted) {
30617
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30618
+ return;
30619
+ }
30620
+ const newTriples = await parseBackgroundQueryResponse(response);
30621
+ if (controller.signal.aborted) {
30622
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30623
+ return;
30624
+ }
30625
+ const existingKeys = new Set(
30626
+ this.triples.map((t) => `${t.subject}|${t.predicate}|${t.object.value}`)
30627
+ );
30628
+ const uniqueNew = newTriples.filter(
30629
+ (t) => !existingKeys.has(`${t.subject}|${t.predicate}|${t.object.value}`)
30630
+ );
30631
+ if (uniqueNew.length > 0) {
30632
+ this.triples = [...this.triples, ...uniqueNew];
30633
+ this.mergeNewTriples();
30634
+ }
30635
+ restoreNode(EXPANDED_BORDER_WIDTH);
30636
+ } catch (error) {
30637
+ if ((error == null ? void 0 : error.name) === "AbortError") {
30638
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30639
+ return;
30640
+ }
30641
+ console.error("yasgui-graph-plugin: error expanding node", uri, error);
30642
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30643
+ }
30644
+ }
30645
+ /**
30646
+ * Incrementally add new triples to the vis-network DataSets without a full redraw.
30647
+ * New nodes and edges are added; existing ones are left untouched.
30648
+ * Expects `this.triples` to already include the new triples.
30649
+ */
30650
+ mergeNewTriples() {
30651
+ if (!this.triples || !this.prefixMap || !this.nodesDataSet || !this.edgesDataSet) return;
30652
+ const themeColors = getThemeNodeColors(this.currentTheme);
30653
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
30654
+ const existingValues = new Set(this.nodesDataSet.get().map((n) => n.fullValue));
30655
+ const nodesToAdd = nodes.filter((n) => !existingValues.has(n.fullValue));
30656
+ const existingEdgeKeys = new Set(
30657
+ this.edgesDataSet.get().map((e) => `${e.from}|${e.predicate}|${e.to}`)
30658
+ );
30659
+ const edgesToAdd = edges.filter(
30660
+ (e) => !existingEdgeKeys.has(`${e.from}|${e.predicate}|${e.to}`)
30661
+ );
30662
+ if (nodesToAdd.length > 0) {
30663
+ this.nodesDataSet.add(nodesToAdd);
30664
+ nodesToAdd.forEach((n) => {
30665
+ if (n.uri != null) {
30666
+ this.uriToNodeId.set(n.uri, n.id);
30667
+ }
30668
+ });
30669
+ }
30670
+ if (edgesToAdd.length > 0) {
30671
+ this.edgesDataSet.add(edgesToAdd);
30672
+ }
30673
+ }
30674
+ /**
29891
30675
  * @returns Icon element
29892
30676
  */
29893
30677
  getIcon() {
@@ -29908,6 +30692,11 @@ var GraphPlugin = class {
29908
30692
  * Cleanup when plugin is destroyed
29909
30693
  */
29910
30694
  destroy() {
30695
+ this.removeClickOutsideHandler();
30696
+ if (this.expansionAbortController) {
30697
+ this.expansionAbortController.abort();
30698
+ this.expansionAbortController = null;
30699
+ }
29911
30700
  if (this.themeObserver) {
29912
30701
  this.themeObserver.disconnect();
29913
30702
  this.themeObserver = null;