diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
index 7ffc49974198a52eebccb49ded91fc989d92e957..b97e11e612d5212202dc52f9e6e86e4482bb48e6 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
@@ -17,6 +17,7 @@ export function processLinkPrediction(ml: ML, graph: GraphType): GraphType {
           value: link.attributes.jaccard_coefficient as number,
           mlEdge: true,
           color: 0x000000,
+          attributes: {},
         };
         graph.links[toAdd.id] = toAdd;
       }
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index 9d45b688d55114a935b0cd333b419d443235eff5..8dd8460eb2118fab9f1d53735a2bb0edff15dcfb 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
@@ -102,7 +102,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     graphics.lineStyle(lineWidth, 0x4e586a);
     graphics.beginFill(0xffffff, 1);
 
-    if (props.configuration.shapes?.shape == 'circle') {
+    if (props.configuration.nodes?.shape.type == "circle") {
       graphics.drawCircle(size / 2 + lineWidth / 2, size / 2 + lineWidth / 2, size / 2);
     } else {
       graphics.drawRect(lineWidth, lineWidth, size - lineWidth, size - lineWidth);
@@ -155,7 +155,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     graph.current.nodes.forEach((node: any) => {
       updateNodeLabel(node);
     });
-  }, [responsiveScale, props.configuration.shapes?.shape]);
+  }, [responsiveScale, props.configuration.nodes?.shape?.type]);
 
   const [config, setConfig] = useState({
     width: 1000,
@@ -179,11 +179,11 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
 
   const glyphTexture = useMemo(() => {
     return getTexture(_glyphTexture);
-  }, [responsiveScale, props.configuration.shapes?.shape]);
+  }, [responsiveScale, props.configuration.nodes?.shape?.type]);
 
   const selectedTexture = useMemo(() => {
     return getTexture(_selectedTexture, true);
-  }, [responsiveScale, props.configuration.shapes?.shape]);
+  }, [responsiveScale, props.configuration.nodes?.shape?.type]);
 
   useEffect(() => {
     setConfig((lastConfig) => {
@@ -427,6 +427,8 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
 
     // Update texture when selected
     const nodeMeta = props.graph.nodes[node._id];
+    if (nodeMeta == null) return;
+
     const texture = (gfx as any).selected ? selectedTexture : glyphTexture;
     gfx.texture = texture;
 
@@ -456,8 +458,61 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
   };
 
   const getNodeLabel = (nodeMeta: NodeType) => {
-    return nodeMeta.label;
-  };
+    try {
+      var attribute = props.configuration.nodes.labelAttributes[nodeMeta.label];
+    } catch(e) {
+      return nodeMeta.label;
+    }
+
+    if (attribute == 'Default' || attribute == null) {
+      return nodeMeta.label;
+    }
+
+    
+    const value = nodeMeta.attributes[attribute];
+
+    if (Array.isArray(value)) {
+      return value.join(', ');
+    }
+
+    if (typeof value === 'number' || typeof value === 'string') {
+      return String(value);
+    }
+
+    if (typeof value === 'object' && Object.keys(value).length != 0) {
+      return JSON.stringify(value);
+    }
+
+    return '-';
+  }
+
+  const getLinkLabel = (linkMeta: LinkType) => {
+    try {
+      var attribute = props.configuration.edges.labelAttributes[linkMeta.name];
+    } catch(e) {
+      return linkMeta.name;
+    }
+    
+    if (attribute == 'Default' || attribute == null) {
+      return linkMeta.name;
+    }
+
+    const value = linkMeta.attributes[attribute];
+
+    if (Array.isArray(value)) {
+      return value.join(', ');
+    }
+
+    if (typeof value === 'number' || typeof value === 'string') {
+      return String(value);
+    }
+
+    if (typeof value === 'object' && Object.keys(value).length != 0) {
+      return JSON.stringify(value);
+    }
+
+    return '';
+  }
 
   const createNode = (node: NodeTypeD3, selected?: boolean) => {
     const nodeMeta = props.graph.nodes[node._id];
@@ -497,8 +552,9 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
       fontSize: 20,
       fill: 0xffffff,
       wordWrap: true,
-      wordWrapWidth: 65,
-      align: 'center',
+      breakWords: true,
+      wordWrapWidth: config.NODE_RADIUS,
+      align: 'center'
     });
     text.eventMode = 'none';
     text.cullable = true;
@@ -520,7 +576,8 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
 
     const linkMeta = props.graph.links[link._id];
 
-    const text = new Text(linkMeta.name, {
+    const label = getLinkLabel(linkMeta);
+    const text = new Text(label, {
       fontSize: 60,
       fill: config.LINE_COLOR_DEFAULT,
       stroke: imperative.current.getBackgroundColor(),
@@ -628,6 +685,9 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     const source = nodeMap.current.get(link.source as string) as Sprite;
     const target = nodeMap.current.get(link.target as string) as Sprite;
 
+    const linkMeta = props.graph.links[link._id];
+    text.text = getLinkLabel(linkMeta);
+
     text.x = (source.x + target.x) / 2;
     text.y = (source.y + target.y) / 2;
 
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
index 2c6d73ce61d024cad07742c33c9c12043ce425ab..3e649a9910ea30adda7054bcad6aa3df19f22a83 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
@@ -264,6 +264,7 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
         name: uniqueEdges[i].attributes.Type,
         mlEdge: false,
         color: 0x000000,
+        attributes: uniqueEdges[i].attributes
       };
       ret.links[toAdd.id] = toAdd;
     }
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
index 8a4857f6dd3f50cc8fb00275c0801e0f4cde7206..a2d1c931383daa4212981d02706ef6447104482c 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
@@ -13,6 +13,26 @@ import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResul
 import { IPointData } from 'pixi.js';
 import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common';
 
+  
+// For backwards compatibility with older saveStates, we migrate information from settings.nodes to settings.location
+// FIXME: this can be removed once all systems have updated their saveStates.
+function patchLegacySettings(settings: NodelinkVisProps): NodelinkVisProps {
+  if (!('nodes' in settings)) {
+    settings = JSON.parse(JSON.stringify(settings));  // Undo Object.preventExtensions()
+
+    settings.nodes = {
+      shape: {
+        type: (settings as any).shapes.shape,
+        similar: (settings as any).shapes.similar,
+        shapeMap: undefined
+      },
+      labelAttributes: {}
+    };
+    settings.edges.labelAttributes = {};
+  }
+  return settings;
+}
+
 export interface NodeLinkVisHandle {
   exportImageInternal: () => void;
 }
@@ -22,16 +42,20 @@ export interface NodelinkVisProps {
   name: string;
   layout: string;
   showPopUpOnHover: boolean;
-  shapes: {
-    similar: boolean;
-    shape: 'circle' | 'rectangle';
-    shapeMap: { [id: string]: 'circle' | 'rectangle' } | undefined;
-  };
+  nodes: {
+    shape: {
+      similar: boolean;
+      type: 'circle' | 'rectangle';
+      shapeMap: { [id: string]: 'circle' | 'rectangle' } | undefined;
+    };
+    labelAttributes: { [nodeType: string]: string };
+  },
   edges: {
     width: {
       similar: boolean;
       width: number;
     };
+    labelAttributes: { [nodeType: string]: string };
   };
   nodeList: string[];
 }
@@ -41,13 +65,17 @@ const settings: NodelinkVisProps = {
   name: 'NodeLinkVis',
   layout: Layouts.FORCEATLAS2WEBWORKER as string,
   showPopUpOnHover: false,
-  shapes: {
-    similar: true,
-    shape: 'circle',
-    shapeMap: undefined,
+  nodes: {
+    shape: {
+      similar: true,
+      type: 'circle',
+      shapeMap: undefined,
+    },
+    labelAttributes: {},
   },
   edges: {
     width: { similar: true, width: 0.8 },
+    labelAttributes: {},
   },
   nodeList: [],
 };
@@ -60,6 +88,8 @@ const NodeLinkVis = forwardRef<NodeLinkVisHandle, VisualizationPropTypes<Nodelin
     const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]);
     const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]);
 
