diff --git a/apps/web/public/assets/sprite.png b/apps/web/public/assets/sprite.png
deleted file mode 100644
index a9d77b0520b6b4945bbe38e04553e4e349f1794e..0000000000000000000000000000000000000000
Binary files a/apps/web/public/assets/sprite.png and /dev/null differ
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
Binary files a/apps/web/public/assets/sprite_selected.png and /dev/null differ
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
Binary files a/apps/web/public/assets/sprite_selected_square.png and /dev/null differ
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
Binary files a/apps/web/public/assets/sprite_square.png and /dev/null differ
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index 6e1277c661cae41d4b710a484e4465caa1bdf286..d9c2966882c3dee3e5b32ffe46345ec2852c8740 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);
     });