@matdata/yasgui-graph-plugin 1.5.0 → 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.
@@ -116,6 +116,40 @@ function getDefaultNetworkOptions(themeColors, settings) {
116
116
  }
117
117
 
118
118
  // src/parsers.ts
119
+ async function parseBackgroundQueryResponse(response) {
120
+ var _a, _b;
121
+ if (!response) return [];
122
+ try {
123
+ let data2;
124
+ if (typeof response.json === "function") {
125
+ data2 = await response.json();
126
+ } else if (typeof response.data === "string") {
127
+ data2 = JSON.parse(response.data);
128
+ } else if (typeof response === "object") {
129
+ data2 = response;
130
+ } else {
131
+ return [];
132
+ }
133
+ const bindings = (_b = (_a = data2 == null ? void 0 : data2.results) == null ? void 0 : _a.bindings) != null ? _b : [];
134
+ const triples = [];
135
+ for (const binding of bindings) {
136
+ if (!binding.subject || !binding.predicate || !binding.object || typeof binding.subject.value !== "string" || typeof binding.predicate.value !== "string" || typeof binding.object.value !== "string") continue;
137
+ triples.push({
138
+ subject: binding.subject.value,
139
+ predicate: binding.predicate.value,
140
+ object: {
141
+ value: binding.object.value,
142
+ type: binding.object.type || "uri",
143
+ datatype: binding.object.datatype,
144
+ lang: binding.object["xml:lang"]
145
+ }
146
+ });
147
+ }
148
+ return triples;
149
+ } catch (e) {
150
+ return [];
151
+ }
152
+ }
119
153
  function parseConstructResults(yasrResults) {
120
154
  const triples = [];
121
155
  if (!yasrResults || !yasrResults.getBindings) {
@@ -29988,10 +30022,16 @@ function saveSettings(settings) {
29988
30022
  }
29989
30023
 
29990
30024
  // src/GraphPlugin.ts
30025
+ var LOADING_BORDER_WIDTH = 4;
30026
+ var LOADING_BORDER_COLOR = "#ffa500";
30027
+ var EXPANDED_BORDER_WIDTH = 3;
30028
+ var DEFAULT_BORDER_WIDTH = 2;
29991
30029
  var GraphPlugin = class {
29992
30030
  constructor(yasr) {
29993
30031
  this.settingsPanelOpen = false;
29994
30032
  this.clickOutsideHandler = null;
30033
+ this.expansionAbortController = null;
30034
+ this.uriToNodeId = /* @__PURE__ */ new Map();
29995
30035
  this.yasr = yasr;
29996
30036
  this.network = null;
29997
30037
  this.currentTheme = getCurrentTheme();
@@ -30015,6 +30055,12 @@ var GraphPlugin = class {
30015
30055
  static get label() {
30016
30056
  return "Graph";
30017
30057
  }
30058
+ /**
30059
+ * Help/documentation URL
30060
+ */
30061
+ static get helpReference() {
30062
+ return "https://yasgui-doc.matdata.eu/docs/user-guide#graph-plugin";
30063
+ }
30018
30064
  /**
30019
30065
  * Check if plugin can handle the current results
30020
30066
  * @returns True if results are from CONSTRUCT or DESCRIBE query
@@ -30036,6 +30082,12 @@ var GraphPlugin = class {
30036
30082
  */
30037
30083
  draw() {
30038
30084
  const wasPanelOpen = this.settingsPanelOpen;
30085
+ if (this.expansionAbortController) {
30086
+ this.expansionAbortController.abort();
30087
+ this.expansionAbortController = null;
30088
+ }
30089
+ this.expandedNodes = /* @__PURE__ */ new Set();
30090
+ this.uriToNodeId = /* @__PURE__ */ new Map();
30039
30091
  this.yasr.resultsEl.innerHTML = "";
30040
30092
  try {
30041
30093
  this.triples = parseConstructResults(this.yasr.results);
@@ -30092,6 +30144,13 @@ var GraphPlugin = class {
30092
30144
  this.nodesDataSet.update(updates);
30093
30145
  }
30094
30146
  });
30147
+ this.setupNodeExpansion();
30148
+ this.uriToNodeId = /* @__PURE__ */ new Map();
30149
+ this.nodesDataSet.get().forEach((node) => {
30150
+ if (node.uri) {
30151
+ this.uriToNodeId.set(node.uri, node.id);
30152
+ }
30153
+ });
30095
30154
  if (!this.themeObserver) {
30096
30155
  this.themeObserver = watchThemeChanges((newTheme) => {
30097
30156
  this.applyTheme(newTheme);
@@ -30474,7 +30533,119 @@ var GraphPlugin = class {
30474
30533
  return panel;
30475
30534
  }
30476
30535
  /**
30477
- * Get icon for plugin tab
30536
+ * Setup double-click handler for node expansion
30537
+ */
30538
+ setupNodeExpansion() {
30539
+ if (!this.network) return;
30540
+ this.network.on("doubleClick", (params) => {
30541
+ if (params.nodes.length === 0) return;
30542
+ const nodeId = params.nodes[0];
30543
+ const node = this.nodesDataSet.get(nodeId);
30544
+ if (node && node.uri && !node.uri.startsWith("_:")) {
30545
+ this.expandNode(node.uri);
30546
+ }
30547
+ });
30548
+ }
30549
+ /**
30550
+ * Expand a node by executing a DESCRIBE query for the given URI and
30551
+ * merging the returned triples into the existing graph.
30552
+ * @param uri - URI of the node to expand
30553
+ */
30554
+ async expandNode(uri) {
30555
+ if (!this.yasr.executeQuery) {
30556
+ console.warn("yasgui-graph-plugin: background query execution not available (yasr.executeQuery is not configured)");
30557
+ return;
30558
+ }
30559
+ if (!this.triples || !this.prefixMap) return;
30560
+ if (this.expansionAbortController) {
30561
+ this.expansionAbortController.abort();
30562
+ }
30563
+ const controller = new AbortController();
30564
+ this.expansionAbortController = controller;
30565
+ const nodeId = this.uriToNodeId.get(uri);
30566
+ let originalColor = void 0;
30567
+ let originalBorderWidth = void 0;
30568
+ if (nodeId !== void 0) {
30569
+ const node = this.nodesDataSet.get(nodeId);
30570
+ if (node) {
30571
+ originalColor = node.color;
30572
+ originalBorderWidth = node.borderWidth;
30573
+ }
30574
+ this.nodesDataSet.update({
30575
+ id: nodeId,
30576
+ borderWidth: LOADING_BORDER_WIDTH,
30577
+ color: typeof originalColor === "object" && originalColor !== null ? { ...originalColor, border: LOADING_BORDER_COLOR } : { border: LOADING_BORDER_COLOR, background: originalColor != null ? originalColor : void 0 }
30578
+ });
30579
+ }
30580
+ const restoreNode = (borderWidth) => {
30581
+ if (nodeId !== void 0) {
30582
+ this.nodesDataSet.update({ id: nodeId, borderWidth, color: originalColor });
30583
+ }
30584
+ };
30585
+ try {
30586
+ const response = await this.yasr.executeQuery(`DESCRIBE <${uri}>`, {
30587
+ acceptHeader: "application/sparql-results+json",
30588
+ signal: controller.signal
30589
+ });
30590
+ if (controller.signal.aborted) {
30591
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30592
+ return;
30593
+ }
30594
+ const newTriples = await parseBackgroundQueryResponse(response);
30595
+ if (controller.signal.aborted) {
30596
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30597
+ return;
30598
+ }
30599
+ const existingKeys = new Set(
30600
+ this.triples.map((t) => `${t.subject}|${t.predicate}|${t.object.value}`)
30601
+ );
30602
+ const uniqueNew = newTriples.filter(
30603
+ (t) => !existingKeys.has(`${t.subject}|${t.predicate}|${t.object.value}`)
30604
+ );
30605
+ if (uniqueNew.length > 0) {
30606
+ this.triples = [...this.triples, ...uniqueNew];
30607
+ this.mergeNewTriples();
30608
+ }
30609
+ restoreNode(EXPANDED_BORDER_WIDTH);
30610
+ } catch (error) {
30611
+ if ((error == null ? void 0 : error.name) === "AbortError") {
30612
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30613
+ return;
30614
+ }
30615
+ console.error("yasgui-graph-plugin: error expanding node", uri, error);
30616
+ restoreNode(originalBorderWidth != null ? originalBorderWidth : DEFAULT_BORDER_WIDTH);
30617
+ }
30618
+ }
30619
+ /**
30620
+ * Incrementally add new triples to the vis-network DataSets without a full redraw.
30621
+ * New nodes and edges are added; existing ones are left untouched.
30622
+ * Expects `this.triples` to already include the new triples.
30623
+ */
30624
+ mergeNewTriples() {
30625
+ if (!this.triples || !this.prefixMap || !this.nodesDataSet || !this.edgesDataSet) return;
30626
+ const themeColors = getThemeNodeColors(this.currentTheme);
30627
+ const { nodes, edges } = triplesToGraph(this.triples, this.prefixMap, themeColors, this.settings);
30628
+ const existingValues = new Set(this.nodesDataSet.get().map((n) => n.fullValue));
30629
+ const nodesToAdd = nodes.filter((n) => !existingValues.has(n.fullValue));
30630
+ const existingEdgeKeys = new Set(
30631
+ this.edgesDataSet.get().map((e) => `${e.from}|${e.predicate}|${e.to}`)
30632
+ );
30633
+ const edgesToAdd = edges.filter(
30634
+ (e) => !existingEdgeKeys.has(`${e.from}|${e.predicate}|${e.to}`)
30635
+ );
30636
+ if (nodesToAdd.length > 0) {
30637
+ this.nodesDataSet.add(nodesToAdd);
30638
+ nodesToAdd.forEach((n) => {
30639
+ if (n.uri != null) {
30640
+ this.uriToNodeId.set(n.uri, n.id);
30641
+ }
30642
+ });
30643
+ }
30644
+ if (edgesToAdd.length > 0) {
30645
+ this.edgesDataSet.add(edgesToAdd);
30646
+ }
30647
+ }
30648
+ /**
30478
30649
  * @returns Icon element
30479
30650
  */
30480
30651
  getIcon() {
@@ -30496,6 +30667,10 @@ var GraphPlugin = class {
30496
30667
  */
30497
30668
  destroy() {
30498
30669
  this.removeClickOutsideHandler();
30670
+ if (this.expansionAbortController) {
30671
+ this.expansionAbortController.abort();
30672
+ this.expansionAbortController = null;
30673
+ }
30499
30674
  if (this.themeObserver) {
30500
30675
  this.themeObserver.disconnect();
30501
30676
  this.themeObserver = null;