@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.
@@ -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
  },
@@ -152,10 +158,129 @@ function getNodeColor(node, triples, themeColors) {
152
158
  return themeColors.uri;
153
159
  }
154
160
 
161
+ // src/predicateIcons.ts
162
+ var PREDICATE_ICONS = {
163
+ // RDF core
164
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": "a",
165
+ // Turtle's "a" shorthand
166
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#value": "val",
167
+ // RDFS
168
+ "http://www.w3.org/2000/01/rdf-schema#label": "lbl",
169
+ "http://www.w3.org/2000/01/rdf-schema#comment": "cmt",
170
+ "http://www.w3.org/2000/01/rdf-schema#subClassOf": "\u2282",
171
+ // ⊂
172
+ "http://www.w3.org/2000/01/rdf-schema#subPropertyOf": "\u2286",
173
+ // ⊆
174
+ "http://www.w3.org/2000/01/rdf-schema#domain": "dom",
175
+ "http://www.w3.org/2000/01/rdf-schema#range": "rng",
176
+ "http://www.w3.org/2000/01/rdf-schema#seeAlso": "see",
177
+ "http://www.w3.org/2000/01/rdf-schema#isDefinedBy": "idb",
178
+ // OWL
179
+ "http://www.w3.org/2002/07/owl#sameAs": "\u2261",
180
+ // ≡
181
+ "http://www.w3.org/2002/07/owl#equivalentClass": "\u2245",
182
+ // ≅
183
+ "http://www.w3.org/2002/07/owl#inverseOf": "\u21C4",
184
+ // ⇄
185
+ "http://www.w3.org/2002/07/owl#disjointWith": "\u2260",
186
+ // ≠
187
+ // SKOS
188
+ "http://www.w3.org/2004/02/skos/core#prefLabel": "\u2605",
189
+ // ★
190
+ "http://www.w3.org/2004/02/skos/core#altLabel": "\u2606",
191
+ // ☆
192
+ "http://www.w3.org/2004/02/skos/core#definition": "def",
193
+ "http://www.w3.org/2004/02/skos/core#broader": "\u2191",
194
+ // ↑
195
+ "http://www.w3.org/2004/02/skos/core#narrower": "\u2193",
196
+ // ↓
197
+ "http://www.w3.org/2004/02/skos/core#related": "\u2194",
198
+ // ↔
199
+ "http://www.w3.org/2004/02/skos/core#note": "note",
200
+ "http://www.w3.org/2004/02/skos/core#exactMatch": "\u2261",
201
+ // ≡
202
+ "http://www.w3.org/2004/02/skos/core#closeMatch": "\u2248",
203
+ // ≈
204
+ // Dublin Core Terms
205
+ "http://purl.org/dc/terms/title": "ttl",
206
+ "http://purl.org/dc/terms/description": "dsc",
207
+ "http://purl.org/dc/terms/created": "crt",
208
+ "http://purl.org/dc/terms/modified": "mod",
209
+ "http://purl.org/dc/terms/creator": "by",
210
+ "http://purl.org/dc/terms/subject": "sbj",
211
+ // FOAF
212
+ "http://xmlns.com/foaf/0.1/name": "nm",
213
+ "http://xmlns.com/foaf/0.1/knows": "\u27F7",
214
+ // ⟷
215
+ "http://xmlns.com/foaf/0.1/member": "mbr",
216
+ // Schema.org
217
+ "http://schema.org/name": "nm",
218
+ "http://schema.org/description": "dsc"
219
+ };
220
+ function getPredicateIcon(predicateUri) {
221
+ return PREDICATE_ICONS[predicateUri];
222
+ }
223
+
155
224
  // src/transformers.ts