+    settings = patchLegacySettings(settings);
+  
     useEffect(() => {
       if (data) {
         setGraph(
@@ -147,6 +177,8 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
 
   if (!settings.nodeList) return null;
 
+  settings = patchLegacySettings(settings);
+
   return (
     <SettingsContainer>
       <div className="mb-4 text-xs">
@@ -187,16 +219,16 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
           <Input
             type="boolean"
             label="Common shape?"
-            value={settings.shapes.similar}
-            onChange={(val) => updateSettings({ shapes: { ...settings.shapes, similar: val } })}
+            value={settings.nodes?.shape.similar}
+            onChange={(val) => updateSettings({ nodes: { ...settings.nodes, shape: { ...settings.nodes.shape, similar: val } } })}
           />
-          {settings.shapes.similar ? (
+          {settings.nodes?.shape?.similar ? (
             <Input
               type="dropdown"
               label="Shape"
-              value={settings.shapes.shape}
+              value={settings.nodes?.shape.type}
               options={[{ circle: 'Circle' }, { rectangle: 'Square' }]}
-              onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })}
+              onChange={(val) => updateSettings({ nodes: { ...settings.nodes, shape: { ...settings.nodes.shape, type: val as any } } })}
             />
           ) : (
             <span>Map shapes to labels (to be implemented)</span>
@@ -204,7 +236,17 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
         </div>
 
         <div>
-          <span className="text-xs font-semibold">Color</span>
+          <span className="text-xs font-semibold">Labels</span>
+          { Object.entries(graphMetadata.nodes.types).map(([label, type]) =>
+            <Input
+              type="dropdown"
+              key={label}
+              label={label}
+              value={settings.nodes.labelAttributes ? settings.nodes.labelAttributes[label] || 'Default' : undefined}
+              options={['Default', ...Object.keys(type.attributes).filter(x => x != 'labels')]}
+              onChange={(val) => updateSettings({ nodes: { ...settings.nodes, labelAttributes: { ... settings.nodes.labelAttributes, [label]: val as string } } })}
+            />
+          )}  
         </div>
       </div>
 
@@ -229,6 +271,20 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
           />
         </div>
       </div>
+      <div>
+        <span className="text-xs font-semibold">Labels</span>
+        { Object.entries(graphMetadata.edges.types).map(([label, type]) =>
+          <Input
+            type="dropdown"
+            key={label}
+            label={label}
+            value={settings.edges.labelAttributes ? settings.edges.labelAttributes[label] || 'Default' : undefined}
+            options={['Default', ...Object.keys(type.attributes).filter(x => x != 'Type')]}
+            onChange={(val) => updateSettings({ edges: { ...settings.edges, labelAttributes: { ... settings.edges.labelAttributes, [label]: val as string } } })}
+          />
+        )}  
+      </div>
+      
     </SettingsContainer>
   );
 };
@@ -239,7 +295,7 @@ export const NodeLinkComponent: VISComponentType<NodelinkVisProps> = {
   description: 'General Patterns and Connections',
   component: React.forwardRef((props: VisualizationPropTypes<NodelinkVisProps>, ref) => <NodeLinkVis {...props} ref={nodeLinkVisRef} />),
   settingsComponent: NodelinkSettings,
-  settings: settings,
+  settings: patchLegacySettings(settings),
   exportImage: () => {
     if (nodeLinkVisRef.current) {
       nodeLinkVisRef.current.exportImageInternal();
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
index 4d039f916bd1aae4c5130e08f5ffb14e5288ca3c..64b03fde046e8805e2db95e90e0d018b42aeb484 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
@@ -64,6 +64,7 @@ export interface LinkType {
   alpha?: number;
   source: string;
   target: string;
+  attributes: Record<string, any>;
 }
 
 export type LinkTypeD3 = d3.SimulationLinkDatum<NodeTypeD3> & { _id: string };