@matdata/yasgui-graph-plugin 1.4.1 → 1.5.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
  },
@@ -178,10 +184,129 @@ function getNodeColor(node, triples, themeColors) {
178
184
  return themeColors.uri;
179
185
  }
180
186
 
187
+ // src/predicateIcons.ts
188
+ var PREDICATE_ICONS = {
189
+ // RDF core
190
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": "a",
191
+ // Turtle's "a" shorthand
192
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#value": "val",
193
+ // RDFS
194
+ "http://www.w3.org/2000/01/rdf-schema#label": "lbl",
195
+ "http://www.w3.org/2000/01/rdf-schema#comment": "cmt",
196
+ "http://www.w3.org/2000/01/rdf-schema#subClassOf": "\u2282",
197
+ // ⊂
198
+ "http://www.w3.org/2000/01/rdf-schema#subPropertyOf": "\u2286",
199
+ // ⊆
200
+ "http://www.w3.org/2000/01/rdf-schema#domain": "dom",
201
+ "http://www.w3.org/2000/01/rdf-schema#range": "rng",
202
+ "http://www.w3.org/2000/01/rdf-schema#seeAlso": "see",
203
+ "http://www.w3.org/2000/01/rdf-schema#isDefinedBy": "idb",
204
+ // OWL
205
+ "http://www.w3.org/2002/07/owl#sameAs": "\u2261",
206
+ // ≡
207
+ "http://www.w3.org/2002/07/owl#equivalentClass": "\u2245",
208
+ // ≅
209
+ "http://www.w3.org/2002/07/owl#inverseOf": "\u21C4",
210
+ // ⇄
211
+ "http://www.w3.org/2002/07/owl#disjointWith": "\u2260",
212
+ // ≠
213
+ // SKOS
214
+ "http://www.w3.org/2004/02/skos/core#prefLabel": "\u2605",
215
+ // ★
216
+ "http://www.w3.org/2004/02/skos/core#altLabel": "\u2606",
217
+ // ☆
218
+ "http://www.w3.org/2004/02/skos/core#definition": "def",
219
+ "http://www.w3.org/2004/02/skos/core#broader": "\u2191",
220
+ // ↑
221
+ "http://www.w3.org/2004/02/skos/core#narrower": "\u2193",
222
+ // ↓
223
+ "http://www.w3.org/2004/02/skos/core#related": "\u2194",
224
+ // ↔
225
+ "http://www.w3.org/2004/02/skos/core#note": "note",
226
+ "http://www.w3.org/2004/02/skos/core#exactMatch": "\u2261",
227
+ // ≡
228
+ "http://www.w3.org/2004/02/skos/core#closeMatch": "\u2248",
229
+ // ≈
230
+ // Dublin Core Terms
231
+ "http://purl.org/dc/terms/title": "ttl",
232
+ "http://purl.org/dc/terms/description": "dsc",
233
+ "http://purl.org/dc/terms/created": "crt",
234
+ "http://purl.org/dc/terms/modified": "mod",
235
+ "http://purl.org/dc/terms/creator": "by",
236
+ "http://purl.org/dc/terms/subject": "sbj",
237
+ // FOAF
238
+ "http://xmlns.com/foaf/0.1/name": "nm",
239
+ "http://xmlns.com/foaf/0.1/knows": "\u27F7",
240
+ // ⟷
241
+ "http://xmlns.com/foaf/0.1/member": "mbr",
242
+ // Schema.org
243
+ "http://schema.org/name": "nm",
244
+ "http://schema.org/description": "dsc"
245
+ };
246
+ function getPredicateIcon(predicateUri) {
247
+ return PREDICATE_ICONS[predicateUri];
248
+ }
249
+
181
250
  // src/transformers.ts
