From 8584ffa4df7a411df2d03c151505cf9ceaa48944 Mon Sep 17 00:00:00 2001
From: Milho001 <l.milhomemfrancochristino@uu.nl>
Date: Thu, 4 Jul 2024 12:39:17 +0000
Subject: [PATCH] fix(vis): remove read-only bug from nl

required refactor of how the graph data structure is handled by nl
---
 .../vis/components/config/SelectionConfig.tsx |   7 +-
 .../components/NLMachineLearning.tsx          |  40 ++---
 .../nodelinkvis/components/NLPixi.tsx         | 139 +++++++++---------
 .../nodelinkvis/components/query2NL.tsx       |  34 +++--
 .../nodelinkvis/components/utils.tsx          |   5 +-
 .../nodelinkvis/nodelinkvis.tsx               |  12 +-
 .../vis/visualizations/nodelinkvis/types.ts   |  41 +++---
 7 files changed, 142 insertions(+), 136 deletions(-)

diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx
index 7d41cf4a1..1d5e96f04 100644
--- a/libs/shared/lib/vis/components/config/SelectionConfig.tsx
+++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
 import { SelectionStateI, unSelect } from '@graphpolaris/shared/lib/data-access/store/interactionSlice';
 import { Delete } from '@mui/icons-material';
 import { useDispatch } from 'react-redux';
@@ -25,8 +26,8 @@ export const SelectionConfig = () => {
         />
       </div>
       {selection.content.map((item, index) => (
-        <>
-          <div key={index + 'id'} className="flex justify-between items-center px-4 py-1 gap-1">
+        <React.Fragment key={index + 'id'}>
+          <div className="flex justify-between items-center px-4 py-1 gap-1">
             <span className="text-xs font-normal">ID</span>
             <span className="text-xs">{item._id}</span>
           </div>
@@ -43,7 +44,7 @@ export const SelectionConfig = () => {
               </div>
             );
           })}
-        </>
+        </React.Fragment>
       ))}
     </div>
   );
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
index 78d36400c..19c50b629 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
@@ -6,18 +6,18 @@ export function processLinkPrediction(ml: ML, graph: GraphType): GraphType {
   if (ml === undefined || ml.linkPrediction === undefined) return graph;
 
   if (ml.linkPrediction.enabled) {
-    let allNodeIds = new Set(graph.nodes.map((n) => n._id));
+    let allNodeIds = new Set(Object.keys(graph.nodes));
     ml.linkPrediction.result.forEach((link) => {
       if (allNodeIds.has(link.from) && allNodeIds.has(link.to)) {
         const toAdd: LinkType = {
-          id: link.from + link.to,
+          id: link.from + ':LP:' + link.to, // TODO: this only supports one link between two nodes
           source: link.from,
           target: link.to,
           value: link.attributes.jaccard_coefficient as number,
           mlEdge: true,
           color: 0x000000,
         };
-        graph.links.push(toAdd);
+        graph.links[toAdd.id] = toAdd;
       }
     });
   }
@@ -35,18 +35,16 @@ export function processCommunityDetection(ml: ML, graph: GraphType): GraphType {
       });
     });
 
