diff --git a/apps/web/public/assets/sprite.png b/apps/web/public/assets/sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9d77b0520b6b4945bbe38e04553e4e349f1794e
Binary files /dev/null and b/apps/web/public/assets/sprite.png differ
diff --git a/apps/web/public/assets/sprite_selected.png b/apps/web/public/assets/sprite_selected.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd57dcc236de77693873365fdf8d007f2349d2ba
Binary files /dev/null and b/apps/web/public/assets/sprite_selected.png differ
diff --git a/apps/web/public/assets/sprite_selected_square.png b/apps/web/public/assets/sprite_selected_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..eb7a57e4f70d60c5096d07f0cd53803454285099
Binary files /dev/null and b/apps/web/public/assets/sprite_selected_square.png differ
diff --git a/apps/web/public/assets/sprite_square.png b/apps/web/public/assets/sprite_square.png
new file mode 100644
index 0000000000000000000000000000000000000000..38a4cd208703a2e481ca4337430f631026c6bed0
Binary files /dev/null and b/apps/web/public/assets/sprite_square.png differ
diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx
index 9fea3c4c9518091e2cc3b5449f02aafdd2712550..be780115dfe688aaa70531a18a15341176fb18a6 100644
--- a/libs/shared/lib/components/dropdowns/index.tsx
+++ b/libs/shared/lib/components/dropdowns/index.tsx
@@ -109,7 +109,7 @@ export function DropdownItem({ value, disabled, className, onClick, submenu, sel
       onMouseEnter={() => setIsSubmenuOpen(true)}
       onMouseLeave={() => setIsSubmenuOpen(false)}
     >
-      {value}
+      { value }
       {submenu && isSubmenuOpen && <DropdownSubmenuContainer ref={submenuRef}>{submenu}</DropdownSubmenuContainer>}
     </li>
   );
diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx
index c3a3e9838297450c562f4e8dc66c2a73d1a4d809..df18203eb3a9295a4e7bb3ed0a5c8602d6fba336 100644
--- a/libs/shared/lib/components/inputs/index.tsx
+++ b/libs/shared/lib/components/inputs/index.tsx
@@ -96,7 +96,7 @@ type DropdownProps = {
   disabled?: boolean;
   className?: string;
   value?: number | string;
-  options: number[] | string[];
+  options: number[] | string[] | {[key: string]: string}[];
   onChange?: (value: number | string) => void;
 };
 
@@ -371,6 +371,23 @@ export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps)
   );
 };
 
