From efb8e55d1df65803dea0e99879dfad81b3fe484a Mon Sep 17 00:00:00 2001
From: Dennis Collaris <d.a.c.collaris@uu.nl>
Date: Thu, 25 Jul 2024 16:11:29 +0000
Subject: [PATCH] feat: implement node labels for the node link visualization

---
 apps/web/public/assets/sprite.png             | Bin 687 -> 0 bytes
 apps/web/public/assets/sprite_selected.png    | Bin 697 -> 0 bytes
 .../public/assets/sprite_selected_square.png  | Bin 96 -> 0 bytes
 apps/web/public/assets/sprite_square.png      | Bin 344 -> 0 bytes
 .../nodelinkvis/components/NLPixi.tsx         | 255 +++++++++++++-----
 5 files changed, 184 insertions(+), 71 deletions(-)
 delete mode 100644 apps/web/public/assets/sprite.png
 delete mode 100644 apps/web/public/assets/sprite_selected.png
 delete mode 100644 apps/web/public/assets/sprite_selected_square.png
 delete mode 100644 apps/web/public/assets/sprite_square.png

diff --git a/apps/web/public/assets/sprite.png b/apps/web/public/assets/sprite.png
deleted file mode 100644
index a9d77b0520b6b4945bbe38e04553e4e349f1794e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 687
zcmeAS@N?(olHy`uVBq!ia0vp^ZXnFT3?e@WZ39vj0X`wFK-w=N%P%|&L?WS}=&YdV
zY>-GKL>$P@_6yJUjm+^5&+zoj{r~?zP&PO=f6?-dKY#xE@#E+2{l^lss(%0e{ps_U
zH*ep+eD&tWt$WvQ+&y>k+M%OoCQe_}HDOL=V_#uqM@B)znOKo*pxweHL4LsuZ!i5=
zvogU$ivRwNLsQx!f_2!h<z3j$z`%If)5S3);_#fQoYR^N1YDdsG*)PoDJWQX-2315
zIn64ONy+;k<FC@trB*487kDMU>fYW~bBf7a<@d*!<Bx=Oj(<HN$Jci@edAo_o2RCJ
zVtrA1TTOl6q0HLF%8#WJ+&7)@{cif|@fN+$I;T%t_5SuK|GmxNN!`V?5??0eNy{#)
z#lDMowktkr?VHf1Qd@lV=I^A_0v8syuHVwHxasj5k@%CHtU3xws%PiVd>pvdi&Z-$
zOYv3hj@31lq8q$E2L1D?4U@EG&2g{^jhp=a&_oF{q2ecIo32mVeIV_L-LyR8$@@5N
z^i23?u(sD|F;iK<i9P?SyXN+3-Eb96yx;!YXk+>Ev#<UIOi@WtXG*;|(OCD*va5Fs
zH(X9&_;&ZFws72g5gw0^=l+MCJ~~6lK;htqJC4EZZY9lHZycH3Om1*EIr&cBxGEs(
za<k%I6LpFG37e<O2rN6AD`Pp?cg4X8QQsK<Hw*D-e(ax6`8>fxPx!c2N`gmQ>5t5#
zei;l6jS_7i({qK?3^L>OGo#dUwPXu-efaX{Qo2XHG3(LJwIYi&FI{f1e5}WNznO{C
zhfhs4!<u7e!?y=t8~-kmb8qRJChKW)s)}XTi$9v;URMLVrpGY<4blotVK_ET^}&|e
S-<5%}&*16m=d#Wzp$P!Bu{<>Z

diff --git a/apps/web/public/assets/sprite_selected.png b/apps/web/public/assets/sprite_selected.png
deleted file mode 100644
index 1717d0ce9b6570151ce5f68bacb12864bc542e00..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 697
zcmV;q0!ICbP)<h;3K|Lk000e1NJLTq002w?002w~0{{R3@JXQ=0001cP)t-s0000_
zS!zyLYfe~dO;>79S8GmKYED>bO<8GGRc20CYED>cPFQMASZYpJYED>cPFZSCS7}XH
zYEM~ePgrYET4+^OW=>dY|NsBd)Y-4Jylr)aXK;Y__V@Dh^y}>I=;`d|>Fd+g+Qi4r
zwYtHTo2GMnid$rLeua`MI{i`r000+sQchC<FZt%Rl6^iO2k-3P+0x9xtd3_<HV=D)
z(C`2N0oF-GK~z}7oYe`AgD?yQ(6E<bDN9=$C_9wi|AZrTMk0X_;=Th!w*0@slzcDG
z>^KZeGYDfl%RQfc>Fy>bY?z6AXJi*Qf&-CzVH8FRN~xj$lntkBz2_bdXmK9w)Xf4-
z){Uw84iGrL@`hI+xhn2zSU_YQ-og6;vLEl@Efc<AdeR-Gb3Ny&=o-YPE2jA=olUao
z#e$uxY*Emb73^5MEBO06z6O*SUi+JF6XNXl8}Y!n+t%6S9yUr;C}HC!^w9#rQlL_?
zfWUejFIn|?qOT4YH9+vL@l^c=!I}?SoU29+wK$tCA8hGSOTS1*OMzFQrUH2BfA&?G
z&`-!;4_qOuNFb!w{X7-{XGDS#IML^?75a!ILQn78Ee9kSwtFUg#E^&&Tf&%c%1=o@
zsOd8gI4u&j!0}kF$OSPSIm=)*BiRhAOepz;WRp<#SW!!2L1Pxeszb64Rv`;u)gV~|
ztAOz_!!Phb`)$QOXaInLAcpa0Z4xF>srq$EeXM@77NE99ZQ43cb<pbys>^Y%?Rrq^
zshdZ*#)BGMW~8cNZ^qCXkY{wSP%&|5f>=ew39l2YC-6@dF(t{=H&ZM{<uzqo)`VYR
fezp3gZ`IKNWhbEIJ}Luq00000NkvXXu0mjf;+Z>d

diff --git a/apps/web/public/assets/sprite_selected_square.png b/apps/web/public/assets/sprite_selected_square.png
deleted file mode 100644
index 8e6a75992f913fa454c18c4138c8a3883791f98f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 96
zcmeAS@N?(olHy`uVBq!ia0vp^P9V(43?y&PnzR~7u?6^qxcWt8{r~^}OmX@epopla
ti(`n!`Q!!M8~*b%Enf5Rz#>})2E{iYa@)L}HGm2jJYD@<);T3K0RXyd9TNZm

diff --git a/apps/web/public/assets/sprite_square.png b/apps/web/public/assets/sprite_square.png
deleted file mode 100644
index 38a4cd208703a2e481ca4337430f631026c6bed0..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 344
zcmeAS@N?(olHy`uVBq!ia0vp^wjj*G1SBUieA@@47>k44ofy`glX(f`xTHpSruq6Z
zXaU(A42<oW3@jieKr98s3=GT*7#Wy>G$TlC0TW!-b^$Yj4N_<*$-N#(9rAQ>49Q@9
zdut=_AqNqbz@8QSL3fzewM%up6qwjMm;aIX<BYc{;qUdn6z#S5W1j!{z}fpJ-~M}h
z(D&;;nHl%iKYw_7{+@f+dcBubZJ)jT#q&M2$!1S{dF`LSmENqFpY_8s|Nac#%HYH2
zD}Nu$IWb$R?%ZwjH0OI)J`~^EXDIz?S=;>2-+FIOFgN`(XM6scwmmB!;x}T(tG!?T
mEA$7R4}R~t`|IkB+CLZ^8CYlcMn~QU`O4GP&t;ucLK6V{;DuBG

diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index 6e1277c66..d9c296688 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
@@ -3,17 +3,16 @@ import { dataColors, visualizationColors } from 'config';
 import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
 import {
   Application,
-  AssetsBundle,
   Color,
   Container,
   FederatedPointerEvent,
   Graphics,
   IPointData,
   Sprite,
-  Assets,
   Text,
   Texture,
   Resource,
+  RenderTexture
 } from 'pixi.js';
 import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access';
 import { NLPopup } from './NLPopup';
@@ -47,7 +46,6 @@ type LayoutState = 'reset' | 'running' | 'paused';
 export const NLPixi = (props: Props) => {
   const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>();
   const [popups, setPopups] = useState<{ node: NodeTypeD3; pos: IPointData }[]>([]);
-  const [assetsLoaded, setAssetsLoaded] = useState(false);
 
   const app = useMemo(
     () =>
@@ -61,7 +59,13 @@ export const NLPixi = (props: Props) => {
     [],
   );
   const nodeLayer = useMemo(() => new Container(), []);
-  const labelLayer = useMemo(() => {
+  const linkLabelLayer = useMemo(() => {
+    const container = new Container();
+    container.alpha = 0;
+    container.renderable = false;
+    return container;
+  }, []);
+  const nodeLabelLayer = useMemo(() => {
     const container = new Container();
     container.alpha = 0;
     container.renderable = false;
@@ -70,7 +74,8 @@ export const NLPixi = (props: Props) => {
 
   const nodeMap = useRef(new Map<string, Sprite>());
   const linkGfx = new Graphics();
-  const labelMap = useRef(new Map<string, Text>());
+  const linkLabelMap = useRef(new Map<string, Text>());
+  const nodeLabelMap = useRef(new Map<string, Text>());
   const viewport = useRef<Viewport>();
   const layoutState = useRef<LayoutState>('reset');
   const layoutStoppedCount = useRef(0);
@@ -87,11 +92,71 @@ export const NLPixi = (props: Props) => {
   // const cull = new Cull();
   // let cullDirty = useRef(true);
 
-  const textureId = (selected: boolean = false) => {
-    const selectionSuffix = selected ? '_selected' : '';
-    const shapeSuffix = props.configuration.shapes.shape == 'rectangle' ? '_square' : '';
-    return `texture${selectionSuffix}${shapeSuffix}`;
-  };
+  const _glyphTexture = RenderTexture.create();
+  const _selectedTexture = RenderTexture.create();
+
+  const getTexture = (renderTexture: RenderTexture, selected: boolean = false): RenderTexture => {
+    let size = config.NODE_RADIUS * (1 / responsiveScale);
+    let lineWidth = selected ? 12 : 6;
+    renderTexture.resize(size + lineWidth, size + lineWidth);
+    
+    const graphics = new Graphics();
+    graphics.lineStyle(lineWidth, 0x4e586a);
+    graphics.beginFill(0xffffff, 1);
+
+    if (props.configuration.shapes?.shape == "circle") {
+      graphics.drawCircle((size / 2) + (lineWidth / 2), (size / 2) + (lineWidth/2), (size / 2));
+    } else {
+      graphics.drawRect(lineWidth, lineWidth, size - lineWidth, size - lineWidth);
+    }
+    graphics.endFill();
+    
+    app.renderer.render(graphics, { renderTexture });
+
+    return renderTexture;
+  }
+  
+  // Pixi viewport zoom scale, but discretized to single decimal.
+  const [responsiveScale, setResponsiveScale] = useState(1);
+
+  useEffect(() => {
+    if (nodeMap.current.size === 0) return;
+
+    graph.current.nodes.forEach((node) => {
+      const sprite = nodeMap.current.get(node._id) as Sprite;
+      const nodeMeta = props.graph.nodes[node._id];
+      sprite.texture = (sprite as any).selected ? selectedTexture : glyphTexture;
+
+      // To calculate the scale, we:
+      // 1) Determine the node radius, with a minimum of 5. If not available, we default to 5.
+      // 2) Get the ratio with respect to the typical size of the node (divide by NODE_RADIUS).
+      // 3) Scale this ratio by the current scale factor.
+      let scale = (Math.max(nodeMeta.radius || 5, 5) / config.NODE_RADIUS) * 2;
+      scale *= responsiveScale;
+      sprite.scale.set(scale, scale);
+    });
+
+    if (graph.current.nodes.length > config.LABEL_MAX_NODES) return;
+
+    // Change font size at specific scale intervals
+    const fontSize = (responsiveScale <= 0.1) ? 15 : (responsiveScale <= 0.2) ? 22.5 : (responsiveScale <= 0.4) ? 30 : (responsiveScale <= 0.6) ? 37.5 : 45;
+
+    const strokeWidth = fontSize / 2;
+    linkLabelMap.current.forEach((text) => {
+      text.style.fontSize = fontSize;
+      text.style.strokeThickness = strokeWidth;
+      text.resolution = Math.ceil(0.5 / responsiveScale);
+    });
+
+    nodeLabelMap.current.forEach((text) => {
+      text.style.fontSize = fontSize * (2 / 3);
+      text.resolution = Math.ceil(1 / responsiveScale);
+    });
+
+    graph.current.nodes.forEach((node: any) => {
+      updateNodeLabel(node);
+    });
+  }, [responsiveScale, props.configuration.shapes?.shape]);
 
   const [config, setConfig] = useState({
     width: 1000,
@@ -101,7 +166,7 @@ export const NLPixi = (props: Props) => {
 
     LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER,
 
-    NODE_RADIUS: 5,
+    NODE_RADIUS: 70,
     // NODE_BORDER_LINE_WIDTH: 1.0,
     // NODE_BORDER_LINE_WIDTH_SELECTED: 5.0, // if selected and normal width are different the thicker line will be still in the gfx
     // NODE_BORDER_COLOR_DEFAULT: dataColors.neutral[70],
@@ -113,6 +178,14 @@ export const NLPixi = (props: Props) => {
     LINE_WIDTH_DEFAULT: 0.8,
   });
 
+  const glyphTexture = useMemo(() => {
+    return getTexture(_glyphTexture);
+  }, [responsiveScale, props.configuration.shapes?.shape]);
+
+  const selectedTexture = useMemo(() => {
+    return getTexture(_selectedTexture, true);
+  }, [responsiveScale, props.configuration.shapes?.shape]);
+
   useEffect(() => {
     setConfig((lastConfig) => {
       return {
@@ -122,15 +195,6 @@ export const NLPixi = (props: Props) => {
     });
   }, [props.layoutAlgorithm, props.configuration]);
 
-  useEffect(() => {
-    if (nodeMap.current.size === 0) return;
-
-    const texture = Assets.get(textureId());
-    for (const sprite of nodeMap.current.values()) {
-      sprite.texture = texture;
-    }
-  }, [props.configuration.shapes?.shape]);
-
   const imperative = useRef<any>(null);
 
   const mouseClickThreshold = 200; // Time between mouse up and down events that is considered a click, and not a drag.
@@ -161,11 +225,13 @@ export const NLPixi = (props: Props) => {
         setPopups([{ node: node, pos: toGlobal(node) }]);
         for (const popup of popups) {
           const sprite = nodeMap.current.get(popup.node._id) as Sprite;
-          sprite.texture = Assets.get(textureId(false));
+          sprite.texture = glyphTexture;
+          (sprite as any).selected = false;
         }
       }
 
-      sprite.texture = Assets.get(textureId(true));
+      sprite.texture = selectedTexture;
+      (sprite as any).selected = true;
 
       props.onClick({ node: node, pos: toGlobal(node) });
 
@@ -180,7 +246,8 @@ export const NLPixi = (props: Props) => {
       if (holdDownTime < mouseClickThreshold) {
         for (const popup of popups) {
           const sprite = nodeMap.current.get(popup.node._id) as Sprite;
-          sprite.texture = Assets.get(textureId(false));
+          sprite.texture = glyphTexture;
+          (sprite as any).selected = false;
         }
         setPopups([]);
         props.onClick();
@@ -220,23 +287,27 @@ export const NLPixi = (props: Props) => {
     onZoom(event: FederatedPointerEvent) {
       const scale = viewport.current!.transform.scale.x;
 
-      if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
-        labelLayer.alpha = scale > 2 ? Math.min(1, (scale - 2) * 3) : 0;
+      if (scale > 2) {     
+        const scale  = 1 / viewport.current!.scale.x;  // starts from 0.5 down to 0.
+        setResponsiveScale((scale < 0.05) ? 0.1 : (scale < 0.1) ? 0.2 : (scale < 0.2) ? 0.4 : (scale < 0.3) ? 0.6 : 0.8);
+      } else {
+        setResponsiveScale(1);
+      }
 
-        if (labelLayer.alpha > 0) {
-          labelLayer.renderable = true;
+      if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
+        linkLabelLayer.alpha = (scale > 2) ? Math.min(1, (scale - 2) * 3) : 0;
 
-          const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0.
+        if (linkLabelLayer.alpha > 0) {
+          linkLabelLayer.renderable = true;
+        } else {
+          linkLabelLayer.renderable = false;
+        }
 
-          // Only change the fontSize for specific intervals, continuous change has too big of an impact on performance
-          const fontSize = scale < 0.1 ? 30 : scale < 0.2 ? 40 : scale < 0.3 ? 50 : 60;
-          const strokeWidth = fontSize / 2;
-          labelMap.current.forEach((text) => {
-            text.style.fontSize = fontSize;
-            text.style.strokeThickness = strokeWidth;
-          });
+        nodeLabelLayer.alpha = (scale > 5) ? Math.min(1, (scale - 5) * 3) : 0;
+        if (nodeLabelLayer.alpha > 0) {
+          nodeLabelLayer.renderable = true;
         } else {
-          labelLayer.renderable = false;
+          nodeLabelLayer.renderable = false;
         }
       }
     },
@@ -286,8 +357,9 @@ export const NLPixi = (props: Props) => {
 
     // Update texture when selected
     const nodeMeta = props.graph.nodes[node._id];
-    const texture = Assets.get(textureId(nodeMeta.selected));
+    const texture = (gfx as any).selected ? selectedTexture : glyphTexture;
     gfx.texture = texture;
+    
 
     // Cluster colors
     if (nodeMeta?.cluster) {
@@ -314,6 +386,10 @@ export const NLPixi = (props: Props) => {
     // }
   };
 
+  const getNodeLabel = (nodeMeta: NodeType) => {
+    return nodeMeta.label
+  }
+
   const createNode = (node: NodeTypeD3, selected?: boolean) => {
     const nodeMeta = props.graph.nodes[node._id];
 
@@ -325,11 +401,11 @@ export const NLPixi = (props: Props) => {
     if (node.x === undefined || node.y === undefined) return;
 
     let sprite: Sprite;
-    const texture = Assets.get(textureId());
+    const texture = glyphTexture;
     sprite = new Sprite(texture);
 
     sprite.tint = nodeColor(nodeMeta.type);
-    const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2;
+    const scale = (Math.max(nodeMeta.radius || 5, 5) / config.NODE_RADIUS) * 2;
     sprite.scale.set(scale, scale);
     sprite.anchor.set(0.5, 0.5);
     sprite.cullable = true;
@@ -346,13 +422,31 @@ export const NLPixi = (props: Props) => {
     updateNode(node);
     (sprite as any).node = node;
 
+    // Node label
+    const attribute = getNodeLabel(nodeMeta);
+    const text = new Text(attribute, { 
+      fontSize: 20,
+      fill: 0xffffff,
+      wordWrap: true,
+      wordWrapWidth: 65,
+      align: 'center'
+    });
+    text.eventMode = 'none';
+    text.cullable = true;
+    text.anchor.set(0.5, 0.5);
+    text.scale.set(0.1, 0.1);
+    nodeLabelMap.current.set(node._id, text);
+    nodeLabelLayer.addChild(text);
+
+    updateNodeLabel(node);
+
     return sprite;
   };
 
   const createLinkLabel = (link: LinkTypeD3) => {
     // check if link is already drawn, and if so, delete it
-    if (link && link?._id && labelMap.current.has(link._id)) {
-      labelMap.current.delete(link._id);
+    if (link && link?._id && linkLabelMap.current.has(link._id)) {
+      linkLabelMap.current.delete(link._id);
     }
 
     const linkMeta = props.graph.links[link._id];
@@ -366,8 +460,8 @@ export const NLPixi = (props: Props) => {
     text.cullable = true;
     text.anchor.set(0.5, 0.5);
     text.scale.set(0.1, 0.1);
-    labelMap.current.set(link._id, text);
-    labelLayer.addChild(text);
+    linkLabelMap.current.set(link._id, text);
+    linkLabelLayer.addChild(text);
 
     updateLinkLabel(link);
 
@@ -450,7 +544,9 @@ export const NLPixi = (props: Props) => {
   };
 
   const updateLinkLabel = (link: LinkTypeD3) => {
-    const text = labelMap.current.get(link._id);
+    if (graph.current.nodes.length > config.LABEL_MAX_NODES) return;
+
+    const text = linkLabelMap.current.get(link._id);
     if (!text) return;
 
     const _source = link.source;
@@ -488,9 +584,34 @@ export const NLPixi = (props: Props) => {
     } else {
       text.rotation = rads;
     }
-  };
+  }
 
-  // const text = labelMap.current.get(link._id);
+  const updateNodeLabel = (node: NodeTypeD3) => {
+    if (graph.current.nodes.length > config.LABEL_MAX_NODES) return;
+    const text = nodeLabelMap.current.get(node._id) as Text | undefined;
+    if (text == null) return;
+
+    if (node.x) text.x = node.x;
+    if (node.y) text.y = node.y;
+
+    const nodeMeta = props.graph.nodes[node._id];
+    const originalText = getNodeLabel(nodeMeta);
+
+    text.text = originalText;  // This is required to ensure the text size check (next line) works
+
+    if ((text.width/text.scale.x) <= 90 && (text.height/text.scale.y) <= 90) {
+      text.text = originalText;
+    } else {
+      // Change character limit at specific scale intervals
+      const charLimit = (responsiveScale > 0.2) ? 15 : (responsiveScale > 0.1) ? 30 : 75;
+      text.text = `${ originalText.slice(0, charLimit)}…`;
+    }
+
+    text.alpha = ((text.width/text.scale.x) <= 90 && (text.height/text.scale.y) <= 90) ? 1 : 0;
+  }
+
+
+  // const text = linkLabelMap.current.get(link._id);
   //   if (!text) return;
 
   //   const source = link.source as NodeTypeD3;
@@ -511,27 +632,14 @@ export const NLPixi = (props: Props) => {
   //     text.rotation = rads;
   //   }
 
-  async function loadAssets() {
-    if (!Assets.cache.has('texture')) {
-      Assets.addBundle('glyphs', {
-        texture: 'assets/sprite.png',
-        texture_square: 'assets/sprite_square.png',
-        texture_selected: 'assets/sprite_selected.png',
-        texture_selected_square: 'assets/sprite_selected_square.png',
-      });
-      await Assets.loadBundle('glyphs');
-    }
-    setAssetsLoaded(true);
-  }
-
   useEffect(() => {
-    loadAssets();
     return () => {
       nodeMap.current.clear();
-      labelMap.current.clear();
+      linkLabelMap.current.clear();
       linkGfx.clear();
       nodeLayer.removeChildren();
-      labelLayer.removeChildren();
+      linkLabelLayer.removeChildren();
+      nodeLabelLayer.removeChildren();
 
       const layout = layoutAlgorithm.current as GraphologyForceAtlas2Webworker;
       if(layout?.cleanup != null) layout.cleanup();
@@ -539,11 +647,11 @@ export const NLPixi = (props: Props) => {
   }, []);
 
   useEffect(() => {
-    if (assetsLoaded && props.graph && ref.current && ref.current.children.length > 0 && imperative.current) {
+    if (props.graph && ref.current && ref.current.children.length > 0 && imperative.current) {
       if (isSetup.current === false) setup();
       else update(false);
     }
-  }, [config, assetsLoaded]);
+  }, [config]);
 
   useEffect(() => {
     if (props.graph) {
@@ -589,6 +697,8 @@ export const NLPixi = (props: Props) => {
         }
 
         gfx.position.copyFrom(node as IPointData);
+
+        updateNodeLabel(node);
       });
 
       if (stopped === graph.current.nodes.length) {
@@ -623,7 +733,8 @@ export const NLPixi = (props: Props) => {
         nodeMap.current.clear();
         linkGfx.clear();
         nodeLayer.removeChildren();
-        labelLayer.removeChildren();
+        linkLabelLayer.removeChildren();
+        nodeLabelLayer.removeChildren();
       }
 
       nodeMap.current.forEach((gfx, id) => {
@@ -634,11 +745,11 @@ export const NLPixi = (props: Props) => {
         }
       });
 
-      labelMap.current.forEach((text, id) => {
+      linkLabelMap.current.forEach((text, id) => {
         if (!graph.current.links.find((link) => link._id === id)) {
-          labelLayer.removeChild(text);
+          linkLabelLayer.removeChild(text);
           text.destroy();
-          labelMap.current.delete(id);
+          linkLabelMap.current.delete(id);
         }
       });
 
@@ -651,6 +762,7 @@ export const NLPixi = (props: Props) => {
           node.x = old?.x || node.x;
           node.y = old?.y || node.y;
           updateNode(node);
+          updateNodeLabel(node);
         } else {
           createNode(node);
         }
@@ -658,7 +770,7 @@ export const NLPixi = (props: Props) => {
 
       if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
         graph.current.links.forEach((link) => {
-          if (!forceClear && labelMap.current.has(link._id)) {
+          if (!forceClear && linkLabelMap.current.has(link._id)) {
             updateLinkLabel(link);
           } else {
             createLinkLabel(link);
@@ -694,7 +806,7 @@ export const NLPixi = (props: Props) => {
    */
   const setup = async () => {
     nodeLayer.removeChildren();
-    labelLayer.removeChildren();
+    linkLabelLayer.removeChildren();
     app.stage.removeChildren();
 
     if (!props.graph) throw Error('Graph is undefined');
@@ -723,8 +835,9 @@ export const NLPixi = (props: Props) => {
     viewport.current.drag().pinch().wheel({ smooth: 2 }).animate({}).decelerate({ friction: 0.75 });
 
     viewport.current.addChild(linkGfx);
-    viewport.current.addChild(labelLayer);
+    viewport.current.addChild(linkLabelLayer);
     viewport.current.addChild(nodeLayer);
+    viewport.current.addChild(nodeLabelLayer);
     viewport.current.on('moved', (event) => {
       imperative.current.onMoved(event);
     });
-- 
GitLab