182
- function createNodeMap(triples, prefixMap, themeColors) {
251
+ var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
252
+ var SCHEMA_IMAGE = "https://schema.org/image";
253
+ var SCHEMA_ICON = "https://schema.org/icon";
254
+ var RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
255
+ var RDFS_SUBCLASSOF = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
256
+ var SUPPRESSED_PREDICATES = /* @__PURE__ */ new Set([SCHEMA_IMAGE, SCHEMA_ICON, RDFS_LABEL]);
257
+ function appendTooltipRows(title, rows) {
258
+ const closingTag = "</div>";
259
+ const idx = title.lastIndexOf(closingTag);
260
+ if (idx === -1) return title + rows;
261
+ return title.slice(0, idx) + rows + closingTag;
262
+ }
263
+ function buildVisualTooltipRow(key, value) {
264
+ return `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">${escapeHtml(key)}</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
265
+ }
266
+ function escapeHtml(str) {
267
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
268
+ }
269
+ function createCompactNodeTooltipHTML(uri, triples, prefixMap) {
270
+ const isBlankNode = uri.startsWith("_:");
271
+ let rows = `<div class="yasgui-tooltip-type">${isBlankNode ? "Blank Node" : "URI"}</div>`;
272
+ 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>`;
273
+ triples.filter((t) => t.subject === uri && t.predicate === RDF_TYPE).forEach((t) => {
274
+ const typeLabel = applyPrefix(t.object.value, prefixMap);
275
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">rdf:type</span><span class="yasgui-tooltip-val">${escapeHtml(typeLabel)}</span></div>`;
276
+ });
277
+ triples.filter((t) => t.subject === uri && t.object.type === "literal").forEach((t) => {
278
+ const predLabel = applyPrefix(t.predicate, prefixMap);
279
+ 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>`;
280
+ });
281
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
282
+ }
283
+ function createNodeTooltipHTML(nodeType, value, datatype, lang, prefixMap) {
284
+ const typeLabel = nodeType === "uri" ? "URI" : nodeType === "literal" ? "Literal" : "Blank Node";
285
+ let rows = `<div class="yasgui-tooltip-type">${typeLabel}</div>`;
286
+ if (nodeType === "literal") {
287
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Value</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
288
+ if (datatype) {
289
+ const dtLabel = prefixMap ? applyPrefix(datatype, prefixMap) : datatype;
290
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Datatype</span><span class="yasgui-tooltip-val">${escapeHtml(dtLabel)}</span></div>`;
291
+ }
292
+ if (lang) {
293
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Language</span><span class="yasgui-tooltip-val">${escapeHtml(lang)}</span></div>`;
294
+ }
295
+ } else if (nodeType === "uri") {
296
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Full URI</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
297
+ } else if (nodeType === "bnode") {
298
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Identifier</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
299
+ }
300
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
301
+ }
302
+ function createEdgeTooltipHTML(predicateUri) {
303
+ 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>`;
304
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
305
+ }
306
+ function createNodeMap(triples, prefixMap, themeColors, settings) {
183
307
  const nodeMap = /* @__PURE__ */ new Map();
184
308
  let nodeId = 1;
309
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
185
310
  triples.forEach((triple) => {
186
311
  if (!nodeMap.has(triple.subject)) {
187
312
  const isBlankNode = triple.subject.startsWith("_:");
@@ -193,11 +318,18 @@ function createNodeMap(triples, prefixMap, themeColors) {
193
318
  color: getNodeColor({ uri: triple.subject, type: "uri" }, triples, themeColors),
194
319
  type: "uri",
195
320
  fullValue: triple.subject,
196
- title: isBlankNode ? triple.subject : applyPrefix(triple.subject, prefixMap)
321
+ size: 10 * sizeMultiplier,
322
+ title: createNodeTooltipHTML(
323
+ isBlankNode ? "bnode" : "uri",
324
+ triple.subject,
325
+ void 0,
326
+ void 0,
327
+ prefixMap
328
+ )
197
329
  });
198
330
  }
199
331
  const objValue = triple.object.value;
200
- if (!nodeMap.has(objValue)) {
332
+ if (!nodeMap.has(objValue) && !SUPPRESSED_PREDICATES.has(triple.predicate)) {
201
333
  const isLiteral = triple.object.type === "literal";
202
334
  const isBlankNode = !isLiteral && objValue.startsWith("_:");
203
335
  let label;
@@ -206,15 +338,21 @@ function createNodeMap(triples, prefixMap, themeColors) {
206
338
  if (isLiteral) {
207
339
  label = truncateLabel(objValue);
208
340
  fullValue = objValue;
209
- title = triple.object.datatype ? `"${objValue}"^^${applyPrefix(triple.object.datatype, prefixMap)}` : `"${objValue}"`;
341
+ title = createNodeTooltipHTML(
342
+ "literal",
343
+ objValue,
344
+ triple.object.datatype,
345
+ triple.object.lang,
346
+ prefixMap
347
+ );
210
348
  } else if (isBlankNode) {
211
349
  label = objValue;
212
350
  fullValue = objValue;
213
- title = objValue;
351
+ title = createNodeTooltipHTML("bnode", objValue);
214
352
  } else {
215
353
  label = truncateLabel(applyPrefix(objValue, prefixMap));
216
354
  fullValue = objValue;
217
- title = applyPrefix(objValue, prefixMap);
355
+ title = createNodeTooltipHTML("uri", objValue, void 0, void 0, prefixMap);
218
356
  }
219
357
  nodeMap.set(objValue, {
220
358
  id: nodeId++,
@@ -227,39 +365,157 @@ function createNodeMap(triples, prefixMap, themeColors) {
227
365
  ),
228
366
  type: isLiteral ? "literal" : "uri",
229
367
  fullValue,
368
+ size: (isLiteral ? 5 : 10) * sizeMultiplier,
230
369
  title
231
370
  });
232
371
  }
233
372
  });
234
373
  return nodeMap;
235
374
  }
236
- function createEdgesArray(triples, nodeMap, prefixMap) {
375
+ function createEdgesArray(triples, nodeMap, prefixMap, settings) {
237
376
  const edges = [];
238
377
  const edgeSet = /* @__PURE__ */ new Set();
239
378
  triples.forEach((triple) => {
379
+ var _a, _b;
380
+ if (SUPPRESSED_PREDICATES.has(triple.predicate)) return;
240
381
  const fromNode = nodeMap.get(triple.subject);
241
382
  const toNode = nodeMap.get(triple.object.value);
242
383
  if (!fromNode || !toNode) return;
243
384
  const edgeKey = `${fromNode.id}-${triple.predicate}-${toNode.id}`;
244
385
  if (!edgeSet.has(edgeKey)) {
245
386
  edgeSet.add(edgeKey);
387
+ let edgeLabel;
388
+ const predicateDisplay = (_a = settings == null ? void 0 : settings.predicateDisplay) != null ? _a : "label";
389
+ if (predicateDisplay === "none") {
390
+ edgeLabel = "";
391
+ } else if (predicateDisplay === "icon") {
392
+ edgeLabel = (_b = getPredicateIcon(triple.predicate)) != null ? _b : truncateLabel(applyPrefix(triple.predicate, prefixMap));
393
+ } else {
394
+ edgeLabel = truncateLabel(applyPrefix(triple.predicate, prefixMap));
395
+ }
246
396
  edges.push({
247
397
  id: `edge_${fromNode.id}_${toNode.id}_${edges.length}`,
248
398
  from: fromNode.id,
249
399
  to: toNode.id,
250
- label: truncateLabel(applyPrefix(triple.predicate, prefixMap)),
400
+ label: edgeLabel,
251
401
  predicate: triple.predicate,
252
- title: applyPrefix(triple.predicate, prefixMap),
402
+ title: createEdgeTooltipHTML(triple.predicate),
253
403
  arrows: "to"
254
404
  });
255
405
  }
256
406
  });
257
407
  return edges;
258
408
  }
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());
409
+ function isNodeVisible(node, triples, settings) {
410
+ if (node.uri && node.uri.startsWith("_:")) {
411
+ return true;
412
+ }
413
+ if (!settings.compactMode) {
414
+ return true;
415
+ }
416
+ if (node.type === "literal") {
417
+ return false;
418
+ }
419
+ const isClass = triples.some(
420
+ (t) => t.predicate === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" && t.object.value === node.uri
421
+ );
422
+ if (isClass) {
423
+ return false;
424
+ }
425
+ return true;
426
+ }
427
+ function triplesToGraph(triples, prefixMap, themeColors, settings) {
428
+ const nodeMap = createNodeMap(triples, prefixMap, themeColors, settings);
429
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
430
+ const tripleIndex = /* @__PURE__ */ new Map();
431
+ for (const t of triples) {
432
+ if (!tripleIndex.has(t.subject)) tripleIndex.set(t.subject, /* @__PURE__ */ new Map());
433
+ const sp = tripleIndex.get(t.subject);
434
+ if (!sp.has(t.predicate)) sp.set(t.predicate, []);
435
+ sp.get(t.predicate).push(t.object.value);
436
+ }
437
+ function getNodeVisualIdx(uri) {
438
+ const sp = tripleIndex.get(uri);
439
+ if (!sp) return {};
440
+ const icons = sp.get(SCHEMA_ICON);
441
+ if (icons == null ? void 0 : icons.length) return { icon: icons[0] };
442
+ const images = sp.get(SCHEMA_IMAGE);
443
+ if (images == null ? void 0 : images.length) return { image: images[0] };
444
+ return {};
445
+ }
446
+ function resolveCompactVisualIdx(uri) {
447
+ var _a, _b, _c, _d;
448
+ const own = getNodeVisualIdx(uri);
449
+ if (own.icon || own.image) return own;
450
+ const typeUris = (_b = (_a = tripleIndex.get(uri)) == null ? void 0 : _a.get(RDF_TYPE)) != null ? _b : [];
451
+ for (const typeUri of typeUris) {
452
+ const cv = getNodeVisualIdx(typeUri);
453
+ if (cv.icon || cv.image) return cv;
454
+ }
455
+ for (const typeUri of typeUris) {
456
+ const superUris = (_d = (_c = tripleIndex.get(typeUri)) == null ? void 0 : _c.get(RDFS_SUBCLASSOF)) != null ? _d : [];
457
+ for (const superUri of superUris) {
458
+ const sv = getNodeVisualIdx(superUri);
459
+ if (sv.icon || sv.image) return sv;
460
+ }
461
+ }
462
+ return {};
463
+ }
464
+ if (settings == null ? void 0 : settings.compactMode) {
465
+ const subjects = new Set(triples.map((t) => t.subject));
466
+ subjects.forEach((subjectUri) => {
467
+ const node = nodeMap.get(subjectUri);
468
+ if (node) {
469
+ node.title = createCompactNodeTooltipHTML(subjectUri, triples, prefixMap);
470
+ }
471
+ });
472
+ }
473
+ nodeMap.forEach((node) => {
474
+ var _a, _b;
475
+ if (!node.uri || node.type === "literal") return;
476
+ const visual = (settings == null ? void 0 : settings.compactMode) ? resolveCompactVisualIdx(node.uri) : getNodeVisualIdx(node.uri);
477
+ const rdfsLabel = (_b = (_a = tripleIndex.get(node.uri)) == null ? void 0 : _a.get(RDFS_LABEL)) == null ? void 0 : _b[0];
478
+ if (visual.icon) {
479
+ node.shape = "text";
480
+ node.label = rdfsLabel ? `${visual.icon}
481
+ ${rdfsLabel}` : visual.icon;
482
+ node.font = { size: 14 * sizeMultiplier };
483
+ if (!(settings == null ? void 0 : settings.compactMode)) {
484
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Icon", visual.icon));
485
+ }
486
+ } else if (visual.image) {
487
+ let imageAllowed = false;
488
+ try {
489
+ const parsed = new URL(visual.image);
490
+ imageAllowed = parsed.protocol === "http:" || parsed.protocol === "https:";
491
+ } catch (e) {
492
+ }
493
+ if (imageAllowed) {
494
+ node.shape = "circularImage";
495
+ node.image = visual.image;
496
+ }
497
+ if (rdfsLabel) {
498
+ node.label = rdfsLabel;
499
+ node.font = { size: 14 * sizeMultiplier };
500
+ }
501
+ if (!(settings == null ? void 0 : settings.compactMode)) {
502
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Image", visual.image));
503
+ }
504
+ } else if (rdfsLabel) {
505
+ node.label = rdfsLabel;
506
+ }
507
+ if (rdfsLabel && !(settings == null ? void 0 : settings.compactMode)) {
508
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("rdfs:label", rdfsLabel));
509
+ }
510
+ });
511
+ const visibleNodeIds = /* @__PURE__ */ new Set();
512
+ nodeMap.forEach((node) => {
513
+ if (!settings || isNodeVisible(node, triples, settings)) {
514
+ visibleNodeIds.add(node.id);
515
+ }
516
+ });
517
+ const edges = createEdgesArray(triples, nodeMap, prefixMap, settings).filter((e) => visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to));
518
+ const nodes = Array.from(nodeMap.values()).filter((n) => visibleNodeIds.has(n.id));
263
519
  return { nodes, edges };
264
520
  }
265
521
 
@@ -29730,9 +29986,38 @@ function watchThemeChanges(callback) {
29730
29986
  return observer;
29731
29987
  }
29732
29988
 
29989
+ // src/settings.ts
29990
+ var DEFAULT_SETTINGS = {
29991
+ edgeStyle: "curved",
29992
+ compactMode: false,
29993
+ predicateDisplay: "icon",
29994
+ showNodeLabels: true,
29995
+ physicsEnabled: true,
29996
+ nodeSize: "medium"
29997
+ };
29998
+ var STORAGE_KEY = "yasgui-graph-plugin-settings";
29999
+ function loadSettings() {
30000
+ try {
30001
+ const stored = localStorage.getItem(STORAGE_KEY);
30002
+ if (stored) {
30003
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
30004
+ }
30005
+ } catch (e) {
30006
+ }
30007
+ return { ...DEFAULT_SETTINGS };
30008
+ }
30009
+ function saveSettings(settings) {
30010
+ try {
30011
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
30012
+ } catch (e) {
30013
+ }
30014
+ }
30015
+
29733
30016
  // src/GraphPlugin.ts
29734
30017
  var GraphPlugin = class {
29735
30018
  constructor(yasr) {
30019
+ this.settingsPanelOpen = false;
30020
+ this.clickOutsideHandler = null;
29736
30021
  this.yasr = yasr;
29737
30022
  this.network = null;
29738
30023
  this.currentTheme = getCurrentTheme();
@@ -29742,6 +30027,7 @@ var GraphPlugin = class {
29742
30027
  this.edgesDataSet = null;
29743
30028
  this.triples = null;
29744
30029
  this.prefixMap = null;
30030
+ this.settings = loadSettings();
29745
30031
  }
29746
30032
  /**
29747
30033
  * Plugin priority (higher = shown first in tabs)
@@ -29775,6 +30061,7 @@ var GraphPlugin = class {
29775
30061
  * Render the graph visualization
29776
30062
  */
29777
30063
  draw() {
30064
+ const wasPanelOpen = this.settingsPanelOpen;
29778
30065
  this.yasr.resultsEl.innerHTML = "";
29779
30066
  try {
29780
30067
  this.triples = parseConstructResults(this.yasr.results);
@@ -29791,24 +30078,45 @@ var GraphPlugin = class {
29791
30078
  this.prefixMap = extractPrefixes(this.yasr);
29792
30079
  this.currentTheme = getCurrentTheme();
29793
30080
  const themeColors = getThemeNodeColors(this.currentTheme);
29794
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30081
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29795
30082
  const container = document.createElement("div");
29796
30083
  container.className = "yasgui-graph-plugin-container";
29797
30084
  container.id = "yasgui-graph-plugin-container";
29798
30085
  this.yasr.resultsEl.appendChild(container);
29799
30086
  this.nodesDataSet = new DataSet(nodes);
29800
30087
  this.edgesDataSet = new DataSet(edges);
29801
- const options = getDefaultNetworkOptions(themeColors);
30088
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29802
30089
  this.network = new Network(
29803
30090
  container,
29804
30091
  { nodes: this.nodesDataSet, edges: this.edgesDataSet },
29805
30092
  options
29806
30093
  );
30094
+ this.setupHtmlTooltips(container);
29807
30095
  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);
30096
+ this.setupContainerResize(container);
30097
+ if (this.settings.physicsEnabled) {
30098
+ this.network.on("stabilizationIterationsDone", () => {
30099
+ this.network.setOptions({ physics: { enabled: true } });
30100
+ this.network.fit({ maxZoomLevel: 3 });
30101
+ });
30102
+ } else {
30103
+ setTimeout(() => {
30104
+ if (this.network) {
30105
+ this.network.fit({ maxZoomLevel: 3 });
30106
+ }
30107
+ }, 100);
30108
+ }
30109
+ this.network.on("dragEnd", (params) => {
30110
+ if (params.nodes.length > 0) {
30111
+ const positions = this.network.getPositions(params.nodes);
30112
+ const updates = params.nodes.map((id2) => ({
30113
+ id: id2,
30114
+ x: positions[id2].x,
30115
+ y: positions[id2].y,
30116
+ fixed: { x: true, y: true }
30117
+ }));
30118
+ this.nodesDataSet.update(updates);
30119
+ }
29812
30120
  });
29813
30121
  if (!this.themeObserver) {
29814
30122
  this.themeObserver = watchThemeChanges((newTheme) => {
@@ -29827,6 +30135,20 @@ var GraphPlugin = class {
29827
30135
  }
29828
30136
  };
29829
30137
  controls.appendChild(fitButton);
30138
+ const settingsButton = document.createElement("button");
30139
+ settingsButton.className = "yasgui-graph-button yasgui-graph-settings-button";
30140
+ settingsButton.setAttribute("aria-label", "Graph settings");
30141
+ settingsButton.innerHTML = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
30142
+ <path d="M8 10.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
30143
+ <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"/>
30144
+ </svg> Settings`;
30145
+ settingsButton.onclick = () => {
30146
+ this.toggleSettingsPanel(container);
30147
+ };
30148
+ controls.appendChild(settingsButton);
30149
+ if (wasPanelOpen) {
30150
+ this.toggleSettingsPanel(container);
30151
+ }
29830
30152
  } catch (error) {
29831
30153
  console.error("Error rendering graph:", error);
29832
30154
  const errorDiv = document.createElement("div");
@@ -29835,6 +30157,111 @@ var GraphPlugin = class {
29835
30157
  this.yasr.resultsEl.appendChild(errorDiv);
29836
30158
  }
29837
30159
  }
30160
+ /**
30161
+ * Setup custom HTML tooltip rendering for vis-network
30162
+ * @param container - The graph container element
30163
+ */
30164
+ setupHtmlTooltips(container) {
30165
+ if (!this.network) return;
30166
+ let hideTimeout = null;
30167
+ this.network.on("hoverNode", (params) => {
30168
+ if (hideTimeout) {
30169
+ clearTimeout(hideTimeout);
30170
+ hideTimeout = null;
30171
+ }
30172
+ const nodeId = params.node;
30173
+ const node = this.nodesDataSet.get(nodeId);
30174
+ if (node && node.title) {
30175
+ this.showHtmlTooltip(container, node.title, params.pointer.DOM);
30176
+ }
30177
+ });
30178
+ this.network.on("hoverEdge", (params) => {
30179
+ if (hideTimeout) {
30180
+ clearTimeout(hideTimeout);
30181
+ hideTimeout = null;
30182
+ }
30183
+ const edgeId = params.edge;
30184
+ const edge = this.edgesDataSet.get(edgeId);
30185
+ if (edge && edge.title) {
30186
+ this.showHtmlTooltip(container, edge.title, params.pointer.DOM);
30187
+ }
30188
+ });
30189
+ this.network.on("blurNode", () => {
30190
+ hideTimeout = window.setTimeout(() => {
30191
+ this.hideHtmlTooltipIfNotHovered(container);
30192
+ }, 200);
30193
+ });
30194
+ this.network.on("blurEdge", () => {
30195
+ hideTimeout = window.setTimeout(() => {
30196
+ this.hideHtmlTooltipIfNotHovered(container);
30197
+ }, 200);
30198
+ });
30199
+ this.network.on("dragStart", () => {
30200
+ if (hideTimeout) {
30201
+ clearTimeout(hideTimeout);
30202
+ hideTimeout = null;
30203
+ }
30204
+ this.hideHtmlTooltip(container);
30205
+ });
30206
+ this.network.on("zoom", () => {
30207
+ if (hideTimeout) {
30208
+ clearTimeout(hideTimeout);
30209
+ hideTimeout = null;
30210
+ }
30211
+ this.hideHtmlTooltip(container);
30212
+ });
30213
+ }
30214
+ /**
30215
+ * Show HTML tooltip at specified position
30216
+ * @param container - Container element
30217
+ * @param htmlContent - HTML content to display
30218
+ * @param position - Mouse position {x, y}
30219
+ */
30220
+ showHtmlTooltip(container, htmlContent, position) {
30221
+ this.hideHtmlTooltip(container);
30222
+ const tooltip = document.createElement("div");
30223
+ tooltip.className = "yasgui-graph-tooltip-container";
30224
+ tooltip.innerHTML = htmlContent;
30225
+ tooltip.style.position = "absolute";
30226
+ tooltip.style.left = `${position.x + 10}px`;
30227
+ tooltip.style.top = `${position.y + 10}px`;
30228
+ tooltip.style.zIndex = "1000";
30229
+ tooltip.addEventListener("mouseleave", () => {
30230
+ this.hideHtmlTooltip(container);
30231
+ });
30232
+ container.appendChild(tooltip);
30233
+ const rect = tooltip.getBoundingClientRect();
30234
+ const containerRect = container.getBoundingClientRect();
30235
+ if (rect.right > containerRect.right) {
30236
+ tooltip.style.left = `${position.x - rect.width - 10}px`;
30237
+ }
30238
+ if (rect.bottom > containerRect.bottom) {
30239
+ tooltip.style.top = `${position.y - rect.height - 10}px`;
30240
+ }
30241
+ }
30242
+ /**
30243
+ * Hide HTML tooltip
30244
+ * @param container - Container element
30245
+ */
30246
+ hideHtmlTooltip(container) {
30247
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30248
+ if (existingTooltip) {
30249
+ existingTooltip.remove();
30250
+ }
30251
+ }
30252
+ /**
30253
+ * Hide HTML tooltip only if mouse is not hovering over it
30254
+ * @param container - Container element
30255
+ */
30256
+ hideHtmlTooltipIfNotHovered(container) {
30257
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30258
+ if (existingTooltip) {
30259
+ const isHovered = existingTooltip.matches(":hover");
30260
+ if (!isHovered) {
30261
+ existingTooltip.remove();
30262
+ }
30263
+ }
30264
+ }
29838
30265
  /**
29839
30266
  * Apply theme to existing network
29840
30267
  * @param newTheme - 'light' or 'dark'
@@ -29845,12 +30272,30 @@ var GraphPlugin = class {
29845
30272
  }
29846
30273
  this.currentTheme = newTheme;
29847
30274
  const themeColors = getThemeNodeColors(newTheme);
29848
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30275
+ const fixedNodes = {};
30276
+ this.nodesDataSet.get().forEach((node) => {
30277
+ var _a, _b;
30278
+ 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);
30279
+ if (isFixed && node.x !== void 0 && node.y !== void 0) {
30280
+ fixedNodes[node.id] = { x: node.x, y: node.y };
30281
+ }
30282
+ });
30283
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29849
30284
  this.nodesDataSet.clear();
29850
30285
  this.nodesDataSet.add(nodes);
29851
30286
  this.edgesDataSet.clear();
29852
30287
  this.edgesDataSet.add(edges);
29853
- const options = getDefaultNetworkOptions(themeColors);
30288
+ const fixedIds = Object.keys(fixedNodes);
30289
+ if (fixedIds.length > 0) {
30290
+ const updates = fixedIds.map((id2) => ({
30291
+ id: id2,
30292
+ x: fixedNodes[id2].x,
30293
+ y: fixedNodes[id2].y,
30294
+ fixed: { x: true, y: true }
30295
+ }));
30296
+ this.nodesDataSet.update(updates);
30297
+ }
30298
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29854
30299
  this.network.setOptions(options);
29855
30300
  this.applyCanvasBackground(themeColors.background);
29856
30301
  }
@@ -29886,6 +30331,174 @@ var GraphPlugin = class {
29886
30331
  });
29887
30332
  this.resizeObserver.observe(parent2);
29888
30333
  }
30334
+ /**
30335
+ * Toggle the settings panel open/closed
30336
+ * @param container - The graph container element
30337
+ */
30338
+ toggleSettingsPanel(container) {
30339
+ const existing = container.querySelector(".yasgui-graph-settings-panel");
30340
+ if (existing) {
30341
+ existing.remove();
30342
+ this.settingsPanelOpen = false;
30343
+ this.removeClickOutsideHandler();
30344
+ } else {
30345
+ const panel = this.createSettingsPanel(container);
30346
+ container.appendChild(panel);
30347
+ this.settingsPanelOpen = true;
30348
+ this.setupClickOutsideHandler(container, panel);
30349
+ }
30350
+ }
30351
+ /**
30352
+ * Setup click-outside-to-close handler for settings panel
30353
+ * @param container - The graph container element
30354
+ * @param panel - The settings panel element
30355
+ */
30356
+ setupClickOutsideHandler(container, panel) {
30357
+ this.removeClickOutsideHandler();
30358
+ this.clickOutsideHandler = (event) => {
30359
+ const target = event.target;
30360
+ if (!panel.contains(target) && !this.isSettingsButton(target)) {
30361
+ this.toggleSettingsPanel(container);
30362
+ }
30363
+ };
30364
+ setTimeout(() => {
30365
+ document.addEventListener("click", this.clickOutsideHandler);
30366
+ }, 100);
30367
+ }
30368
+ /**
30369
+ * Remove the click-outside handler
30370
+ */
30371
+ removeClickOutsideHandler() {
30372
+ if (this.clickOutsideHandler) {
30373
+ document.removeEventListener("click", this.clickOutsideHandler);
30374
+ this.clickOutsideHandler = null;
30375
+ }
30376
+ }
30377
+ /**
30378
+ * Check if a node is the settings button or inside it
30379
+ * @param node - The node to check
30380
+ */
30381
+ isSettingsButton(node) {
30382
+ let current = node;
30383
+ while (current) {
30384
+ if (current instanceof Element && current.classList.contains("yasgui-graph-settings-button")) {
30385
+ return true;
30386
+ }
30387
+ current = current.parentNode;
30388
+ }
30389
+ return false;
30390
+ }
30391
+ /**
30392
+ * Build and return the settings panel element
30393
+ * @param container - The graph container element (used to re-draw on change)
30394
+ */
30395
+ createSettingsPanel(_container) {
30396
+ const panel = document.createElement("div");
30397
+ panel.className = "yasgui-graph-settings-panel";
30398
+ panel.setAttribute("role", "dialog");
30399
+ panel.setAttribute("aria-label", "Graph settings");
30400
+ const title = document.createElement("div");
30401
+ title.className = "yasgui-graph-settings-title";
30402
+ title.textContent = "Graph Settings";
30403
+ panel.appendChild(title);
30404
+ const addSection = (label) => {
30405
+ const h = document.createElement("div");
30406
+ h.className = "yasgui-graph-settings-section";
30407
+ h.textContent = label;
30408
+ panel.appendChild(h);
30409
+ };
30410
+ const addToggle = (label, checked, onChange) => {
30411
+ const row = document.createElement("label");
30412
+ row.className = "yasgui-graph-settings-row";
30413
+ const input = document.createElement("input");
30414
+ input.type = "checkbox";
30415
+ input.checked = checked;
30416
+ input.addEventListener("change", () => onChange(input.checked));
30417
+ const span = document.createElement("span");
30418
+ span.textContent = label;
30419
+ row.appendChild(input);
30420
+ row.appendChild(span);
30421
+ panel.appendChild(row);
30422
+ };
30423
+ const addSelect = (label, options, current, onChange) => {
30424
+ const row = document.createElement("div");
30425
+ row.className = "yasgui-graph-settings-row";
30426
+ const lbl = document.createElement("span");
30427
+ lbl.textContent = label;
30428
+ const sel = document.createElement("select");
30429
+ sel.className = "yasgui-graph-settings-select";
30430
+ options.forEach((o) => {
30431
+ const opt = document.createElement("option");
30432
+ opt.value = o.value;
30433
+ opt.textContent = o.label;
30434
+ if (o.value === current) opt.selected = true;
30435
+ sel.appendChild(opt);
30436
+ });
30437
+ sel.addEventListener("change", () => onChange(sel.value));
30438
+ row.appendChild(lbl);
30439
+ row.appendChild(sel);
30440
+ panel.appendChild(row);
30441
+ };
30442
+ const applyAndRedraw = () => {
30443
+ saveSettings(this.settings);
30444
+ this.draw();
30445
+ };
30446
+ addSection("Arrows");
30447
+ addSelect(
30448
+ "Style",
30449
+ [
30450
+ { value: "curved", label: "Curved" },
30451
+ { value: "straight", label: "Straight" }
30452
+ ],
30453
+ this.settings.edgeStyle,
30454
+ (v) => {
30455
+ this.settings.edgeStyle = v;
30456
+ applyAndRedraw();
30457
+ }
30458
+ );
30459
+ addSection("Predicate display");
30460
+ addSelect(
30461
+ "Display",
30462
+ [
30463
+ { value: "label", label: "Label (prefixed URI)" },
30464
+ { value: "icon", label: "Icon / symbol" },
30465
+ { value: "none", label: "Hidden" }
30466
+ ],
30467
+ this.settings.predicateDisplay,
30468
+ (v) => {
30469
+ this.settings.predicateDisplay = v;
30470
+ applyAndRedraw();
30471
+ }
30472
+ );
30473
+ addSection("Compact mode");
30474
+ addToggle("Compact mode", this.settings.compactMode, (v) => {
30475
+ this.settings.compactMode = v;
30476
+ applyAndRedraw();
30477
+ });
30478
+ addSection("Display");
30479
+ addToggle("Show node labels", this.settings.showNodeLabels, (v) => {
30480
+ this.settings.showNodeLabels = v;
30481
+ applyAndRedraw();
30482
+ });
30483
+ addToggle("Enable physics", this.settings.physicsEnabled, (v) => {
30484
+ this.settings.physicsEnabled = v;
30485
+ applyAndRedraw();
30486
+ });
30487
+ addSelect(
30488
+ "Node size",
30489
+ [
30490
+ { value: "small", label: "Small" },
30491
+ { value: "medium", label: "Medium" },
30492
+ { value: "large", label: "Large" }
30493
+ ],
30494
+ this.settings.nodeSize,
30495
+ (v) => {
30496
+ this.settings.nodeSize = v;
30497
+ applyAndRedraw();
30498
+ }
30499
+ );
30500
+ return panel;
30501
+ }
29889
30502
  /**
29890
30503
  * Get icon for plugin tab
29891
30504
  * @returns Icon element
@@ -29908,6 +30521,7 @@ var GraphPlugin = class {
29908
30521
  * Cleanup when plugin is destroyed
29909
30522
  */
29910
30523
  destroy() {
30524
+ this.removeClickOutsideHandler();
29911
30525
  if (this.themeObserver) {
29912
30526
  this.themeObserver.disconnect();
29913
30527
  this.themeObserver = null;