diff --git a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
index 2df47ed97db9e42f97693c17809c5a53141d7414..6ce5e968127eff10977481a6d2e206222f59b4a3 100644
--- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
+++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
@@ -143,10 +143,88 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
     graphMetadata.edges.labels.forEach((label: string) => {
       const layerId = `${label}-edges-line`;
 
-      const edgeData = data.edges.filter(edge => {
-        const from = nodeLocations[edge.from];
-        const to = nodeLocations[edge.to];
-        return from && to && !hiddenNodes.has(edge.from) && !hiddenNodes.has(edge.to);
+      const showArrows = layerSettings?.showArrows;
+
+      const parseEdge = (edge: EdgeQueryResult) => {
+        const [sx, sy] = nodeLocations[edge.from];
+        const [tx, ty] = nodeLocations[edge.to];
+
+        const [wsx, wsy] = this.context.viewport.projectPosition([sx, sy, 1]);
+        // eslint-disable-next-line prefer-const
+        let [wtx, wty, wtz] = this.context.viewport.projectPosition([tx, ty, 1]);
+
+        if (showArrows) {
+          // Perpendicular normalized vector
+          let ax = wtx - wsx;
+          let ay = wty - wsy;
+          const amag = Math.sqrt(ax * ax + ay * ay);
+          if (amag != 0) {
+            ax /= amag;
+            ay /= amag;
+          }
+
+          const nodeRadius = wtz * 40;
+
+          if (wtx - wsx != 0) wtx -= (nodeRadius / amag) * (wtx - wsx);
+          if (wty - wsy != 0) wty -= (nodeRadius / amag) * (wty - wsy);
+
+          // Draw arrow heads
+          const arrowSize = nodeRadius / 7;
+          const arrowRatio = 1.75;
+
+          let px = wty - wsy;
+          let py = -(wtx - wsx);
+          const pmag = Math.sqrt(px * px + py * py);
+          if (pmag != 0) {
+            px /= pmag;
+            py /= pmag;
+          }
+
+          const arrow1_x = px - ax * arrowRatio;
+          const arrow1_y = py - ay * arrowRatio;
+          const arrow2_x = -px - ax * arrowRatio;
+          const arrow2_y = -py - ay * arrowRatio;
+
+          return [
+            // Arrow head line 1
+            {
+              label: edge.label,
+              from: this.context.viewport.unprojectPosition([wtx, wty]),
+              to: this.context.viewport.unprojectPosition([wtx + arrow1_x * arrowSize, wty + arrow1_y * arrowSize]),
+              attributes: edge.attributes,
+            },
+            // Edge
+            {
+              _id: edge._id,
+              label: edge.label,
+              attributes: edge.attributes,
+              from: [sx, sy],
+              fromID: edge.from,
+              to: this.context.viewport.unprojectPosition([wtx, wty]),
+              toID: edge.to,
+            },
+            // Arrow head line 2
+            {
+              label: edge.label,
+              from: this.context.viewport.unprojectPosition([wtx, wty]),
+              to: this.context.viewport.unprojectPosition([wtx + arrow2_x * arrowSize, wty + arrow2_y * arrowSize]),
+              attributes: edge.attributes,
+            },
+          ];
+        } else {
+          return {
+            _id: edge._id,
+            label: edge.label,
+            attributes: edge.attributes,
+            from: [sx, sy],
+            fromID: edge.from,
+            to: [tx, ty],
+            toID: edge.to,
+          };
+        }
+      };
+      const edgeData = data.edges.flatMap(parseEdge).filter(edge => {
+        return edge != null && edge.from && edge.to && !hiddenNodes.has(edge.fromID) && !hiddenNodes.has(edge.toID);
       });
 
       this._layers[layerId] = new LineLayer({
@@ -155,8 +233,8 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
         visible: !layerSettings?.edges[label]?.hidden,
         pickable: true,
         getWidth: layerSettings?.edges[label]?.width,
-        getSourcePosition: d => getNodeLocation(d.from),
-        getTargetPosition: d => getNodeLocation(d.to),
+        getSourcePosition: d => d.from,
+        getTargetPosition: d => d.to,
         getColor: (d: EdgeQueryResult) => this.getEdgeColor(d),
         extensions: [brushingExtension],
         brushingEnabled: layerSettings?.enableBrushing,
@@ -169,8 +247,8 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
         data: ml.linkPrediction.result,
         pickable: false,
         getWidth: 1,
-        getSourcePosition: d => getNodeLocation(d.from),
-        getTargetPosition: d => getNodeLocation(d.to),
+        getSourcePosition: d => d.from,
+        getTargetPosition: d => d.to,
         getColor: d => [0, 0, 0],
       });
     }
diff --git a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
index 85a2821895b1b0d9da99022384f551a68c51feeb..d54313a49226e4da4e678eba9906ec3398acf820 100644
--- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
+++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
@@ -30,6 +30,7 @@ const defaultEdgeSettings = () => ({
   colorByAttribute: false,
   colorAttribute: undefined,
   colorAttributeType: undefined,
+  showArrows: false,
 });
 
 export function NodeLinkOptions({
@@ -275,6 +276,15 @@ export function NodeLinkOptions({
                     }}
                   />
 
+                  <Input
+                    label="Show arrows"
+                    type="boolean"
+                    value={settings?.showArrows}
+                    onChange={val => {
+                      updateLayerSettings({ showArrows: val as boolean });
+                    }}
+                  />
+
                   <Accordion>
                     <AccordionItem>
                       <AccordionHead>