diff --git a/.env.development b/.env.development
index aab317865c4e399dd82b0e237171783951d6e8f0..2b9c6f14484c2b4d1b2b8c86a48fbf73f1bbc64d 100644
--- a/.env.development
+++ b/.env.development
@@ -28,5 +28,6 @@ VIS1D=true
 INSIGHT_SHARING=true
 VIEWER_PERMISSIONS=true
 SHARABLE_EXPLORATION=true
+EDGE_BUNDLING=true
 
 MAPBOX_TOKEN=
\ No newline at end of file
diff --git a/.env.example b/.env.example
index c8b01e2637900885fe49be5dd24748e42f4dc066..6988f791bc6ecad00982c90b728beca80d9664c4 100644
--- a/.env.example
+++ b/.env.example
@@ -27,5 +27,6 @@ VIS1D=true
 INSIGHT_SHARING=true
 VIEWER_PERMISSIONS=true
 SHARABLE_EXPLORATION=true
+EDGE_BUNDLING=true
 
 MAPBOX_TOKEN=
\ No newline at end of file
diff --git a/src/config/envVariables.ts b/src/config/envVariables.ts
index 192875eeaf5ac34658ee806cbdc3ccf4f90d83d8..29b2274fabc9b6281f87983be068117242845d21 100644
--- a/src/config/envVariables.ts
+++ b/src/config/envVariables.ts
@@ -30,6 +30,7 @@ export const envVar = {
   INSIGHT_SHARING: import.meta.env.INSIGHT_SHARING,
   VIEWER_PERMISSIONS: import.meta.env.VIEWER_PERMISSIONS,
   SHARABLE_EXPLORATION: import.meta.env.SHARABLE_EXPLORATION,
+  EDGE_BUNDLING: import.meta.env.EDGE_BUNDLING,
 
   MAPBOX_TOKEN: import.meta.env.MAPBOX_TOKEN,
 };
diff --git a/src/lib/components/featureFlags/featureFlags.ts b/src/lib/components/featureFlags/featureFlags.ts
index aaf2db9bbeb856926660434b0719011fe22c3cb9..0463406f0d83f8898a0e08f36d120e83e3ae8052 100644
--- a/src/lib/components/featureFlags/featureFlags.ts
+++ b/src/lib/components/featureFlags/featureFlags.ts
@@ -34,6 +34,7 @@ export const FEATURE_FLAGS = {
   INSIGHT_SHARING: getFeatureVariable('INSIGHT_SHARING'),
   VIEWER_PERMISSIONS: getFeatureVariable('VIEWER_PERMISSIONS'),
   SHARABLE_EXPLORATION: getFeatureVariable('SHARABLE_EXPLORATION'),
+  EDGE_BUNDLING: getFeatureVariable('EDGE_BUNDLING'),
 } as const satisfies Record<string, boolean>;
 
 export type FeatureFlagName = keyof typeof FEATURE_FLAGS;
diff --git a/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index 446962cbe2e2f0f19664c96ac31e8b28c626c7fa..17b139c1871771de99e8d11b86eb146fe94598f4 100644
--- a/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
+++ b/src/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
@@ -25,6 +25,8 @@ import { GraphType, GraphTypeD3, EdgeType, EdgeTypeD3, NodeType, NodeTypeD3 } fr
 import { NLPopUp } from './NLPopup';
 import { nodeColor, nodeColorHex } from './utils';
 import { useAsyncMemo } from '@/utils';
+import { ForceEdgeBundling, type Point } from './edgeBundling';
+import { canViewFeature } from '@/lib/components/featureFlags';
 
 type Props = {
   onClick: (event?: { node: NodeTypeD3; pos: PointData }) => void;
@@ -55,6 +57,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     update();
   }, [globalConfig.currentTheme]);
 
+  let edgeBundling: Point[][];
   const ref = useRef<HTMLDivElement>(null);
   const canvas = useRef<HTMLCanvasElement>(null);
   const app = useAsyncMemo(async () => {
@@ -221,7 +224,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
         LAYOUT_ALGORITHM: props.layoutAlgorithm || lastConfig.LAYOUT_ALGORITHM,
       };
     });