+function parsedValue(item: number | string | {[key: string]: string}, key: boolean = false) {
+  switch (typeof item) {
+    case 'number': return item.toString();
+    case 'object': return key ? Object.keys(item)[0] : Object.values(item)[0];
+  }
+
+  return item;
+}
+
+function currentValue(value: string | number | undefined, options?: {[key: string]: string}[]) {
+  if (options != null && typeof options[0] == 'object') {
+    return parsedValue(options.find(x => x[value as string]) as {[key: string]: string});
+  }
+
+  return value;
+}
+
 export const DropdownInput = ({
   label,
   value,
@@ -411,7 +428,7 @@ export const DropdownInput = ({
         >
           <DropdownTrigger
             variant={buttonVariant}
-            title={overrideRender || value}
+            title={overrideRender || currentValue(value, options as {[key: string]: string}[]) }
             size={size}
             className="cursor-pointer"
             disabled={disabled}
@@ -427,12 +444,11 @@ export const DropdownInput = ({
                 options.map((item: any, index: number) => (
                   <DropdownItem
                     key={index}
-                    value={item.toString()}
+                    value={parsedValue(item)}
                     selected={value === item}
                     onClick={() => {
-                      const parsedValue = typeof item === 'number' ? item.toString() : item;
                       if (onChange) {
-                        onChange(parsedValue);
+                        onChange(parsedValue(item, true));
                       }
                       setIsDropdownOpen(false);
                     }}
diff --git a/libs/shared/lib/graph-layout/graphology-layouts.ts b/libs/shared/lib/graph-layout/graphology-layouts.ts
index dd82b709330de3d1a9580b2413c6310ee8f01e37..67419cbb7b80d399a9810a954cdf0054d2b7127d 100644
--- a/libs/shared/lib/graph-layout/graphology-layouts.ts
+++ b/libs/shared/lib/graph-layout/graphology-layouts.ts
@@ -198,18 +198,27 @@ export class GraphologyForceAtlas2Webworker extends GraphologyLayout {
 
     const sensibleSettings = forceAtlas2.inferSettings(graph);
 
-    const layout = new FA2Layout(graph, {
-      settings: {
-        ...this.defaultLayoutSettings,
-        ...sensibleSettings,
-        adjustSizes: graph.order < 300 ? true : false,
-      },
-    });
+    let settings = {
+      ...this.defaultLayoutSettings,
+      ...sensibleSettings,
+      adjustSizes: graph.order < 300 ? true : false
+    };
+
+    if (graph.order > 5000) {
+      settings = {
+        ...settings,
+        barnesHutOptimize: true,
+        barnesHutTheta: 0.75,
+        slowDown: 0.75
+      };
+    }
+
+    const layout = new FA2Layout(graph, { settings });
     layout.start();
 
-    // stop the layout after 5 seconds
+    // stop the layout after 10 seconds
     setTimeout(() => {
       layout.stop();
-    }, 20000);
+    }, 10000);
   }
 }
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index 0b14261d003eb4d635a0ededcaad8034d69c7238..2f41de1a81adbd269dcff404a90d3e0439750403 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
@@ -1,7 +1,7 @@
 import { GraphType, LinkType, NodeType } from '../types';
 import { dataColors, visualizationColors } from 'config';
 import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
-import { Application, Circle, Color, Container, FederatedPointerEvent, Graphics, IPointData } from 'pixi.js';
+import { Application, AssetsBundle, Color, Container, FederatedPointerEvent, Graphics, IPointData, Sprite, Assets, Texture, Resource } from 'pixi.js';
 import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access';
 import { NLPopup } from './NLPopup';
 import { hslStringToHex, nodeColor } from './utils';
@@ -30,6 +30,16 @@ type LayoutState = 'reset' | 'running' | 'paused';
 // MAIN COMPONENT
 //////////////////
 
+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');
+}
+
 export const NLPixi = (props: Props) => {
   const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>();
   const [popups, setPopups] = useState<{ node: NodeType; pos: IPointData }[]>([]);
@@ -46,10 +56,9 @@ export const NLPixi = (props: Props) => {
     [],
   );
   const nodeLayer = useMemo(() => new Container(), []);
-  const linkLayer = useMemo(() => new Container(), []);
 
-  const nodeMap = useRef(new Map<string, Graphics>());
-  const linkMap = useRef(new Map<string, Graphics>());
+  const nodeMap = useRef(new Map<string, Sprite>());
+  const linkGfx = new Graphics();
   const viewport = useRef<Viewport>();
   const layoutState = useRef<LayoutState>('reset');
   const layoutStoppedCount = useRef(0);
@@ -57,7 +66,7 @@ export const NLPixi = (props: Props) => {
   const mouseInCanvas = useRef<boolean>(false);
   const isSetup = useRef(false);
   const ml = useML();
-  const dragging = useRef<{ node: NodeType; gfx: Graphics } | null>(null);
+  const dragging = useRef<{ node: NodeType; gfx: Sprite } | null>(null);
   const onlyClicked = useRef(false);
   const searchResults = useSearchResultData();
 
@@ -66,6 +75,12 @@ 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 [config, setConfig] = useState({
     width: 1000,
     height: 1000,
@@ -73,10 +88,10 @@ export const NLPixi = (props: Props) => {
     LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER,
 
     NODE_RADIUS: 5,
-    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],
-    NODE_BORDER_COLOR_SELECTED: dataColors.orange[60],
+    // 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],
+    // NODE_BORDER_COLOR_SELECTED: dataColors.orange[60],
 
     LINE_COLOR_DEFAULT: dataColors.neutral[40],
     LINE_COLOR_SELECTED: visualizationColors.GPSelected.colors[1],
@@ -93,10 +108,19 @@ 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);
 
   useImperativeHandle(imperative, () => ({
-    onDragStart(node: NodeType, gfx: Graphics) {
+    onDragStart(node: NodeType, gfx: Sprite) {
       dragging.current = { node, gfx };
       onlyClicked.current = true;
 
@@ -208,7 +232,7 @@ export const NLPixi = (props: Props) => {
     } else return { x: 0, y: 0 };
   }
 
-  function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Graphics) {
+  function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Sprite) {
     event.stopPropagation();
     if (imperative.current) imperative.current.onDragStart(node, gfx);
   }
@@ -227,22 +251,16 @@ export const NLPixi = (props: Props) => {
     const gfx = nodeMap.current.get(node._id);
     if (!gfx) return;
 
-    const lineColor = node.isShortestPathSource
-      ? dataColors.orange['95']
-      : node.isShortestPathTarget
-        ? dataColors.blue['95']
-        : node.selected
-          ? config.NODE_BORDER_COLOR_SELECTED
-          : config.NODE_BORDER_COLOR_DEFAULT;
-    const lineWidth = node.selected ? config.NODE_BORDER_LINE_WIDTH_SELECTED : config.NODE_BORDER_LINE_WIDTH;
-    gfx.lineStyle(lineWidth, new Color(hslStringToHex(lineColor)));
+    // Update texture when selected
+    const texture = Assets.get(textureId(node.selected));
+    gfx.texture = texture;
 
+    // Cluster colors
     if (node?.cluster) {
-      gfx.beginFill(node.cluster >= 0 ? nodeColor(node.cluster) : 0x000000);
-    } else gfx.beginFill(nodeColor(node.type));
-
-    gfx.drawCircle(0, 0, Math.max(node.radius || 5, 5));
-    gfx.endFill();
+      gfx.tint = node.cluster >= 0 ? nodeColor(node.cluster) : 0x000000;
+    } else {
+      gfx.tint = nodeColor(node.type);
+    }
 
     gfx.position.set(node.x, node.y);
 
@@ -278,12 +296,21 @@ export const NLPixi = (props: Props) => {
 
   const createNode = (node: NodeType, selected?: boolean) => {
     // check if node is already drawn, and if so, delete it
-    if (node && node?._id && nodeMap.current?.has(node._id)) {
+    if (node && node?._id && nodeMap.current.has(node._id)) {
       nodeMap.current.delete(node._id);
     }
     // Do not draw node if it has no position
     if (node.x === undefined || node.y === undefined) return;
-    const gfx = new Graphics();
+
+    let gfx: Sprite;
+    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.scale.set(scale, scale);
+    gfx.anchor.set(0.5, 0.5);
+
     nodeMap.current.set(node._id, gfx);
     nodeLayer.addChild(gfx);
     node.selected = selected;
@@ -303,8 +330,8 @@ export const NLPixi = (props: Props) => {
   //   });
   // };
 
-  const updateLink = (link: LinkType, gfx: Graphics) => {
-    if (!props.graph) return;
+  const updateLink = (link: LinkType) => {
+    if (!props.graph || nodeMap.current.size === 0) return;
 
     const _source = link.source;
     const _target = link.target;
@@ -335,7 +362,7 @@ export const NLPixi = (props: Props) => {
       return;
     }
 
-    if (gfx) {
+    if (linkGfx) {
       // let color = link.color || 0x000000;
       let color = config.LINE_COLOR_DEFAULT;
       let style = config.LINE_WIDTH_DEFAULT;
@@ -366,36 +393,27 @@ export const NLPixi = (props: Props) => {
         style = 3.0;
       }
 
-      gfx.clear();
-      gfx.beginFill();
-      gfx
+      // 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);
+        alpha = isLinkInSearchResults ? 1 : 0.05;
+      }
+
+      linkGfx
         .lineStyle(style, hslStringToHex(color), alpha)
         .moveTo(source.x || 0, source.y || 0)
         .lineTo(target.x || 0, target.y || 0);
-      gfx.endFill();
     } else {
       throw Error('Link not found');
     }
   };
 
-  const createLink = (link: LinkType) => {
-    if (linkMap.current.has(link.id)) {
-      linkMap.current.delete(link.id);
-    }
-    const gfx = new Graphics();
-    gfx.name = 'link_' + link.id;
-    linkMap.current.set(link.id, gfx);
-    updateLink(link, gfx);
-    linkLayer.addChild(gfx);
-    return gfx;
-  };
-
   useEffect(() => {
     return () => {
       nodeMap.current.clear();
-      linkMap.current.clear();
+      linkGfx.clear();
       nodeLayer.removeChildren();
-      linkLayer.removeChildren();
     };
   }, []);
 
@@ -412,15 +430,9 @@ export const NLPixi = (props: Props) => {
         const gfx = nodeMap.current.get(node._id);
         if (!gfx) return;
         const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node._id);
+        
         gfx.alpha = isNodeInSearchResults || searchResults.nodes.length === 0 ? 1 : 0.05;
       });
-
-      props.graph.links.forEach((link: LinkType) => {
-        const gfx = linkMap.current.get(link.id);
-        if (!gfx) return;
-        const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link.id);
-        gfx.alpha = isLinkInSearchResults || searchResults.edges.length === 0 ? 1 : 0.05;
-      });
     }
   }, [searchResults]);
 
@@ -432,6 +444,9 @@ export const NLPixi = (props: Props) => {
       if (!layoutAlgorithm.current) return;
 
       let stopped = 0;
+
+      const widthHalf = app.renderer.width / 2;
+      const heightHalf = app.renderer.height / 2;
       props.graph.nodes.forEach((node: NodeType, i) => {
         if (!layoutAlgorithm.current) return;
         const gfx = nodeMap.current.get(node._id);
@@ -441,7 +456,7 @@ export const NLPixi = (props: Props) => {
 
         if (
           !position ||
-          Math.abs(node.x - position.x - app.renderer.width / 2) + Math.abs(node.y - position.y - app.renderer.height / 2) < 1
+          Math.abs(node.x - position.x - widthHalf) + Math.abs(node.y - position.y - heightHalf) < 1
         ) {
           stopped += 1;
           return;
@@ -449,8 +464,8 @@ export const NLPixi = (props: Props) => {
         try {
           if (layoutAlgorithm.current.provider === 'Graphology') {
             // this is a dirty hack to fix the graphology layout being out of bounds
-            node.x = position.x + app.renderer.width / 2;
-            node.y = position.y + app.renderer.height / 2;
+            node.x = position.x + widthHalf;
+            node.y = position.y + heightHalf;
           } else {
             node.x = position.x;
             node.y = position.y;
@@ -460,14 +475,7 @@ export const NLPixi = (props: Props) => {
           layoutState.current = 'paused';
         }
 
-        if (layoutState.current === 'running') {
-          gfx.position.copyFrom({
-            x: node.fx || gfx.position.x + ((node.x || 0) - gfx.position.x) * 0.1 * delta,
-            y: node.fy || gfx.position.y + ((node.y || 0) - gfx.position.y) * 0.1 * delta,
-          });
-        } else {
-          gfx.position.copyFrom(node as IPointData);
-        }
+        gfx.position.copyFrom(node as IPointData);
       });
 
       if (stopped === props.graph.nodes.length) {
@@ -483,12 +491,13 @@ export const NLPixi = (props: Props) => {
         layoutState.current = 'running';
       }
 
-      // Update forces of the links
+      // Draw the links
+      linkGfx.clear();
+      linkGfx.beginFill();
       props.graph.links.forEach((link: any) => {
-        if (linkMap.current && !!linkMap.current.has(link.id)) {
-          updateLink(link, linkMap.current.get(link.id) as Graphics);
-        }
+        updateLink(link);
       });
+      linkGfx.endFill();
     }
   };
 
@@ -499,9 +508,8 @@ export const NLPixi = (props: Props) => {
     if (props.graph) {
       if (forceClear) {
         nodeMap.current.clear();
-        linkMap.current.clear();
+        linkGfx.clear();
         nodeLayer.removeChildren();
-        linkLayer.removeChildren();
       }
 
       nodeMap.current.forEach((gfx, id) => {
@@ -512,30 +520,23 @@ export const NLPixi = (props: Props) => {
         }
       });
 
-      linkMap.current.forEach((gfx, id) => {
-        if (!props.graph?.links?.find((link) => link.id === id)) {
-          linkLayer.removeChild(gfx);
-          gfx.destroy();
-          linkMap.current.delete(id);
-        }
-      });
+      linkGfx.clear();
 
       props.graph.nodes.forEach((node: NodeType) => {
         if (!forceClear && nodeMap.current.has(node._id)) {
           const old = nodeMap.current.get(node._id);
-          node.x = old?.x || node.x;
-          node.y = old?.y || node.y;
-          updateNode(node);
+          
+          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
+          }
         } else {
           createNode(node);
         }
       });
-      props.graph.links.forEach((link: LinkType) => {
-        if (!forceClear && linkMap.current.has(link.id)) {
-          const gfx = linkMap.current.get(link.id);
-          if (gfx) updateLink(link, gfx);
-        } else createLink(link);
-      });
 
       // // update text colour (written after nodes so that text appears on top of nodes)
       //   nodes.forEach((node: NodeType) => {
@@ -563,9 +564,8 @@ export const NLPixi = (props: Props) => {
    * It creates graphic objects and adds these to the PIXI containers. It also clears both of these of previous nodes and links.
    * @param graph The graph returned from the database and that is parsed into a nodelist and edgelist.
    */
-  const setup = () => {
+  const setup = async () => {
     nodeLayer.removeChildren();
-    linkLayer.removeChildren();
     app.stage.removeChildren();
 
     if (!props.graph) throw Error('Graph is undefined');
@@ -583,7 +583,7 @@ export const NLPixi = (props: Props) => {
     // activate plugins
     viewport.current.drag().pinch().wheel({ smooth: 2 }).animate({}).decelerate({ friction: 0.75 });
 
-    viewport.current.addChild(linkLayer);
+    viewport.current.addChild(linkGfx);
     viewport.current.addChild(nodeLayer);
     viewport.current.on('drag-start', (event) => {
       imperative.current.onPan();
@@ -595,7 +595,7 @@ export const NLPixi = (props: Props) => {
     app.stage.on('mouseup', onDragEnd);
 
     nodeMap.current.clear();
-    linkMap.current.clear();
+    linkGfx.clear();
     update(true);
     isSetup.current = true;
   };
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
index f5c06cf4f16b16791e0905214d21118e8ec22c76..70c7867ba36adf9dbfe162a15f597dbce985a20d 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
@@ -177,7 +177,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
               type="dropdown"
               label="Shape"
               value={settings.shapes.shape}
-              options={['Circle', 'Square']}
+              options={[{circle: 'Circle'}, {rectangle: 'Square'}]}
               onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })}
             />
           ) : (