-    graph.nodes = graph.nodes.map((node, i) => {
-      if (allNodeIdMap.has(node._id)) {
-        node.cluster = allNodeIdMap.get(node._id);
+    Object.keys(graph.nodes).forEach((nodeId) => {
+      if (allNodeIdMap.has(nodeId)) {
+        graph.nodes[nodeId].cluster = allNodeIdMap.get(nodeId);
       } else {
-        node.cluster = -1;
+        graph.nodes[nodeId].cluster = -1;
       }
-      return node;
     });
   } else {
-    graph.nodes = graph.nodes.map((node, i) => {
-      node.cluster = undefined;
-      return node;
+    Object.keys(graph.nodes).forEach((nodeId) => {
+      graph.nodes[nodeId].cluster = undefined;
     });
   }
   return graph;
@@ -101,6 +99,7 @@ export const useNLMachineLearning = (props: {
    * Gets the edges corresponding to the shortestPath.
    * @param pathString The path as a string.
    * @returns The path as a LinkType[]
+   * @deprecated This function is not working anymore
    */
   function getShortestPathEdges(pathString: string[]): LinkType[] {
     try {
@@ -112,13 +111,14 @@ export const useNLMachineLearning = (props: {
           continue;
         }
         let edgeFound = false;
-        props.graph.links.forEach((link: any) => {
-          const { source, target } = link;
+        Object.keys(props.graph.links).forEach((key) => {
+          const link = props.graph.links[key];
           if (
-            (pathString[index] == source.id && pathString[index + 1] == target.id) ||
-            (pathString[index] == source && pathString[index + 1] == target) ||
-            (pathString[index + 1] == source.id && pathString[index] == target.id) ||
-            (pathString[index + 1] == source && pathString[index] == target)
+            false // FIXME: This is not working anymore
+            // (pathString[index] == source.id && pathString[index + 1] == target.id) ||
+            // (pathString[index] == source && pathString[index + 1] == target) ||
+            // (pathString[index + 1] == source.id && pathString[index] == target.id) ||
+            // (pathString[index + 1] == source && pathString[index] == target)
           ) {
             newPath.push(link);
             edgeFound = true;
@@ -172,10 +172,10 @@ export const useNLMachineLearning = (props: {
    * after a community detection algorithm, where the cluster of these nodes could have been changed.
    */
   const resetClusterOfNodes = (type: number): void => {
-    props.graph.nodes.forEach((node: NodeType) => {
-      const numberOfClusters = props.numberOfMlClusters;
+    Object.keys(props.graph.nodes).forEach((key) => {
+      const node = props.graph.nodes[key];
       if (node.cluster == type) {
-        node.cluster = numberOfClusters;
+        node.cluster = props.numberOfMlClusters;
       }
       if (node.type == type) {
         node.cluster = node.type;
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index 36ea28f65..6357b96bd 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
@@ -1,4 +1,4 @@
-import { GraphType, LinkType, NodeType } from '../types';
+import { GraphType, GraphTypeD3, LinkType, LinkTypeD3, NodeType, NodeTypeD3 } from '../types';
 import { dataColors, visualizationColors } from 'config';
 import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
 import {
@@ -19,19 +19,18 @@ import { NLPopup } from './NLPopup';
 import { hslStringToHex, nodeColor } from './utils';
 import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts } from '../../../../graph-layout';
 import { MultiGraph } from 'graphology';
-import { VisualizationSettingsType } from '../../../common';
 import { Viewport } from 'pixi-viewport';
 import { NodelinkVisProps } from '../nodelinkvis';
 
 type Props = {
-  onClick: (event?: { node: NodeType; pos: IPointData }) => void;
+  onClick: (event?: { node: NodeTypeD3; pos: IPointData }) => void;
   // onHover: (data: { node: NodeType; pos: IPointData }) => void;
   // onUnHover: (data: { node: NodeType; pos: IPointData }) => void;
   highlightNodes: NodeType[];
   configuration: NodelinkVisProps;
   currentShortestPathEdges?: LinkType[];
   highlightedLinks?: LinkType[];
-  graph?: GraphType;
+  graph: GraphType;
   layoutAlgorithm: string;
   showPopupsOnHover: boolean;
 };
@@ -69,9 +68,10 @@ export const NLPixi = (props: Props) => {
   const mouseInCanvas = useRef<boolean>(false);
   const isSetup = useRef(false);
   const ml = useML();
-  const dragging = useRef<{ node: NodeType; gfx: Sprite } | null>(null);
+  const dragging = useRef<{ node: NodeTypeD3; gfx: Sprite } | null>(null);
   const onlyClicked = useRef(false);
   const searchResults = useSearchResultData();
+  const graph = useRef<GraphTypeD3>({ nodes: [], links: [] });
 
   const layoutAlgorithm = useRef<CytoscapeLayout | GraphologyLayout>(new LayoutFactory().createLayout(Layouts.DAGRE));
 
@@ -123,7 +123,7 @@ export const NLPixi = (props: Props) => {
   const imperative = useRef<any>(null);
 
   useImperativeHandle(imperative, () => ({
-    onDragStart(node: NodeType, gfx: Sprite) {
+    onDragStart(node: NodeTypeD3, gfx: Sprite) {
       dragging.current = { node, gfx };
       onlyClicked.current = true;
 
@@ -167,7 +167,7 @@ export const NLPixi = (props: Props) => {
             setPopups(popups.filter((p) => p.node._id !== dragging.current?.node._id));
             props.onClick();
           } else {
-            setPopups([...popups, { node: dragging.current.node, pos: toGlobal(dragging.current.node) }]);
+            setPopups([...popups, { node: props.graph.nodes[dragging.current.node._id], pos: toGlobal(dragging.current.node) }]);
             props.onClick({ node: dragging.current.node, pos: toGlobal(dragging.current.node) });
           }
         }
@@ -176,7 +176,7 @@ export const NLPixi = (props: Props) => {
       } else {
       }
     },
-    onHover(node: NodeType) {
+    onHover(node: NodeTypeD3) {
       if (
         mouseInCanvas.current &&
         viewport?.current &&
@@ -184,7 +184,7 @@ export const NLPixi = (props: Props) => {
         node &&
         popups.filter((p) => p.node._id === node._id).length === 0
       ) {
-        setQuickPopup({ node, pos: toGlobal(node) });
+        setQuickPopup({ node: props.graph.nodes[node._id], pos: toGlobal(node) });
       }
     },
     onUnHover() {
@@ -224,7 +224,7 @@ export const NLPixi = (props: Props) => {
     }
   }, [ref]);
 
-  function toGlobal(node: NodeType): IPointData {
+  function toGlobal(node: NodeTypeD3): IPointData {
     if (viewport?.current) {
       // const rect = ref.current?.getBoundingClientRect();
       const rect = { x: 0, y: 0 };
@@ -234,7 +234,7 @@ export const NLPixi = (props: Props) => {
     } else return { x: 0, y: 0 };
   }
 
-  function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Sprite) {
+  function onDragStart(event: FederatedPointerEvent, node: NodeTypeD3, gfx: Sprite) {
     event.stopPropagation();
     if (imperative.current) imperative.current.onDragStart(node, gfx);
   }
@@ -249,19 +249,20 @@ export const NLPixi = (props: Props) => {
     if (imperative.current) imperative.current.onDragEnd();
   }
 
-  const updateNode = (node: NodeType) => {
+  const updateNode = (node: NodeTypeD3) => {
     const gfx = nodeMap.current.get(node._id);
     if (!gfx) return;
 
     // Update texture when selected
-    const texture = Assets.get(textureId(node.selected));
+    const nodeMeta = props.graph.nodes[node._id];
+    const texture = Assets.get(textureId(nodeMeta.selected));
     gfx.texture = texture;
 
     // Cluster colors
-    if (node?.cluster) {
-      gfx.tint = node.cluster >= 0 ? nodeColor(node.cluster) : 0x000000;
+    if (nodeMeta?.cluster) {
+      gfx.tint = nodeMeta.cluster >= 0 ? nodeColor(nodeMeta.cluster) : 0x000000;
     } else {
-      gfx.tint = nodeColor(node.type);
+      gfx.tint = nodeColor(nodeMeta.type);
     }
 
     gfx.position.set(node.x, node.y);
@@ -296,7 +297,9 @@ export const NLPixi = (props: Props) => {
     // }
   };
 
-  const createNode = (node: NodeType, selected?: boolean) => {
+  const createNode = (node: NodeTypeD3, selected?: boolean) => {
+    const nodeMeta = props.graph.nodes[node._id];
+
     // check if node is already drawn, and if so, delete it
     if (node && node?._id && nodeMap.current.has(node._id)) {
       nodeMap.current.delete(node._id);
@@ -308,14 +311,13 @@ export const NLPixi = (props: Props) => {
     const texture = Assets.get(textureId());
     gfx = new Sprite(texture);
 
-    gfx.tint = nodeColor(node.type);
-    const scale = (Math.max(node.radius || 5, 5) / 70) * 2;
+    gfx.tint = nodeColor(nodeMeta.type);
+    const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2;
     gfx.scale.set(scale, scale);
     gfx.anchor.set(0.5, 0.5);
 
     nodeMap.current.set(node._id, gfx);
     nodeLayer.addChild(gfx);
-    node.selected = selected;
 
     updateNode(node);
     gfx.name = 'node_' + node._id;
@@ -332,30 +334,30 @@ export const NLPixi = (props: Props) => {
   //   });
   // };
 
-  const updateLink = (link: LinkType) => {
+  const updateLink = (link: LinkTypeD3) => {
     if (!props.graph || nodeMap.current.size === 0) return;
+    const linkMeta = props.graph.links[link._id];
 
     const _source = link.source;
     const _target = link.target;
 
     if (!_source || !_target) {
-      console.log('source or target not found', _source, _target);
       return;
     }
 
     let sourceId = '';
     let targetId = '';
-    let source: NodeType | undefined;
-    let target: NodeType | undefined;
+    let source: NodeTypeD3 | undefined;
+    let target: NodeTypeD3 | undefined;
 
     if (typeof _source === 'string') {
       sourceId = link.source as string;
       targetId = link.target as string;
-      source = nodeMap.current.get(sourceId) as NodeType | undefined;
-      target = nodeMap.current.get(targetId) as NodeType | undefined;
+      source = nodeMap.current.get(sourceId) as NodeTypeD3 | undefined;
+      target = nodeMap.current.get(targetId) as NodeTypeD3 | undefined;
     } else {
-      source = link.source as NodeType;
-      target = link.target as NodeType;
+      source = link.source as NodeTypeD3;
+      target = link.target as NodeTypeD3;
       sourceId = source._id;
       targetId = target._id;
     }
@@ -368,28 +370,28 @@ export const NLPixi = (props: Props) => {
       // let color = link.color || 0x000000;
       let color = config.LINE_COLOR_DEFAULT;
       let style = config.LINE_WIDTH_DEFAULT;
-      let alpha = link.alpha || 1;
-      if (link.mlEdge) {
+      let alpha = linkMeta.alpha || 1;
+      if (linkMeta.mlEdge) {
         color = config.LINE_COLOR_ML;
-        if (link.value > ml.communityDetection.jaccard_threshold) {
-          style = link.value * 1.8;
+        if (linkMeta.value > ml.communityDetection.jaccard_threshold) {
+          style = linkMeta.value * 1.8;
         } else {
           style = 0;
           alpha = 0.2;
         }
-      } else if (props.highlightedLinks && props.highlightedLinks.includes(link)) {
-        if (link.mlEdge && ml.communityDetection.jaccard_threshold) {
-          if (link.value > ml.communityDetection.jaccard_threshold) {
+      } else if (props.highlightedLinks && props.highlightedLinks.includes(linkMeta)) {
+        if (linkMeta.mlEdge && ml.communityDetection.jaccard_threshold) {
+          if (linkMeta.value > ml.communityDetection.jaccard_threshold) {
             color = dataColors.magenta[50];
             // 0xaa00ff;
-            style = link.value * 1.8;
+            style = linkMeta.value * 1.8;
           }
         } else {
           color = dataColors.red[70];
           // color = 0xff0000;
           style = 1.0;
         }
-      } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(link)) {
+      } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(linkMeta)) {
         color = dataColors.green[50];
         // color = 0x00ff00;
         style = 3.0;
@@ -398,7 +400,7 @@ export const NLPixi = (props: Props) => {
       // Conditional alpha for search results
       if (searchResults.nodes.length > 0 || searchResults.edges.length > 0) {
         // FIXME: searchResults.edges should be a hashmap to improve performance.
-        const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link.id);
+        const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link._id);
         alpha = isLinkInSearchResults ? 1 : 0.05;
       }
 
@@ -438,11 +440,11 @@ export const NLPixi = (props: Props) => {
       if (isSetup.current === false) setup();
       else update(false);
     }
-  }, [props.graph, config, assetsLoaded]);
+  }, [config, assetsLoaded]);
 
   useEffect(() => {
     if (props.graph) {
-      props.graph.nodes.forEach((node: NodeType) => {
+      graph.current.nodes.forEach((node) => {
         const gfx = nodeMap.current.get(node._id);
         if (!gfx) return;
         const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node._id);
@@ -463,7 +465,7 @@ export const NLPixi = (props: Props) => {
 
       const widthHalf = app.renderer.width / 2;
       const heightHalf = app.renderer.height / 2;
-      props.graph.nodes.forEach((node: NodeType, i) => {
+      graph.current.nodes.forEach((node, i) => {
         if (!layoutAlgorithm.current) return;
         const gfx = nodeMap.current.get(node._id);
         if (!gfx || node.x === undefined || node.y === undefined) return;
@@ -474,24 +476,19 @@ export const NLPixi = (props: Props) => {
           stopped += 1;
           return;
         }
-        try {
-          if (layoutAlgorithm.current.provider === 'Graphology') {
-            // this is a dirty hack to fix the graphology layout being out of bounds
-            node.x = position.x + widthHalf;
-            node.y = position.y + heightHalf;
-          } else {
-            node.x = position.x;
-            node.y = position.y;
-          }
-        } catch (e) {
-          // node.x and .y become read-only when some layout algorithms are finished
-          layoutState.current = 'paused';
+        if (layoutAlgorithm.current.provider === 'Graphology') {
+          // this is a dirty hack to fix the graphology layout being out of bounds
+          node.x = position.x + widthHalf;
+          node.y = position.y + heightHalf;
+        } else {
+          node.x = position.x;
+          node.y = position.y;
         }
 
         gfx.position.copyFrom(node as IPointData);
       });
 
-      if (stopped === props.graph.nodes.length) {
+      if (stopped === graph.current.nodes.length) {
         layoutStoppedCount.current = layoutStoppedCount.current + 1;
         if (layoutStoppedCount.current > 1000) {
           layoutState.current = 'paused';
@@ -507,7 +504,7 @@ export const NLPixi = (props: Props) => {
       // Draw the links
       linkGfx.clear();
       linkGfx.beginFill();
-      props.graph.links.forEach((link: any) => {
+      graph.current.links.forEach((link: any) => {
         updateLink(link);
       });
       linkGfx.endFill();
@@ -526,7 +523,7 @@ export const NLPixi = (props: Props) => {
       }
 
       nodeMap.current.forEach((gfx, id) => {
-        if (!props.graph?.nodes?.find((node) => node._id === id)) {
+        if (!graph.current.nodes.find((node) => node._id === id)) {
           nodeLayer.removeChild(gfx);
           gfx.destroy();
           nodeMap.current.delete(id);
@@ -535,17 +532,13 @@ export const NLPixi = (props: Props) => {
 
       linkGfx.clear();
 
-      props.graph.nodes.forEach((node: NodeType) => {
+      graph.current.nodes.forEach((node) => {
         if (!forceClear && nodeMap.current.has(node._id)) {
           const old = nodeMap.current.get(node._id);
 
-          try {
-            node.x = old?.x || node.x;
-            node.y = old?.y || node.y;
-            updateNode(node);
-          } catch (e) {
-            // node.x and .y become read-only when some layout algorithms are finished
-          }
+          node.x = old?.x || node.x;
+          node.y = old?.y || node.y;
+          updateNode(node);
         } else {
           createNode(node);
         }
@@ -583,6 +576,16 @@ export const NLPixi = (props: Props) => {
 
     if (!props.graph) throw Error('Graph is undefined');
 
+    //Setup d3 graph structure
+    graph.current = {
+      nodes: Object.values(props.graph.nodes).map((n) => ({ _id: n._id, x: n.defaultX, y: n.defaultY })),
+      links: Object.values(props.graph.links).map((l) => ({
+        _id: l.id,
+        source: l.source,
+        target: l.target,
+      })),
+    };
+
     const size = ref.current?.getBoundingClientRect();
     viewport.current = new Viewport({
       screenWidth: size?.width || 1000,
@@ -620,12 +623,12 @@ export const NLPixi = (props: Props) => {
     if (!layoutAlgorithm) throw Error('LayoutAlgorithm is undefined');
 
     const graphologyGraph = new MultiGraph();
-    props.graph?.nodes.forEach((node) => {
-      if (forceClear) graphologyGraph.addNode(node._id, { size: node.radius || 5 });
-      else graphologyGraph.addNode(node._id, { size: node.radius || 5, x: node.x || 0, y: node.y || 0 });
+    graph.current.nodes.forEach((node) => {
+      if (forceClear) graphologyGraph.addNode(node._id, { size: props.graph.nodes[node._id].radius || 5 });
+      else graphologyGraph.addNode(node._id, { size: props.graph.nodes[node._id].radius || 5, x: node.x || 0, y: node.y || 0 });
     });
 
-    props.graph?.links.forEach((link) => {
+    graph.current.links.forEach((link) => {
       graphologyGraph.addEdge(link.source, link.target);
     });
     const boundingBox = { x1: 0, x2: app.renderer.screen.width, y1: 0, y2: app.renderer.screen.height };
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
index 990719dc6..952d6bc4e 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
@@ -142,7 +142,11 @@ type OptionsI = {
  * @returns {GraphType} A node-link graph containing the nodes and links for the diagram.
  */
 export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: OptionsI = {}): GraphType {
-  const nodes: NodeType[] = [];
+  let ret: GraphType = {
+    nodes: {},
+    links: {},
+  };
+
   const typeDict: { [key: string]: number } = {};
   // Counter for the types
   let counter = 1;
@@ -184,8 +188,8 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
       type: typeNumber,
       displayInfo: preferredText,
       radius: radius,
-      x: (options.defaultX || 0) + Math.random() * radius * 20 - radius * 10,
-      y: (options.defaultY || 0) + Math.random() * radius * 20 - radius * 10,
+      defaultX: (options.defaultX || 0) + Math.random() * radius * 20 - radius * 10,
+      defaultY: (options.defaultY || 0) + Math.random() * radius * 20 - radius * 10,
     };
 
     // let mlExtra = {};
@@ -209,13 +213,13 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
 
     // Add mlExtra to the node if necessary
     // data = { ...data, ...mlExtra };
-    nodes.push(data);
+    ret.nodes[data._id] = data;
   }
 
   // Filter unique edges and transform to LinkTypes
   // List for all links
   let links: LinkType[] = [];
-  let allNodeIds = new Set(nodes.map((n) => n._id));
+  let allNodeIds = new Set(Object.keys(ret.nodes));
 
   // Parse ml edges
   //   if (ml != undefined) {
@@ -239,14 +243,14 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
   for (let i = 0; i < uniqueEdges.length; i++) {
     if (allNodeIds.has(uniqueEdges[i].from) && allNodeIds.has(uniqueEdges[i].to)) {
       const toAdd: LinkType = {
-        id: uniqueEdges[i].from + ':' + uniqueEdges[i].to,
+        id: uniqueEdges[i].from + ':' + uniqueEdges[i].to, // TODO: this only supports one link between two nodes
         source: uniqueEdges[i].from,
         target: uniqueEdges[i].to,
         value: uniqueEdges[i].count,
         mlEdge: false,
         color: 0x000000,
       };
-      links.push(toAdd);
+      ret.links[toAdd.id] = toAdd;
     }
   }
 
@@ -266,13 +270,13 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
   }
 
   // Graph to be returned
-  let toBeReturned: GraphType = {
-    nodes: nodes,
-    links: links,
-    // linkPrediction: linkPredictionInResult,
-    // shortestPath: shortestPathInResult,
-    // communityDetection: communityDetectionInResult,
-  };
+  // let toBeReturned: GraphType = {
+  //   nodes: nodes,
+  //   links: links,
+  // linkPrediction: linkPredictionInResult,
+  // shortestPath: shortestPathInResult,
+  // communityDetection: communityDetectionInResult,
+  // };
 
   // If query with community detection; add number of clusters to the graph
   // const numberOfClusters = {
@@ -283,5 +287,5 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
   // }
 
   // return toBeReturned;
-  return processML(ml, toBeReturned);
+  return processML(ml, ret);
 }
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx
index 36f17720a..173dbecc6 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx
@@ -68,11 +68,12 @@ export function hslStringToHex(hsl: string) {
  */
 export const getRelatedLinks = (graph: GraphType, nodes: NodeType[], jaccardThreshold: number): LinkType[] => {
   const relatedLinks: LinkType[] = [];
-  graph.links.forEach((link: LinkType) => {
+  Object.keys(graph.links).forEach((id) => {
+    const link = graph.links[id];
     const { source, target } = link;
     if (isLinkVisible(link, jaccardThreshold)) {
       nodes.forEach((node: NodeType) => {
-        if (source == node || target == node || source == node._id || target == node._id) {
+        if (source == node._id || target == node._id || source == node._id || target == node._id) {
           relatedLinks.push(link);
         }
       });
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
index 70c7867ba..4daa88491 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
@@ -1,5 +1,5 @@
 import React, { useEffect, useRef, useState } from 'react';
-import { GraphType, LinkType, NodeType } from './types';
+import { GraphType, LinkType, NodeType, NodeTypeD3 } from './types';
 import { NLPixi } from './components/NLPixi';
 import { parseQueryResult } from './components/query2NL';
 import { useImmer } from 'use-immer';
@@ -66,7 +66,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel
     }
   }, [data, ml]);
 
-  const onClickedNode = (event?: { node: NodeType; pos: IPointData }, ml?: ML) => {
+  const onClickedNode = (event?: { node: NodeTypeD3; pos: IPointData }, ml?: ML) => {
     if (graph) {
       if (!event?.node) {
         handleSelect();
@@ -74,11 +74,12 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel
       }
 
       const node = event.node;
-      handleSelect({ nodes: [node as Node] });
+      const nodeMeta = graph.nodes[node._id];
+      handleSelect({ nodes: [nodeMeta as Node] });
 
       if (ml && ml.shortestPath.enabled) {
         setGraph((draft) => {
-          let _node = draft?.nodes.find((n) => n._id === node._id);
+          let _node = draft?.nodes[node._id];
           if (!_node) return draft;
 
           if (!ml.shortestPath.srcNode) {
@@ -105,6 +106,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel
     }
   };
 
+  if (!graph) return null;
   return (
     <NLPixi
       graph={graph}
@@ -177,7 +179,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
               type="dropdown"
               label="Shape"
               value={settings.shapes.shape}
-              options={[{circle: 'Circle'}, {rectangle: 'Square'}]}
+              options={[{ circle: 'Circle' }, { rectangle: 'Square' }]}
               onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })}
             />
           ) : (
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
index 7374b774b..321f20198 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
@@ -9,16 +9,21 @@ import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResul
 
 /** Types for the nodes and links in the node-link diagram. */
 export type GraphType = {
-  nodes: NodeType[];
-  links: LinkType[];
+  nodes: Record<string, NodeType>; // _id -> node
+  links: Record<string, LinkType>; // _id -> link
   // linkPrediction?: boolean;
   // shortestPath?: boolean;
   // communityDetection?: boolean;
   // numberOfMlClusters?: number;
 };
 
+export type GraphTypeD3 = {
+  nodes: NodeTypeD3[];
+  links: LinkTypeD3[];
+};
+
 /** The interface for a node in the node-link diagram */
-export interface NodeType extends d3.SimulationNodeDatum, Node {
+export interface NodeType extends Node {
   _id: string;
 
   // Number to determine the color of the node
@@ -41,28 +46,14 @@ export interface NodeType extends d3.SimulationNodeDatum, Node {
 
   // The text that will be shown on top of the node if selected.
   displayInfo?: string;
-
-  // Node’s current x-position.
-  x?: number;
-
-  // Node’s current y-position.
-  y?: number;
-
-  // Node’s current x-velocity
-  vx?: number;
-
-  // Node’s current y-velocity
-  vy?: number;
-
-  // Node’s fixed x-position (if position was fixed)
-  fx?: number | null;
-
-  // Node’s fixed y-position (if position was fixed)
-  fy?: number | null;
+  defaultX?: number;
+  defaultY?: number;
 }
 
+export type NodeTypeD3 = d3.SimulationNodeDatum & { _id: string };
+
 /** The interface for a link in the node-link diagram */
-export interface LinkType extends d3.SimulationLinkDatum<NodeType> {
+export interface LinkType {
   // The thickness of a line
   id: string;
   value: number;
@@ -70,14 +61,18 @@ export interface LinkType extends d3.SimulationLinkDatum<NodeType> {
   mlEdge: boolean;
   color: number;
   alpha?: number;
+  source: string;
+  target: string;
 }
 
+export type LinkTypeD3 = d3.SimulationLinkDatum<NodeTypeD3> & { _id: string };
+
 /**collectionNode holds 1 entry per node kind (so for example a MockNode with name "parties" and all associated attributes,) */
 export type TypeNode = {
   name: string; //Collection name
   attributes: string[]; //attributes. This includes all attributes found in the collection
   type: number | undefined; //number that represents collection of node, for colorscheme
-  visualisations: Visualization[]; //The way to visualize attributes of this Node kind
+  visualizations: Visualization[]; //The way to visualize attributes of this Node kind
 };
 
 export type CommunityDetectionNode = {
-- 
GitLab