156
- function createNodeMap(triples, prefixMap, themeColors) {
225
+ var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
226
+ var SCHEMA_IMAGE = "https://schema.org/image";
227
+ var SCHEMA_ICON = "https://schema.org/icon";
228
+ var RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
229
+ var RDFS_SUBCLASSOF = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
230
+ var SUPPRESSED_PREDICATES = /* @__PURE__ */ new Set([SCHEMA_IMAGE, SCHEMA_ICON, RDFS_LABEL]);
231
+ function appendTooltipRows(title, rows) {
232
+ const closingTag = "</div>";
233
+ const idx = title.lastIndexOf(closingTag);
234
+ if (idx === -1) return title + rows;
235
+ return title.slice(0, idx) + rows + closingTag;
236
+ }
237
+ function buildVisualTooltipRow(key, value) {
238
+ return `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">${escapeHtml(key)}</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
239
+ }
240
+ function escapeHtml(str) {
241
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
242
+ }
243
+ function createCompactNodeTooltipHTML(uri, triples, prefixMap) {
244
+ const isBlankNode = uri.startsWith("_:");
245
+ let rows = `<div class="yasgui-tooltip-type">${isBlankNode ? "Blank Node" : "URI"}</div>`;
246
+ 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>`;
247
+ triples.filter((t) => t.subject === uri && t.predicate === RDF_TYPE).forEach((t) => {
248
+ const typeLabel = applyPrefix(t.object.value, prefixMap);
249
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">rdf:type</span><span class="yasgui-tooltip-val">${escapeHtml(typeLabel)}</span></div>`;
250
+ });
251
+ triples.filter((t) => t.subject === uri && t.object.type === "literal").forEach((t) => {
252
+ const predLabel = applyPrefix(t.predicate, prefixMap);
253
+ 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>`;
254
+ });
255
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
256
+ }
257
+ function createNodeTooltipHTML(nodeType, value, datatype, lang, prefixMap) {
258
+ const typeLabel = nodeType === "uri" ? "URI" : nodeType === "literal" ? "Literal" : "Blank Node";
259
+ let rows = `<div class="yasgui-tooltip-type">${typeLabel}</div>`;
260
+ if (nodeType === "literal") {
261
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Value</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
262
+ if (datatype) {
263
+ const dtLabel = prefixMap ? applyPrefix(datatype, prefixMap) : datatype;
264
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Datatype</span><span class="yasgui-tooltip-val">${escapeHtml(dtLabel)}</span></div>`;
265
+ }
266
+ if (lang) {
267
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Language</span><span class="yasgui-tooltip-val">${escapeHtml(lang)}</span></div>`;
268
+ }
269
+ } else if (nodeType === "uri") {
270
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Full URI</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
271
+ } else if (nodeType === "bnode") {
272
+ rows += `<div class="yasgui-tooltip-row"><span class="yasgui-tooltip-key">Identifier</span><span class="yasgui-tooltip-val">${escapeHtml(value)}</span></div>`;
273
+ }
274
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
275
+ }
276
+ function createEdgeTooltipHTML(predicateUri) {
277
+ 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>`;
278
+ return `<div class="yasgui-graph-tooltip">${rows}</div>`;
279
+ }
280
+ function createNodeMap(triples, prefixMap, themeColors, settings) {
157
281
  const nodeMap = /* @__PURE__ */ new Map();
158
282
  let nodeId = 1;
283
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
159
284
  triples.forEach((triple) => {
160
285
  if (!nodeMap.has(triple.subject)) {
161
286
  const isBlankNode = triple.subject.startsWith("_:");
@@ -167,11 +292,18 @@ function createNodeMap(triples, prefixMap, themeColors) {
167
292
  color: getNodeColor({ uri: triple.subject, type: "uri" }, triples, themeColors),
168
293
  type: "uri",
169
294
  fullValue: triple.subject,
170
- title: isBlankNode ? triple.subject : applyPrefix(triple.subject, prefixMap)
295
+ size: 10 * sizeMultiplier,
296
+ title: createNodeTooltipHTML(
297
+ isBlankNode ? "bnode" : "uri",
298
+ triple.subject,
299
+ void 0,
300
+ void 0,
301
+ prefixMap
302
+ )
171
303
  });
172
304
  }
173
305
  const objValue = triple.object.value;
174
- if (!nodeMap.has(objValue)) {
306
+ if (!nodeMap.has(objValue) && !SUPPRESSED_PREDICATES.has(triple.predicate)) {
175
307
  const isLiteral = triple.object.type === "literal";
176
308
  const isBlankNode = !isLiteral && objValue.startsWith("_:");
177
309
  let label;
@@ -180,15 +312,21 @@ function createNodeMap(triples, prefixMap, themeColors) {
180
312
  if (isLiteral) {
181
313
  label = truncateLabel(objValue);
182
314
  fullValue = objValue;
183
- title = triple.object.datatype ? `"${objValue}"^^${applyPrefix(triple.object.datatype, prefixMap)}` : `"${objValue}"`;
315
+ title = createNodeTooltipHTML(
316
+ "literal",
317
+ objValue,
318
+ triple.object.datatype,
319
+ triple.object.lang,
320
+ prefixMap
321
+ );
184
322
  } else if (isBlankNode) {
185
323
  label = objValue;
186
324
  fullValue = objValue;
187
- title = objValue;
325
+ title = createNodeTooltipHTML("bnode", objValue);
188
326
  } else {
189
327
  label = truncateLabel(applyPrefix(objValue, prefixMap));
190
328
  fullValue = objValue;
191
- title = applyPrefix(objValue, prefixMap);
329
+ title = createNodeTooltipHTML("uri", objValue, void 0, void 0, prefixMap);
192
330
  }
193
331
  nodeMap.set(objValue, {
194
332
  id: nodeId++,
@@ -201,39 +339,157 @@ function createNodeMap(triples, prefixMap, themeColors) {
201
339
  ),
202
340
  type: isLiteral ? "literal" : "uri",
203
341
  fullValue,
342
+ size: (isLiteral ? 5 : 10) * sizeMultiplier,
204
343
  title
205
344
  });
206
345
  }
207
346
  });
208
347
  return nodeMap;
209
348
  }
210
- function createEdgesArray(triples, nodeMap, prefixMap) {
349
+ function createEdgesArray(triples, nodeMap, prefixMap, settings) {
211
350
  const edges = [];
212
351
  const edgeSet = /* @__PURE__ */ new Set();
213
352
  triples.forEach((triple) => {
353
+ var _a, _b;
354
+ if (SUPPRESSED_PREDICATES.has(triple.predicate)) return;
214
355
  const fromNode = nodeMap.get(triple.subject);
215
356
  const toNode = nodeMap.get(triple.object.value);
216
357
  if (!fromNode || !toNode) return;
217
358
  const edgeKey = `${fromNode.id}-${triple.predicate}-${toNode.id}`;
218
359
  if (!edgeSet.has(edgeKey)) {
219
360
  edgeSet.add(edgeKey);
361
+ let edgeLabel;
362
+ const predicateDisplay = (_a = settings == null ? void 0 : settings.predicateDisplay) != null ? _a : "label";
363
+ if (predicateDisplay === "none") {
364
+ edgeLabel = "";
365
+ } else if (predicateDisplay === "icon") {
366
+ edgeLabel = (_b = getPredicateIcon(triple.predicate)) != null ? _b : truncateLabel(applyPrefix(triple.predicate, prefixMap));
367
+ } else {
368
+ edgeLabel = truncateLabel(applyPrefix(triple.predicate, prefixMap));
369
+ }
220
370
  edges.push({
221
371
  id: `edge_${fromNode.id}_${toNode.id}_${edges.length}`,
222
372
  from: fromNode.id,
223
373
  to: toNode.id,
224
- label: truncateLabel(applyPrefix(triple.predicate, prefixMap)),
374
+ label: edgeLabel,
225
375
  predicate: triple.predicate,
226
- title: applyPrefix(triple.predicate, prefixMap),
376
+ title: createEdgeTooltipHTML(triple.predicate),
227
377
  arrows: "to"
228
378
  });
229
379
  }
230
380
  });
231
381
  return edges;
232
382
  }
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());
383
+ function isNodeVisible(node, triples, settings) {
384
+ if (node.uri && node.uri.startsWith("_:")) {
385
+ return true;
386
+ }
387
+ if (!settings.compactMode) {
388
+ return true;
389
+ }
390
+ if (node.type === "literal") {
391
+ return false;
392
+ }
393
+ const isClass = triples.some(
394
+ (t) => t.predicate === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" && t.object.value === node.uri
395
+ );
396
+ if (isClass) {
397
+ return false;
398
+ }
399
+ return true;
400
+ }
401
+ function triplesToGraph(triples, prefixMap, themeColors, settings) {
402
+ const nodeMap = createNodeMap(triples, prefixMap, themeColors, settings);
403
+ const sizeMultiplier = (settings == null ? void 0 : settings.nodeSize) === "small" ? 0.5 : (settings == null ? void 0 : settings.nodeSize) === "large" ? 2 : 1;
404
+ const tripleIndex = /* @__PURE__ */ new Map();
405
+ for (const t of triples) {
406
+ if (!tripleIndex.has(t.subject)) tripleIndex.set(t.subject, /* @__PURE__ */ new Map());
407
+ const sp = tripleIndex.get(t.subject);
408
+ if (!sp.has(t.predicate)) sp.set(t.predicate, []);
409
+ sp.get(t.predicate).push(t.object.value);
410
+ }
411
+ function getNodeVisualIdx(uri) {
412
+ const sp = tripleIndex.get(uri);
413
+ if (!sp) return {};
414
+ const icons = sp.get(SCHEMA_ICON);
415
+ if (icons == null ? void 0 : icons.length) return { icon: icons[0] };
416
+ const images = sp.get(SCHEMA_IMAGE);
417
+ if (images == null ? void 0 : images.length) return { image: images[0] };
418
+ return {};
419
+ }
420
+ function resolveCompactVisualIdx(uri) {
421
+ var _a, _b, _c, _d;
422
+ const own = getNodeVisualIdx(uri);
423
+ if (own.icon || own.image) return own;
424
+ const typeUris = (_b = (_a = tripleIndex.get(uri)) == null ? void 0 : _a.get(RDF_TYPE)) != null ? _b : [];
425
+ for (const typeUri of typeUris) {
426
+ const cv = getNodeVisualIdx(typeUri);
427
+ if (cv.icon || cv.image) return cv;
428
+ }
429
+ for (const typeUri of typeUris) {
430
+ const superUris = (_d = (_c = tripleIndex.get(typeUri)) == null ? void 0 : _c.get(RDFS_SUBCLASSOF)) != null ? _d : [];
431
+ for (const superUri of superUris) {
432
+ const sv = getNodeVisualIdx(superUri);
433
+ if (sv.icon || sv.image) return sv;
434
+ }
435
+ }
436
+ return {};
437
+ }
438
+ if (settings == null ? void 0 : settings.compactMode) {
439
+ const subjects = new Set(triples.map((t) => t.subject));
440
+ subjects.forEach((subjectUri) => {
441
+ const node = nodeMap.get(subjectUri);
442
+ if (node) {
443
+ node.title = createCompactNodeTooltipHTML(subjectUri, triples, prefixMap);
444
+ }
445
+ });
446
+ }
447
+ nodeMap.forEach((node) => {
448
+ var _a, _b;
449
+ if (!node.uri || node.type === "literal") return;
450
+ const visual = (settings == null ? void 0 : settings.compactMode) ? resolveCompactVisualIdx(node.uri) : getNodeVisualIdx(node.uri);
451
+ const rdfsLabel = (_b = (_a = tripleIndex.get(node.uri)) == null ? void 0 : _a.get(RDFS_LABEL)) == null ? void 0 : _b[0];
452
+ if (visual.icon) {
453
+ node.shape = "text";
454
+ node.label = rdfsLabel ? `${visual.icon}
455
+ ${rdfsLabel}` : visual.icon;
456
+ node.font = { size: 14 * sizeMultiplier };
457
+ if (!(settings == null ? void 0 : settings.compactMode)) {
458
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Icon", visual.icon));
459
+ }
460
+ } else if (visual.image) {
461
+ let imageAllowed = false;
462
+ try {
463
+ const parsed = new URL(visual.image);
464
+ imageAllowed = parsed.protocol === "http:" || parsed.protocol === "https:";
465
+ } catch (e) {
466
+ }
467
+ if (imageAllowed) {
468
+ node.shape = "circularImage";
469
+ node.image = visual.image;
470
+ }
471
+ if (rdfsLabel) {
472
+ node.label = rdfsLabel;
473
+ node.font = { size: 14 * sizeMultiplier };
474
+ }
475
+ if (!(settings == null ? void 0 : settings.compactMode)) {
476
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("Image", visual.image));
477
+ }
478
+ } else if (rdfsLabel) {
479
+ node.label = rdfsLabel;
480
+ }
481
+ if (rdfsLabel && !(settings == null ? void 0 : settings.compactMode)) {
482
+ node.title = appendTooltipRows(node.title, buildVisualTooltipRow("rdfs:label", rdfsLabel));
483
+ }
484
+ });
485
+ const visibleNodeIds = /* @__PURE__ */ new Set();
486
+ nodeMap.forEach((node) => {
487
+ if (!settings || isNodeVisible(node, triples, settings)) {
488
+ visibleNodeIds.add(node.id);
489
+ }
490
+ });
491
+ const edges = createEdgesArray(triples, nodeMap, prefixMap, settings).filter((e) => visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to));
492
+ const nodes = Array.from(nodeMap.values()).filter((n) => visibleNodeIds.has(n.id));
237
493
  return { nodes, edges };
238
494
  }
239
495
 
@@ -29704,9 +29960,38 @@ function watchThemeChanges(callback) {
29704
29960
  return observer;
29705
29961
  }
29706
29962
 
29963
+ // src/settings.ts
29964
+ var DEFAULT_SETTINGS = {
29965
+ edgeStyle: "curved",
29966
+ compactMode: false,
29967
+ predicateDisplay: "icon",
29968
+ showNodeLabels: true,
29969
+ physicsEnabled: true,
29970
+ nodeSize: "medium"
29971
+ };
29972
+ var STORAGE_KEY = "yasgui-graph-plugin-settings";
29973
+ function loadSettings() {
29974
+ try {
29975
+ const stored = localStorage.getItem(STORAGE_KEY);
29976
+ if (stored) {
29977
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
29978
+ }
29979
+ } catch (e) {
29980
+ }
29981
+ return { ...DEFAULT_SETTINGS };
29982
+ }
29983
+ function saveSettings(settings) {
29984
+ try {
29985
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
29986
+ } catch (e) {
29987
+ }
29988
+ }
29989
+
29707
29990
  // src/GraphPlugin.ts
29708
29991
  var GraphPlugin = class {
29709
29992
  constructor(yasr) {
29993
+ this.settingsPanelOpen = false;
29994
+ this.clickOutsideHandler = null;
29710
29995
  this.yasr = yasr;
29711
29996
  this.network = null;
29712
29997
  this.currentTheme = getCurrentTheme();
@@ -29716,6 +30001,7 @@ var GraphPlugin = class {
29716
30001
  this.edgesDataSet = null;
29717
30002
  this.triples = null;
29718
30003
  this.prefixMap = null;
30004
+ this.settings = loadSettings();
29719
30005
  }
29720
30006
  /**
29721
30007
  * Plugin priority (higher = shown first in tabs)
@@ -29749,6 +30035,7 @@ var GraphPlugin = class {
29749
30035
  * Render the graph visualization
29750
30036
  */
29751
30037
  draw() {
30038
+ const wasPanelOpen = this.settingsPanelOpen;
29752
30039
  this.yasr.resultsEl.innerHTML = "";
29753
30040
  try {
29754
30041
  this.triples = parseConstructResults(this.yasr.results);
@@ -29765,24 +30052,45 @@ var GraphPlugin = class {
29765
30052
  this.prefixMap = extractPrefixes(this.yasr);
29766
30053
  this.currentTheme = getCurrentTheme();
29767
30054
  const themeColors = getThemeNodeColors(this.currentTheme);
29768
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30055
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29769
30056
  const container = document.createElement("div");
29770
30057
  container.className = "yasgui-graph-plugin-container";
29771
30058
  container.id = "yasgui-graph-plugin-container";
29772
30059
  this.yasr.resultsEl.appendChild(container);
29773
30060
  this.nodesDataSet = new DataSet(nodes);
29774
30061
  this.edgesDataSet = new DataSet(edges);
29775
- const options = getDefaultNetworkOptions(themeColors);
30062
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29776
30063
  this.network = new Network(
29777
30064
  container,
29778
30065
  { nodes: this.nodesDataSet, edges: this.edgesDataSet },
29779
30066
  options
29780
30067
  );
30068
+ this.setupHtmlTooltips(container);
29781
30069
  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);
30070
+ this.setupContainerResize(container);
30071
+ if (this.settings.physicsEnabled) {
30072
+ this.network.on("stabilizationIterationsDone", () => {
30073
+ this.network.setOptions({ physics: { enabled: true } });
30074
+ this.network.fit({ maxZoomLevel: 3 });
30075
+ });
30076
+ } else {
30077
+ setTimeout(() => {
30078
+ if (this.network) {
30079
+ this.network.fit({ maxZoomLevel: 3 });
30080
+ }
30081
+ }, 100);
30082
+ }
30083
+ this.network.on("dragEnd", (params) => {
30084
+ if (params.nodes.length > 0) {
30085
+ const positions = this.network.getPositions(params.nodes);
30086
+ const updates = params.nodes.map((id2) => ({
30087
+ id: id2,
30088
+ x: positions[id2].x,
30089
+ y: positions[id2].y,
30090
+ fixed: { x: true, y: true }
30091
+ }));
30092
+ this.nodesDataSet.update(updates);
30093
+ }
29786
30094
  });
29787
30095
  if (!this.themeObserver) {
29788
30096
  this.themeObserver = watchThemeChanges((newTheme) => {
@@ -29801,6 +30109,20 @@ var GraphPlugin = class {
29801
30109
  }
29802
30110
  };
29803
30111
  controls.appendChild(fitButton);
30112
+ const settingsButton = document.createElement("button");
30113
+ settingsButton.className = "yasgui-graph-button yasgui-graph-settings-button";
30114
+ settingsButton.setAttribute("aria-label", "Graph settings");
30115
+ settingsButton.innerHTML = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
30116
+ <path d="M8 10.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
30117
+ <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"/>
30118
+ </svg> Settings`;
30119
+ settingsButton.onclick = () => {
30120
+ this.toggleSettingsPanel(container);
30121
+ };
30122
+ controls.appendChild(settingsButton);
30123
+ if (wasPanelOpen) {
30124
+ this.toggleSettingsPanel(container);
30125
+ }
29804
30126
  } catch (error) {
29805
30127
  console.error("Error rendering graph:", error);
29806
30128
  const errorDiv = document.createElement("div");
@@ -29809,6 +30131,111 @@ var GraphPlugin = class {
29809
30131
  this.yasr.resultsEl.appendChild(errorDiv);
29810
30132
  }
29811
30133
  }
30134
+ /**
30135
+ * Setup custom HTML tooltip rendering for vis-network
30136
+ * @param container - The graph container element
30137
+ */
30138
+ setupHtmlTooltips(container) {
30139
+ if (!this.network) return;
30140
+ let hideTimeout = null;
30141
+ this.network.on("hoverNode", (params) => {
30142
+ if (hideTimeout) {
30143
+ clearTimeout(hideTimeout);
30144
+ hideTimeout = null;
30145
+ }
30146
+ const nodeId = params.node;
30147
+ const node = this.nodesDataSet.get(nodeId);
30148
+ if (node && node.title) {
30149
+ this.showHtmlTooltip(container, node.title, params.pointer.DOM);
30150
+ }
30151
+ });
30152
+ this.network.on("hoverEdge", (params) => {
30153
+ if (hideTimeout) {
30154
+ clearTimeout(hideTimeout);
30155
+ hideTimeout = null;
30156
+ }
30157
+ const edgeId = params.edge;
30158
+ const edge = this.edgesDataSet.get(edgeId);
30159
+ if (edge && edge.title) {
30160
+ this.showHtmlTooltip(container, edge.title, params.pointer.DOM);
30161
+ }
30162
+ });
30163
+ this.network.on("blurNode", () => {
30164
+ hideTimeout = window.setTimeout(() => {
30165
+ this.hideHtmlTooltipIfNotHovered(container);
30166
+ }, 200);
30167
+ });
30168
+ this.network.on("blurEdge", () => {
30169
+ hideTimeout = window.setTimeout(() => {
30170
+ this.hideHtmlTooltipIfNotHovered(container);
30171
+ }, 200);
30172
+ });
30173
+ this.network.on("dragStart", () => {
30174
+ if (hideTimeout) {
30175
+ clearTimeout(hideTimeout);
30176
+ hideTimeout = null;
30177
+ }
30178
+ this.hideHtmlTooltip(container);
30179
+ });
30180
+ this.network.on("zoom", () => {
30181
+ if (hideTimeout) {
30182
+ clearTimeout(hideTimeout);
30183
+ hideTimeout = null;
30184
+ }
30185
+ this.hideHtmlTooltip(container);
30186
+ });
30187
+ }
30188
+ /**
30189
+ * Show HTML tooltip at specified position
30190
+ * @param container - Container element
30191
+ * @param htmlContent - HTML content to display
30192
+ * @param position - Mouse position {x, y}
30193
+ */
30194
+ showHtmlTooltip(container, htmlContent, position) {
30195
+ this.hideHtmlTooltip(container);
30196
+ const tooltip = document.createElement("div");
30197
+ tooltip.className = "yasgui-graph-tooltip-container";
30198
+ tooltip.innerHTML = htmlContent;
30199
+ tooltip.style.position = "absolute";
30200
+ tooltip.style.left = `${position.x + 10}px`;
30201
+ tooltip.style.top = `${position.y + 10}px`;
30202
+ tooltip.style.zIndex = "1000";
30203
+ tooltip.addEventListener("mouseleave", () => {
30204
+ this.hideHtmlTooltip(container);
30205
+ });
30206
+ container.appendChild(tooltip);
30207
+ const rect = tooltip.getBoundingClientRect();
30208
+ const containerRect = container.getBoundingClientRect();
30209
+ if (rect.right > containerRect.right) {
30210
+ tooltip.style.left = `${position.x - rect.width - 10}px`;
30211
+ }
30212
+ if (rect.bottom > containerRect.bottom) {
30213
+ tooltip.style.top = `${position.y - rect.height - 10}px`;
30214
+ }
30215
+ }
30216
+ /**
30217
+ * Hide HTML tooltip
30218
+ * @param container - Container element
30219
+ */
30220
+ hideHtmlTooltip(container) {
30221
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30222
+ if (existingTooltip) {
30223
+ existingTooltip.remove();
30224
+ }
30225
+ }
30226
+ /**
30227
+ * Hide HTML tooltip only if mouse is not hovering over it
30228
+ * @param container - Container element
30229
+ */
30230
+ hideHtmlTooltipIfNotHovered(container) {
30231
+ const existingTooltip = container.querySelector(".yasgui-graph-tooltip-container");
30232
+ if (existingTooltip) {
30233
+ const isHovered = existingTooltip.matches(":hover");
30234
+ if (!isHovered) {
30235
+ existingTooltip.remove();
30236
+ }
30237
+ }
30238
+ }
29812
30239
  /**
29813
30240
  * Apply theme to existing network
29814
30241
  * @param newTheme - 'light' or 'dark'
@@ -29819,12 +30246,30 @@ var GraphPlugin = class {
29819
30246
  }
29820
30247
  this.currentTheme = newTheme;
29821
30248
  const themeColors = getThemeNodeColors(newTheme);
29822
- const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors);
30249
+ const fixedNodes = {};
30250
+ this.nodesDataSet.get().forEach((node) => {
30251
+ var _a, _b;
30252
+ 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);
30253
+ if (isFixed && node.x !== void 0 && node.y !== void 0) {
30254
+ fixedNodes[node.id] = { x: node.x, y: node.y };
30255
+ }
30256
+ });
30257
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
29823
30258
  this.nodesDataSet.clear();
29824
30259
  this.nodesDataSet.add(nodes);
29825
30260
  this.edgesDataSet.clear();
29826
30261
  this.edgesDataSet.add(edges);
29827
- const options = getDefaultNetworkOptions(themeColors);
30262
+ const fixedIds = Object.keys(fixedNodes);
30263
+ if (fixedIds.length > 0) {
30264
+ const updates = fixedIds.map((id2) => ({
30265
+ id: id2,
30266
+ x: fixedNodes[id2].x,
30267
+ y: fixedNodes[id2].y,
30268
+ fixed: { x: true, y: true }
30269
+ }));
30270
+ this.nodesDataSet.update(updates);
30271
+ }
30272
+ const options = getDefaultNetworkOptions(themeColors, this.settings);
29828
30273
  this.network.setOptions(options);
29829
30274
  this.applyCanvasBackground(themeColors.background);
29830
30275
  }
@@ -29860,6 +30305,174 @@ var GraphPlugin = class {
29860
30305
  });
29861
30306
  this.resizeObserver.observe(parent2);
29862
30307
  }
30308
+ /**
30309
+ * Toggle the settings panel open/closed
30310
+ * @param container - The graph container element
30311
+ */
30312
+ toggleSettingsPanel(container) {
30313
+ const existing = container.querySelector(".yasgui-graph-settings-panel");
30314
+ if (existing) {
30315
+ existing.remove();
30316
+ this.settingsPanelOpen = false;
30317
+ this.removeClickOutsideHandler();
30318
+ } else {
30319
+ const panel = this.createSettingsPanel(container);
30320
+ container.appendChild(panel);
30321
+ this.settingsPanelOpen = true;
30322
+ this.setupClickOutsideHandler(container, panel);
30323
+ }
30324
+ }
30325
+ /**
30326
+ * Setup click-outside-to-close handler for settings panel
30327
+ * @param container - The graph container element
30328
+ * @param panel - The settings panel element
30329
+ */
30330
+ setupClickOutsideHandler(container, panel) {
30331
+ this.removeClickOutsideHandler();
30332
+ this.clickOutsideHandler = (event) => {
30333
+ const target = event.target;
30334
+ if (!panel.contains(target) && !this.isSettingsButton(target)) {
30335
+ this.toggleSettingsPanel(container);
30336
+ }
30337
+ };
30338
+ setTimeout(() => {
30339
+ document.addEventListener("click", this.clickOutsideHandler);
30340
+ }, 100);
30341
+ }
30342
+ /**
30343
+ * Remove the click-outside handler
30344
+ */
30345
+ removeClickOutsideHandler() {
30346
+ if (this.clickOutsideHandler) {
30347
+ document.removeEventListener("click", this.clickOutsideHandler);
30348
+ this.clickOutsideHandler = null;
30349
+ }
30350
+ }
30351
+ /**
30352
+ * Check if a node is the settings button or inside it
30353
+ * @param node - The node to check
30354
+ */
30355
+ isSettingsButton(node) {
30356
+ let current = node;
30357
+ while (current) {
30358
+ if (current instanceof Element && current.classList.contains("yasgui-graph-settings-button")) {
30359
+ return true;
30360
+ }
30361
+ current = current.parentNode;
30362
+ }
30363
+ return false;
30364
+ }
30365
+ /**
30366
+ * Build and return the settings panel element
30367
+ * @param container - The graph container element (used to re-draw on change)
30368
+ */
30369
+ createSettingsPanel(_container) {
30370
+ const panel = document.createElement("div");
30371
+ panel.className = "yasgui-graph-settings-panel";
30372
+ panel.setAttribute("role", "dialog");
30373
+ panel.setAttribute("aria-label", "Graph settings");
30374
+ const title = document.createElement("div");
30375
+ title.className = "yasgui-graph-settings-title";
30376
+ title.textContent = "Graph Settings";
30377
+ panel.appendChild(title);
30378
+ const addSection = (label) => {
30379
+ const h = document.createElement("div");
30380
+ h.className = "yasgui-graph-settings-section";
30381
+ h.textContent = label;
30382
+ panel.appendChild(h);
30383
+ };
30384
+ const addToggle = (label, checked, onChange) => {
30385
+ const row = document.createElement("label");
30386
+ row.className = "yasgui-graph-settings-row";
30387
+ const input = document.createElement("input");
30388
+ input.type = "checkbox";
30389
+ input.checked = checked;
30390
+ input.addEventListener("change", () => onChange(input.checked));
30391
+ const span = document.createElement("span");
30392
+ span.textContent = label;
30393
+ row.appendChild(input);
30394
+ row.appendChild(span);
30395
+ panel.appendChild(row);
30396
+ };
30397
+ const addSelect = (label, options, current, onChange) => {
30398
+ const row = document.createElement("div");
30399
+ row.className = "yasgui-graph-settings-row";
30400
+ const lbl = document.createElement("span");
30401
+ lbl.textContent = label;
30402
+ const sel = document.createElement("select");
30403
+ sel.className = "yasgui-graph-settings-select";
30404
+ options.forEach((o) => {
30405
+ const opt = document.createElement("option");
30406
+ opt.value = o.value;
30407
+ opt.textContent = o.label;
30408
+ if (o.value === current) opt.selected = true;
30409
+ sel.appendChild(opt);
30410
+ });
30411
+ sel.addEventListener("change", () => onChange(sel.value));
30412
+ row.appendChild(lbl);
30413
+ row.appendChild(sel);
30414
+ panel.appendChild(row);
30415
+ };
30416
+ const applyAndRedraw = () => {
30417
+ saveSettings(this.settings);
30418
+ this.draw();
30419
+ };
30420
+ addSection("Arrows");
30421
+ addSelect(
30422
+ "Style",
30423
+ [
30424
+ { value: "curved", label: "Curved" },
30425
+ { value: "straight", label: "Straight" }
30426
+ ],
30427
+ this.settings.edgeStyle,
30428
+ (v) => {
30429
+ this.settings.edgeStyle = v;
30430
+ applyAndRedraw();
30431
+ }
30432
+ );
30433
+ addSection("Predicate display");
30434
+ addSelect(
30435
+ "Display",
30436
+ [
30437
+ { value: "label", label: "Label (prefixed URI)" },
30438
+ { value: "icon", label: "Icon / symbol" },
30439
+ { value: "none", label: "Hidden" }
30440
+ ],
30441
+ this.settings.predicateDisplay,
30442
+ (v) => {
30443
+ this.settings.predicateDisplay = v;
30444
+ applyAndRedraw();
30445
+ }
30446
+ );
30447
+ addSection("Compact mode");
30448
+ addToggle("Compact mode", this.settings.compactMode, (v) => {
30449
+ this.settings.compactMode = v;
30450
+ applyAndRedraw();
30451
+ });
30452
+ addSection("Display");
30453
+ addToggle("Show node labels", this.settings.showNodeLabels, (v) => {
30454
+ this.settings.showNodeLabels = v;
30455
+ applyAndRedraw();
30456
+ });
30457
+ addToggle("Enable physics", this.settings.physicsEnabled, (v) => {
30458
+ this.settings.physicsEnabled = v;
30459
+ applyAndRedraw();
30460
+ });
30461
+ addSelect(
30462
+ "Node size",
30463
+ [
30464
+ { value: "small", label: "Small" },
30465
+ { value: "medium", label: "Medium" },
30466
+ { value: "large", label: "Large" }
30467
+ ],
30468
+ this.settings.nodeSize,
30469
+ (v) => {
30470
+ this.settings.nodeSize = v;
30471
+ applyAndRedraw();
30472
+ }
30473
+ );
30474
+ return panel;
30475
+ }
29863
30476
  /**
29864
30477
  * Get icon for plugin tab
29865
30478
  * @returns Icon element
@@ -29882,6 +30495,7 @@ var GraphPlugin = class {
29882
30495
  * Cleanup when plugin is destroyed
29883
30496
  */
29884
30497
  destroy() {
30498
+ this.removeClickOutsideHandler();
29885
30499
  if (this.themeObserver) {
29886
30500
  this.themeObserver.disconnect();
29887
30501
  this.themeObserver = null;