-  }, [props.layoutAlgorithm, props.configuration]);
+  }, [props.layoutAlgorithm, props.configuration, props.configuration.edgeBundlingEnabled]);
 
   const imperative = useRef<any>(null);
 
@@ -375,6 +378,9 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     getNodeAttributes() {
       return props.configuration.nodes.labelAttributes;
     },
+    getEdgeBundlingEnabled() {
+      return (canViewFeature('EDGE_BUNDLING') && props.configuration.edgeBundlingEnabled) ?? false;
+    },
   }));
 
   useImperativeHandle(refExternal, () => ({
@@ -645,7 +651,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     return text;
   };
 
-  const updateEdge = (edge: EdgeTypeD3) => {
+  const updateEdge = (edge: EdgeTypeD3, edgeBundle?: Point[]) => {
     if (!props.graph || nodeMap.current.size === 0) return;
     const edgeMeta = props.graph.edges[edge._id];
 
@@ -653,7 +659,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
       ? graph.current.edges.filter(
           x => (x.source == edgeMeta.source && x.target == edgeMeta.target) || (x.source == edgeMeta.target && x.target == edgeMeta.source),
         ).length
-      : null;
+      : 0;
 
     const _source = edge.source;
     const _target = edge.target;
@@ -683,42 +689,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
       return;
     }
 
-    // let color = edge.color || 0x000000;
-    let color = config.LINE_COLOR_DEFAULT;
-    let style = imperative.current.getEdgeWidth();
-    let alpha = edgeMeta.alpha || 1;
-    if (edgeMeta.mlEdge) {
-      color = config.LINE_COLOR_ML;
-      if (edgeMeta.value > ml.communityDetection.jaccard_threshold) {
-        style = edgeMeta.value * 1.8;
-      } else {
-        style = 0;
-        alpha = 0.2;
-      }
-    } else if (props.highlightedLinks && props.highlightedLinks.includes(edgeMeta)) {
-      if (edgeMeta.mlEdge && ml.communityDetection.jaccard_threshold) {
-        if (edgeMeta.value > ml.communityDetection.jaccard_threshold) {
-          color = dataColors.magenta[50];
-          // 0xaa00ff;
-          style = edgeMeta.value * 1.8;
-        }
-      } else {
-        color = dataColors.red[70];
-        // color = 0xff0000;
-        style = 1.0;
-      }
-    } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(edgeMeta)) {
-      color = dataColors.green[50];
-      // color = 0x00ff00;
-      style = 3.0;
-    }
-
-    // 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 === edge._id);
-      alpha = isLinkInSearchResults ? 1 : 0.05;
-    }
+    const { style, color, alpha } = getEdgeStyle(edgeMeta);
 
     const sx = source.x as number;
     const sy = source.y as number;
@@ -741,7 +712,6 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     }
 
     // Draw the edge
-
     // - Self-loops
     if (sourceId === targetId && target.x != null && target.y != null) {
       const selfLoopSize = 30;
@@ -765,7 +735,19 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     }
 
     // - Regular edge
-    if (imperative.current.getShowMultipleEdges() && multiple > 1) {
+    if (edgeBundle != null) {
+      edgeGfx.moveTo(edgeBundle[0].x, edgeBundle[0].y);
+
+      edgeBundle.forEach(p => {
+        edgeGfx.lineTo(p.x, p.y);
+      });
+
+      edgeGfx.stroke({
+        width: style,
+        color: color,
+        alpha: alpha,
+      });
+    } else if (imperative.current.getShowMultipleEdges() && multiple > 1) {
       // Perpendicular vector
       let px = ty - sy;
       let py = -(tx - sx);
@@ -839,6 +821,47 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     }
   };
 
