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 8ad7df06d3c256b04e58e5b48754ec051eb80c20..efe109ce47ebaf4be2c4d8782f3bf3d805f3ed6d 100644
--- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
+++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
@@ -1,5 +1,5 @@
 import { CompositeLayer, Layer, Viewport } from 'deck.gl';
-import { LineLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers';
+import { LineLayer, ScatterplotLayer, TextLayer, SolidPolygonLayer } from '@deck.gl/layers';
 import { CompositeLayerType, Coordinate, LayerProps, EDGE_COLOR_DEFAULT } from '../../mapvis.types';
 import { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions';
 import { scaleLinear, ScaleLinear, color, interpolateRgb } from 'd3';
@@ -142,6 +142,7 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
 
     graphMetadata.edges.labels.forEach((label: string) => {
       const layerId = `${label}-edges-line`;
+      const arrowPolygons: any[] = [];
 
       const parseEdge = (edge: EdgeQueryResult) => {
         if (edge == null || edge?.attributes?.hidden) return [];
@@ -166,15 +167,13 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
             ay /= amag;
           }
 
-          const nodeRadius = wtz * 40;
+          const nodeTo = data.nodes.find(n => n._id == edge.to)!;
+          const nodeRadius = wtz * getNodeSize(nodeTo, label);
 
           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);
@@ -183,40 +182,23 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
             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
-            {
-              _id: edge._id + '_arrow1',
-              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,
-              hidden: hidden,
-            },
-            // Edge
-            {
-              _id: edge._id,
-              label: edge.label,
-              attributes: edge.attributes,
-              from: [sx, sy, 0] as [number, number, number],
-              to: this.context.viewport.unprojectPosition([wtx, wty]),
-              hidden: hidden,
-            },
-            // Arrow head line 2
-            {
-              _id: edge._id + '_arrow2',
-              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,
-              hidden: hidden,
-            },
-          ];
+          arrowPolygons.push({
+            _id: edge._id + '_arrow',
+            label: edge.label,
+            vectors: { wtx, wty, px, py, ax, ay },
+            attributes: edge.attributes,
+            hidden: hidden,
+          });
+
+          // Return the edge line so both the arrow and edge are rendered.
+          return {
+            _id: edge._id,
+            label: edge.label,
+            attributes: edge.attributes,
+            from: [sx, sy, 0] as [number, number, number],
+            to: this.context.viewport.unprojectPosition([wtx, wty]),
+            hidden: hidden,
+          };
         } else {
           return {
             _id: edge._id,
@@ -244,6 +226,45 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
         extensions: [brushingExtension],
         brushingEnabled: layerSettings?.enableBrushing,
       });
+
+      // Create a new layer for filled arrow triangles if any exist.
+      if (arrowPolygons.length > 0) {
+        const arrowLayerId = `${label}-arrows`;
+        this._layers[arrowLayerId] = new SolidPolygonLayer({
+          id: arrowLayerId,
+          data: arrowPolygons.filter(p => !p.hidden),
+          visible: !layerSettings?.edges[label]?.hidden,
+          pickable: true,
+          // each data object has polygon property containing 3 points
+          getPolygon: d => {
+            const { wtx, wty, px, py, ax, ay } = d.vectors;
+            const arrowSize = 10;
+            const arrowRatio = 1.75;
+            const arrowSizeScaled = arrowSize / 2 ** this.context.viewport.zoom;
+
+            const tip = this.context.viewport.unprojectPosition([wtx, wty]);
+            const point1 = this.context.viewport.unprojectPosition([
+              wtx + (px - ax * arrowRatio) * arrowSizeScaled,
+              wty + (py - ay * arrowRatio) * arrowSizeScaled,
+            ]);
+            const pointMid = this.context.viewport.unprojectPosition([wtx - ax * arrowSizeScaled * 0.8, wty - ay * arrowSizeScaled * 0.8]);
+            const point2 = this.context.viewport.unprojectPosition([
+              wtx + (-px - ax * arrowRatio) * arrowSizeScaled,
+              wty + (-py - ay * arrowRatio) * arrowSizeScaled,
+            ]);
+
+            return [tip, point1, pointMid, point2];
+          },
+          getFillColor: (d: EdgeQueryResult) => this.getEdgeColor(d),
+        });
+
+        // ensure that arrow heads are rerendered every zoom event. Quite a performance hit!
+        this.context.deck!.setProps({
+          onViewStateChange: () => {
+            this.setNeedsUpdate();
+          },
+        });
+      }
     });
 
     if (ml?.linkPrediction?.enabled) {
@@ -292,14 +313,15 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
           return nodeColorRGB(layerSettings?.nodes[label].color);
         },
         getPosition: (d: NodeQueryResult) => getNodeLocation(d._id),
-        getRadius: () => layerSettings?.nodes[label]?.size,
+        // Dynamically compute node radius based on its degree
+        getRadius: (d: NodeQueryResult) => getNodeSize(d, label),
         radiusMinPixels: 5,
-        getLineWidth: (d: NodeQueryResult) => (selected && selected.some(sel => sel._id === d._id) ? 2 : 1),
-        lineWidthUnits: 'pixels',
+        // getLineWidth: (d: NodeQueryResult) => (selected && selected.some(sel => sel._id === d._id) ? 2 : 1),
+        // lineWidthUnits: 'pixels',
         stroked: true,
         updateTriggers: {
           getIcon: [selected],
-          getRadius: [layerSettings?.nodes[label].size],
+          getRadius: [layerSettings?.nodes[label]?.size, data.edges],
           getFillColor: [this.state.colorScales],
         },
       });
@@ -328,6 +350,12 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
       });
     });
 
+    function getNodeSize(d: NodeQueryResult, label: string) {
+      const baseSize = layerSettings?.nodes[label]?.size ?? 40;
+      const relationCount = data.edges.filter(edge => edge.from === d._id || edge.to === d._id).length;
+      return baseSize + relationCount * (layerSettings?.nodeSizeMultiplier ?? 0);
+    }
+
     function getEdgeLocation(edge: EdgeQueryResult, viewport: Viewport) {
       const locationFrom = viewport.projectPosition([...getNodeLocation(edge.from), 1]);
       const locationTo = viewport.projectPosition([...getNodeLocation(edge.to), 1]);
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 5debb05a91856a5093510836b501897fc1afe7f7..425bcee6f0c2e60c0004a816c6d0262d38db83a3 100644
--- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
+++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
@@ -62,7 +62,7 @@ export function NodeLinkOptions({
   updateSpatialAttribute,
 }: LayerSettingsComponentType<MapProps>) {
   const layerType = 'nodelink';
-  const layerSettings = settings[layerType] ?? { enableBrushing: false, nodes: {}, edges: {} };
+  const layerSettings = settings[layerType] ?? { enableBrushing: false, nodes: {}, edges: {}, nodeSizeMultiplier: 0 };
 
   useEffect(() => {
     const nodes = layerSettings.nodes ?? {};
@@ -434,6 +434,23 @@ export function NodeLinkOptions({
             );
           })}
         </Accordion>
+        <div>
+          <span className="text-xs font-semibold">Node Size Degree Multiplier</span>
+          <Input
+            type="slider"
+            label="Node Size Degree Multiplier"
+            size="sm"
+            className="my-1"
+            tooltip="Multiplies the size of the node by the the number of connections the node has."
+            value={layerSettings.nodeSizeMultiplier}
+            onChangeConfirmed={val => {
+              updateLayerSettings({ nodeSizeMultiplier: val });
+            }}
+            min={0}
+            max={10000}
+            step={100}
+          />
+        </div>
       </div>
     )
   );