+  const getEdgeStyle = (edgeMeta: EdgeType) => {
+    // let color = edge.color || 0x000000;
+    let color = config.LINE_COLOR_DEFAULT;
+    let style = imperative.current.getEdgeWidth();
+    let alpha = edgeMeta.alpha || 1;
+    if (edgeMeta.mlEdge) {
+      color = config.LINE_COLOR_ML;
+      if (edgeMeta.value > ml.communityDetection.jaccard_threshold) {
+        style = edgeMeta.value * 1.8;
+      } else {
+        style = 0;
+        alpha = 0.2;
+      }
+    } else if (props.highlightedLinks && props.highlightedLinks.includes(edgeMeta)) {
+      if (edgeMeta.mlEdge && ml.communityDetection.jaccard_threshold) {
+        if (edgeMeta.value > ml.communityDetection.jaccard_threshold) {
+          color = dataColors.magenta[50];
+          // 0xaa00ff;
+          style = edgeMeta.value * 1.8;
+        }
+      } else {
+        color = dataColors.red[70];
+        // color = 0xff0000;
+        style = 1.0;
+      }
+    } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(edgeMeta)) {
+      color = dataColors.green[50];
+      // color = 0x00ff00;
+      style = 3.0;
+    }
+
+    // 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 === edge._id);
+      alpha = isLinkInSearchResults ? 1 : 0.05;
+    }
+
+    return { style, color, alpha };
+  };
+
   const updateEdgeLabel = (edge: EdgeTypeD3) => {
     if (graph.current.nodes.length > config.LABEL_MAX_NODES || viewport.current!.scale.x < 2) return;
 
@@ -965,7 +988,19 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
 
   const tick = () => {
     if (app == null) return;
-    if (layoutState.current === 'paused') return;
+    if (layoutState.current === 'paused') {
+      if (edgeBundling == null && imperative.current.getEdgeBundlingEnabled()) {
+        edgeBundling = ForceEdgeBundling()
+          .nodes(
+            graph.current.nodes.reduce((a, b) => {
+              return { ...a, [b._id]: { x: b.x, y: b.y } };
+            }, {}),
+          )
+          .edges(graph.current.edges)();
+      } else {
+        return;
+      }
+    }
     if (layoutState.current === 'reset') layoutStoppedCount.current = 0;
 
     if (props.graph) {
@@ -1019,8 +1054,13 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
 
       // Draw the edges
       edgeGfx.clear();
-      graph.current.edges.forEach((link: any) => {
-        updateEdge(link);
+
+      graph.current.edges.forEach((link: any, i: number) => {
+        if (edgeBundling != null && imperative.current.getEdgeBundlingEnabled()) {
+          updateEdge(link, edgeBundling[i]); // FIXME: edgeBundling omits self-loops, index may not always match exactly!
+        } else {
+          updateEdge(link);
+        }
         updateEdgeLabel(link);
       });
 
diff --git a/src/lib/vis/visualizations/nodelinkvis/components/edgeBundling.tsx b/src/lib/vis/visualizations/nodelinkvis/components/edgeBundling.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8f09d24137974a15988a5610eb03524dc810b7b8
--- /dev/null
+++ b/src/lib/vis/visualizations/nodelinkvis/components/edgeBundling.tsx
@@ -0,0 +1,476 @@
+/* 
+ FDEB algorithm implementation [www.win.tue.nl/~dholten/papers/forcebundles_eurovis.pdf].
+
+ Author: Corneliu S. (github.com/upphiminn)
+ 2013
+ */
+
+export type Point = { _id?: string; x: number; y: number };
+type Line = { target: Point; source: Point };
+type Edge = { _id?: string; target: string; source: string };
+
+export const ForceEdgeBundling = function () {
+  let data_nodes: { [nodeid: string]: Point } = {}, // {'nodeid':{'x':,'y':},..}
+    data_edges: Edge[] = [], // [{'source':'nodeid1', 'target':'nodeid2'},..]
+    K = 0.1, // global bundling constant controlling edge stiffness
+    S_initial = 0.1, // init. distance to move points
+    P_rate = 2, // subdivision rate increase
+    C = 6, // number of cycles to perform
+    I_initial = 90, // init. number of iterations for cycle
+    I_rate = 0.6666667, // rate at which iteration number decreases i.e. 2/3
+    compatibility_threshold = 0.6,
+    P: Edge | null = null;
+
+  const compatibility_list_for_edge: number[][] = [],
+    subdivision_points_for_edge: Point[][] = [],
+    P_initial = 1, // init. subdivision number
+    eps = 1e-6;
+
+  /*** Geometry Helper Methods ***/
+  function vector_dot_product(p: Point, q: Point) {
+    return p.x * q.x + p.y * q.y;
+  }
+
+  function edge_as_vector(P: Edge) {
+    return {
+      x: data_nodes[P.target].x - data_nodes[P.source].x,
+      y: data_nodes[P.target].y - data_nodes[P.source].y,
+    };
+  }
+
+  function edge_length(e: Edge) {
+    // handling nodes that are on the same location, so that K/edge_length != Inf
+    if (
+      Math.abs(data_nodes[e.source].x - data_nodes[e.target].x) < eps &&
+      Math.abs(data_nodes[e.source].y - data_nodes[e.target].y) < eps
+    ) {
+      return eps;
+    }
+
+    return Math.sqrt(
+      Math.pow(data_nodes[e.source].x - data_nodes[e.target].x, 2) + Math.pow(data_nodes[e.source].y - data_nodes[e.target].y, 2),
+    );
+  }
+
+  function custom_edge_length(e) {
+    return Math.sqrt(Math.pow(e.source.x - e.target.x, 2) + Math.pow(e.source.y - e.target.y, 2));
+  }
+
+  function edge_midpoint(e: Edge) {
+    const middle_x = (data_nodes[e.source].x + data_nodes[e.target].x) / 2.0;
+    const middle_y = (data_nodes[e.source].y + data_nodes[e.target].y) / 2.0;
+
+    return {
+      x: middle_x,
+      y: middle_y,
+    };
+  }
+
+  function compute_divided_edge_length(e_idx: number) {
+    let length = 0;
+
+    for (let i = 1; i < subdivision_points_for_edge[e_idx].length; i++) {
+      const segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]);
+      length += segment_length;
+    }
+
+    return length;
+  }
+
+  function euclidean_distance(p: Point, q: Point) {
+    return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2));
+  }
+
+  function project_point_on_line(p: Point, Q: Line) {
+    const L = Math.sqrt((Q.target.x - Q.source.x) * (Q.target.x - Q.source.x) + (Q.target.y - Q.source.y) * (Q.target.y - Q.source.y));
+    const r = ((Q.source.y - p.y) * (Q.source.y - Q.target.y) - (Q.source.x - p.x) * (Q.target.x - Q.source.x)) / (L * L);
+
+    return {
+      x: Q.source.x + r * (Q.target.x - Q.source.x),
+      y: Q.source.y + r * (Q.target.y - Q.source.y),
+    };
+  }
+
+  /*** ********************** ***/
+
+  /*** Initialization Methods ***/
+  function initialize_edge_subdivisions() {
+    for (let i = 0; i < data_edges.length; i++) {
+      if (P_initial === 1) {
+        subdivision_points_for_edge[i] = []; //0 subdivisions
+      } else {
+        subdivision_points_for_edge[i] = [];
+        subdivision_points_for_edge[i].push(data_nodes[data_edges[i].source]);
+        subdivision_points_for_edge[i].push(data_nodes[data_edges[i].target]);
+      }
+    }
+  }
+
+  function initialize_compatibility_lists() {
+    for (let i = 0; i < data_edges.length; i++) {
+      compatibility_list_for_edge[i] = []; //0 compatible edges.
+    }
+  }
+
+  function filter_self_loops(edgelist) {
+    const filtered_edge_list = [];
+
+    for (let e = 0; e < edgelist.length; e++) {
+      if (
+        data_nodes[edgelist[e].source].x != data_nodes[edgelist[e].target].x ||
+        data_nodes[edgelist[e].source].y != data_nodes[edgelist[e].target].y
+      ) {
+        //or smaller than eps
+        filtered_edge_list.push(edgelist[e]);
+      }
+    }
+
+    return filtered_edge_list;
+  }
+
+  /*** ********************** ***/
+
+  /*** Force Calculation Methods ***/
+  function apply_spring_force(e_idx: number, i: number, kP: number) {
+    const prev = subdivision_points_for_edge[e_idx][i - 1];
+    const next = subdivision_points_for_edge[e_idx][i + 1];
+    const crnt = subdivision_points_for_edge[e_idx][i];
+    let x = prev.x - crnt.x + next.x - crnt.x;
+    let y = prev.y - crnt.y + next.y - crnt.y;
+
+    x *= kP;
+    y *= kP;
+
+    return {
+      x: x,
+      y: y,
+    };
+  }
+
+  function apply_electrostatic_force(e_idx: number, i: number) {
+    const sum_of_forces = {
+      x: 0,
+      y: 0,
+    };
+    const compatible_edges_list = compatibility_list_for_edge[e_idx];
+
+    for (let oe = 0; oe < compatible_edges_list.length; oe++) {
+      const force = {
+        x: subdivision_points_for_edge[compatible_edges_list[oe]][i].x - subdivision_points_for_edge[e_idx][i].x,
+        y: subdivision_points_for_edge[compatible_edges_list[oe]][i].y - subdivision_points_for_edge[e_idx][i].y,
+      };
+
+      if (Math.abs(force.x) > eps || Math.abs(force.y) > eps) {
+        const diff =
+          1 /
+          Math.pow(
+            custom_edge_length({
+              source: subdivision_points_for_edge[compatible_edges_list[oe]][i],
+              target: subdivision_points_for_edge[e_idx][i],
+            }),
+            1,
+          );
+
+        sum_of_forces.x += force.x * diff;
+        sum_of_forces.y += force.y * diff;
+      }
+    }
+
+    return sum_of_forces;
+  }
+
+  function apply_resulting_forces_on_subdivision_points(e_idx: number, P: number, S: number) {
+    const kP = K / (edge_length(data_edges[e_idx]) * (P + 1)); // kP=K/|P|(number of segments), where |P| is the initial length of edge P.
+    // (length * (num of sub division pts - 1))
+    const resulting_forces_for_subdivision_points = [
+      {
+        x: 0,
+        y: 0,
+      },
+    ];
+
+    for (let i = 1; i < P + 1; i++) {
+      // exclude initial end points of the edge 0 and P+1
+      const resulting_force = {
+        x: 0,
+        y: 0,
+      };
+
+      const spring_force = apply_spring_force(e_idx, i, kP);
+      const electrostatic_force = apply_electrostatic_force(e_idx, i);
+
+      resulting_force.x = S * (spring_force.x + electrostatic_force.x);
+      resulting_force.y = S * (spring_force.y + electrostatic_force.y);
+
+      resulting_forces_for_subdivision_points.push(resulting_force);
+    }
+
+    resulting_forces_for_subdivision_points.push({
+      x: 0,
+      y: 0,
+    });
+
+    return resulting_forces_for_subdivision_points;
+  }
+
+  /*** ********************** ***/
+
+  /*** Edge Division Calculation Methods ***/
+  function update_edge_divisions(P: number) {
+    for (let e_idx = 0; e_idx < data_edges.length; e_idx++) {
+      if (P === 1) {
+        subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].source]); // source
+        subdivision_points_for_edge[e_idx].push(edge_midpoint(data_edges[e_idx])); // mid point
+        subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].target]); // target
+      } else {
+        const divided_edge_length = compute_divided_edge_length(e_idx);
+        const segment_length = divided_edge_length / (P + 1);
+        let current_segment_length = segment_length;
+        const new_subdivision_points = [];
+        new_subdivision_points.push(data_nodes[data_edges[e_idx].source]); //source
+
+        for (let i = 1; i < subdivision_points_for_edge[e_idx].length; i++) {
+          let old_segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]);
+
+          while (old_segment_length > current_segment_length) {
+            const percent_position = current_segment_length / old_segment_length;
+            let new_subdivision_point_x = subdivision_points_for_edge[e_idx][i - 1].x;
+            let new_subdivision_point_y = subdivision_points_for_edge[e_idx][i - 1].y;
+
+            new_subdivision_point_x +=
+              percent_position * (subdivision_points_for_edge[e_idx][i].x - subdivision_points_for_edge[e_idx][i - 1].x);
+            new_subdivision_point_y +=
+              percent_position * (subdivision_points_for_edge[e_idx][i].y - subdivision_points_for_edge[e_idx][i - 1].y);
+            new_subdivision_points.push({
+              x: new_subdivision_point_x,
+              y: new_subdivision_point_y,
+            });
+
+            old_segment_length -= current_segment_length;
+            current_segment_length = segment_length;
+          }
+          current_segment_length -= old_segment_length;
+        }
+        new_subdivision_points.push(data_nodes[data_edges[e_idx].target]); //target
+        subdivision_points_for_edge[e_idx] = new_subdivision_points;
+      }
+    }
+  }
+
+  /*** ********************** ***/
+
+  /*** Edge compatibility measures ***/
+  function angle_compatibility(P: Edge, Q: Edge) {
+    return Math.abs(vector_dot_product(edge_as_vector(P), edge_as_vector(Q)) / (edge_length(P) * edge_length(Q)));
+  }
+
+  function scale_compatibility(P: Edge, Q: Edge) {
+    const lavg = (edge_length(P) + edge_length(Q)) / 2.0;
+    return 2.0 / (lavg / Math.min(edge_length(P), edge_length(Q)) + Math.max(edge_length(P), edge_length(Q)) / lavg);
+  }
+
+  function position_compatibility(P: Edge, Q: Edge) {
+    const lavg = (edge_length(P) + edge_length(Q)) / 2.0;
+    const midP = {
+      x: (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0,
+      y: (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0,
+    };
+    const midQ = {
+      x: (data_nodes[Q.source].x + data_nodes[Q.target].x) / 2.0,
+      y: (data_nodes[Q.source].y + data_nodes[Q.target].y) / 2.0,
+    };
+
+    return lavg / (lavg + euclidean_distance(midP, midQ));
+  }
+
+  function edge_visibility(P: Edge, Q: Edge) {
+    const I0 = project_point_on_line(data_nodes[Q.source], {
+      source: data_nodes[P.source],
+      target: data_nodes[P.target],
+    });
+    const I1 = project_point_on_line(data_nodes[Q.target], {
+      source: data_nodes[P.source],
+      target: data_nodes[P.target],
+    }); //send actual edge points positions
+    const midI = {
+      x: (I0.x + I1.x) / 2.0,
+      y: (I0.y + I1.y) / 2.0,
+    };
+    const midP = {
+      x: (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0,
+      y: (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0,
+    };
+
+    return Math.max(0, 1 - (2 * euclidean_distance(midP, midI)) / euclidean_distance(I0, I1));
+  }
+
+  function visibility_compatibility(P: Edge, Q: Edge) {
+    return Math.min(edge_visibility(P, Q), edge_visibility(Q, P));
+  }
+
+  function compatibility_score(P: Edge, Q: Edge) {
+    return angle_compatibility(P, Q) * scale_compatibility(P, Q) * position_compatibility(P, Q) * visibility_compatibility(P, Q);
+  }
+
+  function are_compatible(P: Edge, Q: Edge) {
+    return compatibility_score(P, Q) >= compatibility_threshold;
+  }
+
+  function compute_compatibility_lists() {
+    for (let e = 0; e < data_edges.length - 1; e++) {
+      for (let oe = e + 1; oe < data_edges.length; oe++) {
+        // don't want any duplicates
+        if (are_compatible(data_edges[e], data_edges[oe])) {
+          compatibility_list_for_edge[e].push(oe);
+          compatibility_list_for_edge[oe].push(e);
+        }
+      }
+    }
+  }
+
+  /*** ************************ ***/
+
+  /*** Main Bundling Loop Methods ***/
+  // TODO: make class
+  const forcebundle = function () {
+    let S = S_initial;
+    let I = I_initial;
+    let P = P_initial;
+
+    initialize_edge_subdivisions();
+    initialize_compatibility_lists();
+    update_edge_divisions(P);
+    compute_compatibility_lists();
+
+    for (let cycle = 0; cycle < C; cycle++) {
+      for (let iteration = 0; iteration < I; iteration++) {
+        const forces = [];
+        for (let edge = 0; edge < data_edges.length; edge++) {
+          forces[edge] = apply_resulting_forces_on_subdivision_points(edge, P, S);
+        }
+        for (let e = 0; e < data_edges.length; e++) {
+          for (let i = 0; i < P + 1; i++) {
+            subdivision_points_for_edge[e][i].x += forces[e][i].x;
+            subdivision_points_for_edge[e][i].y += forces[e][i].y;
+          }
+        }
+      }
+      // prepare for next cycle
+      S = S / 2;
+      P = P * P_rate;
+      I = I_rate * I;
+
+      update_edge_divisions(P);
+      //console.log('C' + cycle);
+      //console.log('P' + P);
+      //console.log('S' + S);
+    }
+    return subdivision_points_for_edge;
+  };
+  /*** ************************ ***/
+
+  /*** Getters/Setters Methods ***/
+  forcebundle.nodes = function (nl) {
+    if (arguments.length === 0) {
+      return data_nodes;
+    } else {
+      data_nodes = nl;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.edges = function (ll) {
+    if (arguments.length === 0) {
+      return data_edges;
+    } else {
+      data_edges = filter_self_loops(ll); //remove edges to from to the same point
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.bundling_stiffness = function (k) {
+    if (arguments.length === 0) {
+      return K;
+    } else {
+      K = k;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.step_size = function (step) {
+    if (arguments.length === 0) {
+      return S_initial;
+    } else {
+      S_initial = step;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.cycles = function (c) {
+    if (arguments.length === 0) {
+      return C;
+    } else {
+      C = c;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.iterations = function (i) {
+    if (arguments.length === 0) {
+      return I_initial;
+    } else {
+      I_initial = i;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.iterations_rate = function (i) {
+    if (arguments.length === 0) {
+      return I_rate;
+    } else {
+      I_rate = i;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.subdivision_points_seed = function (p) {
+    if (arguments.length == 0) {
+      return P;
+    } else {
+      P = p;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.subdivision_rate = function (r) {
+    if (arguments.length === 0) {
+      return P_rate;
+    } else {
+      P_rate = r;
+    }
+
+    return forcebundle;
+  };
+
+  forcebundle.compatibility_threshold = function (t) {
+    if (arguments.length === 0) {
+      return compatibility_threshold;
+    } else {
+      compatibility_threshold = t;
+    }
+
+    return forcebundle;
+  };
+
+  /*** ************************ ***/
+
+  return forcebundle;
+};
diff --git a/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
index 8fb7801f405577f57731c45dbf91430c418c6b44..91e1b3a5c662d52f56052a66ff5e8fc8dc40112c 100644
--- a/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
+++ b/src/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
@@ -12,6 +12,7 @@ import { nodeColorHex } from './components/utils';
 import { NodeQueryResult, ML } from 'ts-common';
 import { type PointData } from 'pixi.js';
 import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common';
+import { canViewFeature } from '@/lib/components/featureFlags';
 
 // 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.
@@ -59,6 +60,7 @@ export type NodelinkVisProps = {
   nodeList: string[];
   showArrows: boolean;
   showMultipleEdges: boolean;
+  edgeBundlingEnabled: boolean;
 };
 
 const settings: NodelinkVisProps = {
@@ -81,6 +83,7 @@ const settings: NodelinkVisProps = {
   nodeList: [],
   showArrows: false,
   showMultipleEdges: true,
+  edgeBundlingEnabled: false,
 };
 
 const NodeLinkVis = forwardRef<NodeLinkVisHandle, VisualizationPropTypes<NodelinkVisProps>>(
@@ -161,6 +164,7 @@ const NodeLinkVis = forwardRef<NodeLinkVisHandle, VisualizationPropTypes<Nodelin
         }}
         layoutAlgorithm={settings.layout}
         showPopupsOnHover={settings.showPopUpOnHover}
+        edgeBundlingEnabled={settings.edgeBundlingEnabled}
       />
     );
   },
@@ -277,6 +281,16 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
           onChange={val => updateSettings({ showMultipleEdges: val })}
         />
       </div>
+      {canViewFeature('EDGE_BUNDLING') ? (
+        <div>
+          <Input
+            type="boolean"
+            label="Edge bundling"
+            value={settings.edgeBundlingEnabled}
+            onChange={val => updateSettings({ edgeBundlingEnabled: val })}
+          />
+        </div>
+      ) : null}
     </SettingsContainer>
   );
 };