diff --git a/cypress/integration/frontend-testing.js b/cypress/integration/frontend-testing.js
index 83b02ec2912ab042159badeac5657c503e0e0996..20fb852d8ec736f6c4f77a6dc9a896e27d3d01d9 100644
--- a/cypress/integration/frontend-testing.js
+++ b/cypress/integration/frontend-testing.js
@@ -18,7 +18,7 @@ const PANE_DATA_MAP = {
     dump: {name: 'Tree/RTL', selector: 'view-gccdump'},
     tree: {name: 'Tree', selector: 'view-gnatdebugtree'},
     debug: {name: 'Debug', selector: 'view-gnatdebug'},
-    cfg: {name: 'Graph', selector: 'view-cfg'},
+    cfg: {name: 'CFG', selector: 'view-cfg'},
 };
 
 describe('Individual pane testing', () => {
diff --git a/lib/cfg.js b/lib/cfg.js
index dbaefe2b3fcc5d4e659e53f1d82679529a121e7b..64d09b26bb50b6173b0b4b7fe13a85cd981dce75 100644
--- a/lib/cfg.js
+++ b/lib/cfg.js
@@ -217,7 +217,7 @@ function splitToCanonicalBasicBlock(basicBlock) {
 
 function concatInstructions(asmArr, first, last) {
     return _.chain(asmArr.slice(first, last))
-        .map(x => x.text.substr(0, 50))
+        .map(x => x.text)
         .value()
         .join('\n');
 }
diff --git a/package-lock.json b/package-lock.json
index 7ce65f5fe5d760c0018c0036da7ed0f461b0eeeb..e0206614356c466fb27dd5d736a60c9ddac50f88 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
       "version": "0.0.3",
       "license": "BSD-2-Clause",
       "dependencies": {
+        "@flatten-js/interval-tree": "^1.0.18",
         "@fortawesome/fontawesome-free": "^5.15.4",
         "@sentry/browser": "^6.16.1",
         "@sentry/node": "^6.16.1",
@@ -71,7 +72,6 @@
         "tslib": "^2.3.1",
         "underscore": "^1.13.2",
         "url-join": "^4.0.1",
-        "vis-network": "^9.1.0",
         "whatwg-fetch": "^3.6.2",
         "which": "^2.0.2",
         "winston": "^3.3.3",
@@ -701,18 +701,6 @@
         "node": ">=10.0.0"
       }
     },
-    "node_modules/@egjs/hammerjs": {
-      "version": "2.0.17",
-      "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
-      "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
-      "peer": true,
-      "dependencies": {
-        "@types/hammerjs": "^2.0.36"
-      },
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
     "node_modules/@es-joy/jsdoccomment": {
       "version": "0.20.1",
       "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.20.1.tgz",
@@ -791,6 +779,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/@flatten-js/interval-tree": {
+      "version": "1.0.18",
+      "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.0.18.tgz",
+      "integrity": "sha512-o72sZErW0Y1C82Cg7nk82ojJ/22EtmKyp5I3eNqgcOKFp/VCzetATYYjJIqOBBaR7FQ/MFj/ZpsmP38mL4TkYA=="
+    },
     "node_modules/@fortawesome/fontawesome-free": {
       "version": "5.15.4",
       "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
@@ -1926,12 +1919,6 @@
         "@types/node": "*"
       }
     },
-    "node_modules/@types/hammerjs": {
-      "version": "2.0.41",
-      "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
-      "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==",
-      "peer": true
-    },
     "node_modules/@types/http-proxy": {
       "version": "1.17.9",
       "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz",
@@ -4014,7 +4001,8 @@
     "node_modules/component-emitter": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
-      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+      "dev": true
     },
     "node_modules/compressible": {
       "version": "2.0.18",
@@ -8415,12 +8403,6 @@
         "safe-buffer": "^5.0.1"
       }
     },
-    "node_modules/keycharm": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
-      "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==",
-      "peer": true
-    },
     "node_modules/kind-of": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -13785,12 +13767,6 @@
       "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
       "dev": true
     },
-    "node_modules/timsort": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
-      "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
-      "peer": true
-    },
     "node_modules/tiny-emitter": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
@@ -14357,6 +14333,7 @@
       "version": "8.3.2",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
       "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "dev": true,
       "bin": {
         "uuid": "dist/bin/uuid"
       }
@@ -14407,57 +14384,6 @@
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
       "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
     },
-    "node_modules/vis-data": {
-      "version": "7.1.4",
-      "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz",
-      "integrity": "sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==",
-      "hasInstallScript": true,
-      "peer": true,
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/visjs"
-      },
-      "peerDependencies": {
-        "uuid": "^7.0.0 || ^8.0.0",
-        "vis-util": "^4.0.0 || ^5.0.0"
-      }
-    },
-    "node_modules/vis-network": {
-      "version": "9.1.2",
-      "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz",
-      "integrity": "sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==",
-      "hasInstallScript": true,
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/visjs"
-      },
-      "peerDependencies": {
-        "@egjs/hammerjs": "^2.0.0",
-        "component-emitter": "^1.3.0",
-        "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0",
-        "timsort": "^0.3.0",
-        "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0",
-        "vis-data": "^7.0.0",
-        "vis-util": "^5.0.1"
-      }
-    },
-    "node_modules/vis-util": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
-      "integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
-      "peer": true,
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/visjs"
-      },
-      "peerDependencies": {
-        "@egjs/hammerjs": "^2.0.0",
-        "component-emitter": "^1.3.0"
-      }
-    },
     "node_modules/void-elements": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
@@ -15809,15 +15735,6 @@
       "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
       "dev": true
     },
-    "@egjs/hammerjs": {
-      "version": "2.0.17",
-      "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
-      "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
-      "peer": true,
-      "requires": {
-        "@types/hammerjs": "^2.0.36"
-      }
-    },
     "@es-joy/jsdoccomment": {
       "version": "0.20.1",
       "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.20.1.tgz",
@@ -15876,6 +15793,11 @@
         }
       }
     },
+    "@flatten-js/interval-tree": {
+      "version": "1.0.18",
+      "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.0.18.tgz",
+      "integrity": "sha512-o72sZErW0Y1C82Cg7nk82ojJ/22EtmKyp5I3eNqgcOKFp/VCzetATYYjJIqOBBaR7FQ/MFj/ZpsmP38mL4TkYA=="
+    },
     "@fortawesome/fontawesome-free": {
       "version": "5.15.4",
       "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
@@ -16787,12 +16709,6 @@
         "@types/node": "*"
       }
     },
-    "@types/hammerjs": {
-      "version": "2.0.41",
-      "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
-      "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==",
-      "peer": true
-    },
     "@types/http-proxy": {
       "version": "1.17.9",
       "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz",
@@ -18395,7 +18311,8 @@
     "component-emitter": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
-      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+      "dev": true
     },
     "compressible": {
       "version": "2.0.18",
@@ -21621,12 +21538,6 @@
         "safe-buffer": "^5.0.1"
       }
     },
-    "keycharm": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
-      "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==",
-      "peer": true
-    },
     "kind-of": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -25627,12 +25538,6 @@
       "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
       "dev": true
     },
-    "timsort": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
-      "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
-      "peer": true
-    },
     "tiny-emitter": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
@@ -26046,7 +25951,8 @@
     "uuid": {
       "version": "8.3.2",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "dev": true
     },
     "v8-compile-cache": {
       "version": "2.3.0",
@@ -26090,26 +25996,6 @@
         }
       }
     },
-    "vis-data": {
-      "version": "7.1.4",
-      "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz",
-      "integrity": "sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==",
-      "peer": true,
-      "requires": {}
-    },
-    "vis-network": {
-      "version": "9.1.2",
-      "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz",
-      "integrity": "sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==",
-      "requires": {}
-    },
-    "vis-util": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
-      "integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
-      "peer": true,
-      "requires": {}
-    },
     "void-elements": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
diff --git a/package.json b/package.json
index 83b674025d0b5d7ecaea543dcabef0eb690deef7..6fa21d4a23961f4cd82002e8e019953eb9b87ccc 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
     "cache": false
   },
   "dependencies": {
+    "@flatten-js/interval-tree": "^1.0.18",
     "@fortawesome/fontawesome-free": "^5.15.4",
     "@sentry/browser": "^6.16.1",
     "@sentry/node": "^6.16.1",
@@ -82,7 +83,6 @@
     "tslib": "^2.3.1",
     "underscore": "^1.13.2",
     "url-join": "^4.0.1",
-    "vis-network": "^9.1.0",
     "whatwg-fetch": "^3.6.2",
     "which": "^2.0.2",
     "winston": "^3.3.3",
diff --git a/static/components.interfaces.ts b/static/components.interfaces.ts
index faa0a9988e74fe948310efe7f725e057956807db..a69dfed1a768a258343195df27a5a03c5dbbfe9d 100644
--- a/static/components.interfaces.ts
+++ b/static/components.interfaces.ts
@@ -23,6 +23,7 @@
 // POSSIBILITY OF SUCH DAMAGE.
 
 import {CompilerOutputOptions} from '../types/features/filters.interfaces';
+import {CfgState} from './panes/cfg-view.interfaces';
 import {LLVMOptPipelineViewState} from './panes/llvm-opt-pipeline.interfaces';
 export const COMPILER_COMPONENT_NAME = 'compiler';
 export const EXECUTOR_COMPONENT_NAME = 'executor';
@@ -175,10 +176,11 @@ export type PopulatedGccDumpViewState = {
 } & (Record<GccDumpOptions, unknown> | EmptyState);
 
 export type EmptyCfgViewState = EmptyState;
-export type PopulatedCfgViewState = StateWithId & {
-    editorid: number;
-    treeid: number;
-};
+export type PopulatedCfgViewState = StateWithId &
+    CfgState & {
+        editorid: number;
+        treeid: number;
+    };
 
 export type EmptyConformanceViewState = EmptyState; // TODO: unusued?
 export type PopulatedConformanceViewState = {
diff --git a/static/components.ts b/static/components.ts
index d73cea64b2cf418f0e8417f96ea669617648a45c..85cabb9da4d99f131e3edc1fce643ff2d78b1eeb 100644
--- a/static/components.ts
+++ b/static/components.ts
@@ -526,6 +526,8 @@ export function getCfgViewWith(id: number, editorid: number, treeid: number): Co
         type: 'component',
         componentName: CFG_VIEW_COMPONENT_NAME,
         componentState: {
+            selectedFunction: null,
+            zoom: 1,
             id,
             editorid,
             treeid,
diff --git a/static/graph-layout-core.ts b/static/graph-layout-core.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a9b44f83c649300ef2f5f391dcdec7874645551d
--- /dev/null
+++ b/static/graph-layout-core.ts
@@ -0,0 +1,951 @@
+// Copyright (c) 2022, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import {AnnotatedCfgDescriptor, AnnotatedNodeDescriptor} from '../types/compilation/cfg.interfaces';
+
+import IntervalTree, {Node} from '@flatten-js/interval-tree';
+
+// Much of the algorithm is inspired from
+// https://cutter.re/docs/api/widgets/classGraphGridLayout.html
+// Thanks to the cutter team for their great documentation!
+
+// TODO(jeremy-rifkin)
+function assert(condition: boolean, message?: string, ...args: any[]): asserts condition {
+    if (!condition) {
+        const stack = new Error('Assertion Error').stack;
+        throw (
+            (message
+                ? `Assertion error in llvm-print-after-all-parser: ${message}`
+                : `Assertion error in llvm-print-after-all-parser`) +
+            (args.length > 0 ? `\n${JSON.stringify(args)}\n` : '') +
+            `\n${stack}`
+        );
+    }
+}
+
+enum SegmentType {
+    Horizontal,
+    Vertical,
+}
+
+type Coordinate = {
+    x: number;
+    y: number;
+};
+
+type GridCoordinate = {
+    row: number;
+    col: number;
+};
+
+type EdgeCoordinate = Coordinate & GridCoordinate;
+
+type EdgeSegment = {
+    start: EdgeCoordinate;
+    end: EdgeCoordinate;
+    horizontalOffset: number;
+    verticalOffset: number;
+    type: SegmentType; // is this point the end of a horizontal or vertical segment
+};
+
+type Edge = {
+    color: string;
+    dest: number;
+    mainColumn: number;
+    path: EdgeSegment[];
+};
+
+type BoundingBox = {
+    rows: number;
+    cols: number;
+};
+
+type Block = {
+    data: AnnotatedNodeDescriptor;
+    edges: Edge[];
+    dagEdges: number[];
+    treeEdges: number[];
+    treeParent: number | null;
+    row: number;
+    col: number;
+    boundingBox: BoundingBox;
+    coordinates: Coordinate;
+    incidentEdgeCount: number;
+};
+
+enum DfsState {
+    NotVisited,
+    Pending,
+    Visited,
+}
+
+type ColumnDescriptor = {
+    width: number;
+    totalOffset: number;
+};
+type RowDescriptor = {
+    height: number;
+    totalOffset: number;
+};
+type EdgeColumnMetadata = {
+    subcolumns: number;
+    intervals: IntervalTree<EdgeSegment>[]; // pointers to segments
+};
+type EdgeRowMetadata = {
+    subrows: number;
+    intervals: IntervalTree<EdgeSegment>[]; // pointers to segments
+};
+
+const EDGE_SPACING = 10;
+
+export class GraphLayoutCore {
+    // We use an adjacency list here
+    blocks: Block[] = [];
+    columnCount: number;
+    rowCount: number;
+    blockColumns: ColumnDescriptor[];
+    blockRows: RowDescriptor[];
+    edgeColumns: (ColumnDescriptor & EdgeColumnMetadata)[];
+    edgeRows: (RowDescriptor & EdgeRowMetadata)[];
+    readonly layoutTime: number;
+
+    constructor(cfg: AnnotatedCfgDescriptor) {
+        // block id -> block
+        const blockMap: Record<string, number> = {};
+        for (const node of cfg.nodes) {
+            const block = {
+                data: node,
+                edges: [],
+                dagEdges: [],
+                treeEdges: [],
+                treeParent: null,
+                row: 0,
+                col: 0,
+                boundingBox: {rows: 0, cols: 0},
+                coordinates: {x: 0, y: 0},
+                incidentEdgeCount: 0,
+            };
+            this.blocks.push(block);
+            blockMap[node.id] = this.blocks.length - 1;
+        }
+        for (const {from, to, color} of cfg.edges) {
+            // TODO: Backend can return dest: "null"
+            // e.g. for the simple program
+            // void baz(int n) {
+            //     if(n % 2 == 0) {
+            //         foo();
+            //     } else {
+            //         bar();
+            //     }
+            // }
+            if (from in blockMap && to in blockMap) {
+                this.blocks[blockMap[from]].edges.push({
+                    color,
+                    dest: blockMap[to],
+                    mainColumn: -1,
+                    path: [],
+                });
+            }
+        }
+        //console.log(this.blocks);
+        const start = performance.now();
+        this.layout();
+        const end = performance.now();
+        this.layoutTime = end - start;
+    }
+
+    dfs(visited: DfsState[], order: number[], node: number) {
+        if (visited[node] === DfsState.Visited) {
+            return;
+        }
+        if (visited[node] === DfsState.NotVisited) {
+            visited[node] = DfsState.Pending;
+            const block = this.blocks[node];
+            for (const edge of block.edges) {
+                this.blocks[edge.dest].incidentEdgeCount++;
+                // If we reach another pending node it's a loop edge.
+                // If we reach an unvisited node it's fine, if we reach a visited node that's also part of the dag
+                if (visited[edge.dest] !== DfsState.Pending) {
+                    block.dagEdges.push(edge.dest);
+                }
+                this.dfs(visited, order, edge.dest);
+            }
+            visited[node] = DfsState.Visited;
+            order.push(node);
+        } else {
+            // visited[node] == DfsState.Pending
+            // If we reach a node in the stack then this is a loop edge; we do nothing
+        }
+    }
+
+    computeDag() {
+        // Returns a topological order of blocks
+        // Breaks loop edges with DFS
+        // Can consider doing non-recursive dfs later if needed
+        const visited = Array(this.blocks.length).fill(DfsState.NotVisited);
+        const order: number[] = [];
+        // TODO: Need an actual function entry point from the backend, or will it always be at index 0?
+        this.dfs(visited, order, 0);
+        for (let i = 0; i < this.blocks.length; i++) {
+            this.dfs(visited, order, i);
+        }
+        // we've computed a post-DFS ordering which is always a reverse topological ordering
+        return order.reverse();
+    }
+
+    assignRows(topologicalOrder) {
+        for (const i of topologicalOrder) {
+            const block = this.blocks[i];
+            //console.log(block);
+            for (const j of block.dagEdges) {
+                const target = this.blocks[j];
+                target.row = Math.max(target.row, block.row + 1);
+            }
+        }
+    }
+
+    computeTree(topologicalOrder) {
+        // DAG is reduced to a tree based on what's vertically adjacent
+        //
+        // For something like
+        //
+        //             +-----+
+        //             |  A  |
+        //             +-----+
+        //            /       \
+        //     +-----+         +-----+
+        //     |  B  |         |  C  |
+        //     +-----+         +-----+
+        //            \       /
+        //             +-----+
+        //             |  D  |
+        //             +-----+
+        //
+        // The tree is chosen to be either of the following depending on what the topological order happens to be
+        // This doesn't matter too much as far as readability goes
+        //
+        //      A            A
+        //     / \          / \
+        //    B   C   or   B   C
+        //    |                |
+        //    D                D
+        for (const i of topologicalOrder) {
+            // Only dag edges are considered
+            // Edges - dag edges = the set of back edges
+            const block = this.blocks[i];
+            for (const j of block.dagEdges) {
+                const target = this.blocks[j];
+                if (target.treeParent === null && target.row === block.row + 1) {
+                    block.treeEdges.push(j);
+                    target.treeParent = i;
+                }
+            }
+        }
+    }
+
+    adjustSubtree(root: number, rowShift: number, columnShift: number) {
+        const block = this.blocks[root];
+        block.row += rowShift;
+        block.col += columnShift;
+        for (const j of block.treeEdges) {
+            this.adjustSubtree(j, rowShift, columnShift);
+        }
+    }
+
+    // Note: Currently O(n^2)
+    computeTreeColumnPositions(node: number) {
+        const block = this.blocks[node];
+        if (block.treeEdges.length === 0) {
+            block.row = 0;
+            block.col = 0;
+            block.boundingBox = {
+                rows: 1,
+                cols: 2,
+            };
+        } else if (block.treeEdges.length === 1) {
+            const childIndex = block.treeEdges[0];
+            const child = this.blocks[childIndex];
+            block.row = 0;
+            block.col = child.col;
+            block.boundingBox = {
+                rows: 1 + child.boundingBox.rows,
+                cols: child.boundingBox.cols,
+            };
+            this.adjustSubtree(childIndex, 1, 0);
+        } else {
+            // If the node has more than two children we'll just center between the
+            //let selectedTreeEdges = block.treeEdges.slice(0, 2);
+            const boundingBox = {
+                rows: 0,
+                cols: 0,
+            };
+            // Compute bounding box of all the subtrees and adjust
+            for (const i of block.treeEdges) {
+                const child = this.blocks[i];
+                this.adjustSubtree(i, 1, boundingBox.cols);
+                boundingBox.rows += child.boundingBox.rows;
+                boundingBox.cols += child.boundingBox.cols;
+            }
+            // Position parent
+            boundingBox.rows++;
+            block.boundingBox = boundingBox;
+            block.row = 0;
+            // between immediate children
+            const [left, right] = [this.blocks[block.treeEdges[0]], this.blocks[block.treeEdges[1]]];
+            block.col = Math.floor((left.col + right.col) / 2); // TODO
+        }
+    }
+
+    assignColumns(topologicalOrder) {
+        // Note: Currently not taking shape into account like Cutter does.
+        // Post DFS order means we compute all children before their parents
+        for (const i of topologicalOrder.slice().reverse()) {
+            this.computeTreeColumnPositions(i);
+        }
+        // We have a forrest, CFGs can have multiple source nodes
+        const trees = Array.from(this.blocks.entries()).filter(([_, block]) => block.treeParent === null);
+        // Place trees next to each other
+        let offset = 0;
+        for (const [i, tree] of trees) {
+            this.adjustSubtree(i, 0, offset);
+            offset += tree.boundingBox.cols;
+        }
+    }
+
+    setupRowsAndColumns() {
+        //console.log(this.blocks);
+        this.rowCount = Math.max(...this.blocks.map(block => block.row)) + 1; // one more row for zero offset
+        this.columnCount = Math.max(...this.blocks.map(block => block.col)) + 2; // blocks are two-wide
+        this.blockRows = Array(this.rowCount)
+            .fill(0)
+            .map(() => ({
+                height: 0,
+                totalOffset: 0,
+            }));
+        this.blockColumns = Array(this.columnCount)
+            .fill(0)
+            .map(() => ({
+                width: 0,
+                totalOffset: 0,
+            }));
+        this.edgeRows = Array(this.rowCount + 1)
+            .fill(0)
+            .map(() => ({
+                height: 2 * EDGE_SPACING,
+                totalOffset: 0,
+                subrows: 0,
+                intervals: [],
+            }));
+        this.edgeColumns = Array(this.columnCount + 1)
+            .fill(0)
+            .map(() => ({
+                width: 2 * EDGE_SPACING,
+                totalOffset: 0,
+                subcolumns: 0,
+                intervals: [],
+            }));
+    }
+
+    computeEdgeMainColumns() {
+        // This is heavily inspired by Cutter
+        // We use a sweep line algorithm processing the CFG top to bottom keeping track of when columns are most
+        // recently blocked. Cutter uses an augmented binary tree to assist with finding empty columns, for now this
+        // just naively iterates.
+        enum EventType {
+            Edge = 0,
+            Block = 1,
+        }
+        type Event = {
+            blockIndex: number;
+            edgeIndex: number;
+            row: number;
+            type: EventType;
+        };
+        const events: Event[] = [];
+        for (const [i, block] of this.blocks.entries()) {
+            events.push({
+                blockIndex: i,
+                edgeIndex: -1,
+                row: block.row,
+                type: EventType.Block,
+            });
+            for (const [j, edge] of block.edges.entries()) {
+                events.push({
+                    blockIndex: i,
+                    edgeIndex: j,
+                    row: Math.max(block.row + 1, this.blocks[edge.dest].row),
+                    type: EventType.Edge,
+                });
+            }
+        }
+        // Sort by row (max(src row, target row) for edges), edge row n is before block row n
+        events.sort((a: Event, b: Event) => {
+            if (a.row === b.row) {
+                return a.type - b.type;
+            } else {
+                return a.row - b.row;
+            }
+        });
+        //
+        const blockedColumns = Array(this.columnCount + 1).fill(-1);
+        for (const event of events) {
+            if (event.type === EventType.Block) {
+                const block = this.blocks[event.blockIndex];
+                blockedColumns[block.col + 1] = block.row;
+            } else {
+                const source = this.blocks[event.blockIndex];
+                const edge = source.edges[event.edgeIndex];
+                const target = this.blocks[edge.dest];
+                const sourceColumn = source.col + 1;
+                const targetColumn = target.col + 1;
+                const topRow = Math.min(source.row + 1, target.row);
+                if (blockedColumns[sourceColumn] < topRow) {
+                    // use column under source block
+                    edge.mainColumn = sourceColumn;
+                } else if (blockedColumns[targetColumn] < topRow) {
+                    // use column of the target
+                    edge.mainColumn = targetColumn;
+                } else {
+                    const leftCandidate =
+                        sourceColumn -
+                        1 -
+                        blockedColumns
+                            .slice(0, sourceColumn)
+                            .reverse()
+                            .findIndex(v => v < topRow);
+                    const rightCandidate = sourceColumn + blockedColumns.slice(sourceColumn).findIndex(v => v < topRow);
+                    // hamming distance
+                    const distanceLeft =
+                        Math.abs(sourceColumn - leftCandidate) + Math.abs(targetColumn - leftCandidate);
+                    const distanceRight =
+                        Math.abs(sourceColumn - rightCandidate) + Math.abs(targetColumn - rightCandidate);
+                    // "figure 8" logic from cutter
+                    // Takes a longer path that produces less crossing
+                    if (target.row < source.row) {
+                        if (
+                            targetColumn < sourceColumn &&
+                            blockedColumns[sourceColumn + 1] < topRow &&
+                            sourceColumn - targetColumn <= distanceLeft + 2
+                        ) {
+                            edge.mainColumn = sourceColumn + 1;
+                            continue;
+                        } else if (
+                            targetColumn > sourceColumn &&
+                            blockedColumns[sourceColumn - 1] < topRow &&
+                            targetColumn - sourceColumn <= distanceRight + 2
+                        ) {
+                            edge.mainColumn = sourceColumn - 1;
+                            continue;
+                        }
+                    }
+                    if (distanceLeft === distanceRight) {
+                        // TODO: Could also try this
+                        /*if(target.row <= source.row) {
+                            if(leftCandidate === sourceColumn - 1) {
+                                edge.mainColumn = leftCandidate;
+                                continue;
+                            } else if(rightCandidate === sourceColumn + 1) {
+                                edge.mainColumn = rightCandidate;
+                                continue;
+                            }
+                        }*/
+                        // Place true branches on the left
+                        // TODO: Need to investigate further block placement stuff here
+                        // TODO: Need to investigate further offset placement stuff for the start segments
+                        if (edge.color === 'green') {
+                            edge.mainColumn = leftCandidate;
+                        } else {
+                            edge.mainColumn = rightCandidate;
+                        }
+                    } else if (distanceLeft < distanceRight) {
+                        edge.mainColumn = leftCandidate;
+                    } else {
+                        edge.mainColumn = rightCandidate;
+                    }
+                }
+            }
+        }
+    }
+
+    // eslint-disable-next-line max-statements
+    addEdgePaths() {
+        // (start: GridCoordinate, end: GridCoordinate) => ({
+        const makeSegment = (start: [number, number], end: [number, number]): EdgeSegment => ({
+            start: {
+                //...start,
+                row: start[0],
+                col: start[1],
+                x: 0,
+                y: 0,
+            },
+            end: {
+                //...end,
+                row: end[0],
+                col: end[1],
+                x: 0,
+                y: 0,
+            },
+            horizontalOffset: 0,
+            verticalOffset: 0,
+            type: start[1] === end[1] ? SegmentType.Vertical : SegmentType.Horizontal,
+        });
+        for (const block of this.blocks) {
+            for (const edge of block.edges) {
+                const target = this.blocks[edge.dest];
+                // start just below the source block
+                edge.path.push(makeSegment([block.row + 1, block.col + 1], [block.row + 1, block.col + 1]));
+                // horizontal segment over to main column
+                edge.path.push(makeSegment([block.row + 1, block.col + 1], [block.row + 1, edge.mainColumn]));
+                // vertical segment down the main column
+                edge.path.push(makeSegment([block.row + 1, edge.mainColumn], [target.row, edge.mainColumn]));
+                // horizontal segment over to the target column
+                edge.path.push(makeSegment([target.row, edge.mainColumn], [target.row, target.col + 1]));
+                // finish at the target block
+                edge.path.push(makeSegment([target.row, target.col + 1], [target.row, target.col + 1]));
+                // Simplify segments
+                // Simplifications performed are eliminating (non-sentinel) edges which don't move anywhere and folding
+                // VV -> V and HH -> H.
+                let movement;
+                do {
+                    movement = false;
+                    // i needs to start one into the range since we compare with i - 1
+                    for (let i = 1; i < edge.path.length; i++) {
+                        const prevSegment = edge.path[i - 1];
+                        const segment = edge.path[i];
+                        // sanity checks
+                        for (let j = 0; j < edge.path.length; j++) {
+                            const segment = edge.path[j];
+                            if (
+                                (segment.type === SegmentType.Vertical && segment.start.col !== segment.end.col) ||
+                                (segment.type === SegmentType.Horizontal && segment.start.row !== segment.end.row)
+                            ) {
+                                throw Error("Segment type doesn't match coordinates");
+                            }
+                            if (j > 0) {
+                                const prev = edge.path[j - 1];
+                                if (prev.end.row !== segment.start.row || prev.end.col !== segment.start.col) {
+                                    throw Error("Adjacent segment start/endpoints don't match");
+                                }
+                            }
+                            if (j < edge.path.length - 1) {
+                                const next = edge.path[j + 1];
+                                if (segment.end.row !== next.start.row || segment.end.col !== next.start.col) {
+                                    throw Error("Adjacent segment start/endpoints don't match");
+                                }
+                            }
+                        }
+                        // If a segment doesn't go anywhere and is not a sentinel it can be eliminated
+                        if (
+                            segment.start.col === segment.end.col &&
+                            segment.start.row === segment.end.row &&
+                            i !== edge.path.length - 1
+                        ) {
+                            edge.path.splice(i, 1);
+                            movement = true;
+                            continue;
+                        }
+                        // VV -> V
+                        // HH -> H
+                        if (prevSegment.type === segment.type) {
+                            if (
+                                (prevSegment.type === SegmentType.Vertical &&
+                                    prevSegment.start.col !== segment.start.col) ||
+                                (prevSegment.type === SegmentType.Horizontal &&
+                                    prevSegment.start.row !== segment.start.row)
+                            ) {
+                                throw Error(
+                                    "Adjacent horizontal or vertical segments don't share a common row or column"
+                                );
+                            }
+                            prevSegment.end = segment.end;
+                            edge.path.splice(i, 1);
+                            movement = true;
+                            continue;
+                        }
+                    }
+                } while (movement);
+                // sanity checks
+                for (let j = 0; j < edge.path.length; j++) {
+                    const segment = edge.path[j];
+                    if (
+                        (segment.type === SegmentType.Vertical && segment.start.col !== segment.end.col) ||
+                        (segment.type === SegmentType.Horizontal && segment.start.row !== segment.end.row)
+                    ) {
+                        throw Error("Segment type doesn't match coordinates (post-simplification)");
+                    }
+                    if (j > 0) {
+                        const prev = edge.path[j - 1];
+                        if (prev.end.row !== segment.start.row || prev.end.col !== segment.start.col) {
+                            throw Error("Adjacent segment start/endpoints don't match (post-simplification)");
+                        }
+                    }
+                    if (j < edge.path.length - 1) {
+                        const next = edge.path[j + 1];
+                        if (segment.end.row !== next.start.row || segment.end.col !== next.start.col) {
+                            throw Error("Adjacent segment start/endpoints don't match (post-simplification)");
+                        }
+                    }
+                }
+                // Compute subrows/subcolumns
+                for (const segment of edge.path) {
+                    if (segment.type === SegmentType.Vertical) {
+                        if (segment.start.col !== segment.end.col) {
+                            throw Error('Vertical segment changes column');
+                        }
+                        const col = this.edgeColumns[segment.start.col];
+                        let inserted = false;
+                        for (const tree of col.intervals) {
+                            if (!tree.intersect_any([segment.start.row, segment.end.row])) {
+                                tree.insert([segment.start.row, segment.end.row], segment);
+                                inserted = true;
+                                break;
+                            }
+                        }
+                        if (!inserted) {
+                            const tree = new IntervalTree<EdgeSegment>();
+                            col.intervals.push(tree);
+                            col.subcolumns++;
+                            tree.insert([segment.start.row, segment.end.row], segment);
+                        }
+                    } else {
+                        // horizontal
+                        if (segment.start.row !== segment.end.row) {
+                            throw Error('Horizontal segment changes row');
+                        }
+                        const row = this.edgeRows[segment.start.row];
+                        let inserted = false;
+                        for (const tree of row.intervals) {
+                            if (!tree.intersect_any([segment.start.col, segment.end.col])) {
+                                tree.insert([segment.start.col, segment.end.col], segment);
+                                inserted = true;
+                                break;
+                            }
+                        }
+                        if (!inserted) {
+                            const tree = new IntervalTree<EdgeSegment>();
+                            row.intervals.push(tree);
+                            row.subrows++;
+                            tree.insert([segment.start.col, segment.end.col], segment);
+                        }
+                    }
+                }
+            }
+        }
+        // Throw everything away and do it all again, but smarter
+        for (const edgeColumn of this.edgeColumns) {
+            for (const intervalTree of edgeColumn.intervals) {
+                intervalTree.root = null as unknown as Node<EdgeSegment>;
+            }
+        }
+        for (const edgeRow of this.edgeRows) {
+            for (const intervalTree of edgeRow.intervals) {
+                intervalTree.root = null as unknown as Node<EdgeSegment>;
+            }
+        }
+        // Edge kind is the primary heuristic for subrow/column assignment
+        // For horizontal edges, think of left/vertical/right terminology rotated 90 degrees right
+        enum EdgeKind {
+            LEFTU = -2,
+            LEFTCORNER = -1,
+            VERTICAL = 0,
+            RIGHTCORNER = 1,
+            RIGHTU = 2,
+            NULL = NaN,
+        }
+        const segments: {
+            segment: EdgeSegment;
+            length: number;
+            kind: EdgeKind;
+            tiebreaker: number;
+        }[] = [];
+        for (const block of this.blocks) {
+            for (const edge of block.edges) {
+                const edgeLength = edge.path
+                    .map(({start, end}) => Math.abs(start.col - end.col) + Math.abs(start.row - end.row))
+                    .reduce((A, x) => A + x);
+                const target = this.blocks[edge.dest];
+                for (const [i, segment] of edge.path.entries()) {
+                    let kind = EdgeKind.NULL;
+                    if (i === 0) {
+                        // segment will be vertical
+                        if (edge.path.length === 1) {
+                            kind = EdgeKind.VERTICAL;
+                        } else {
+                            const next = edge.path[i + 1];
+                            if (next.end.col > segment.end.col) {
+                                kind = EdgeKind.RIGHTCORNER;
+                            } else {
+                                kind = EdgeKind.LEFTCORNER;
+                            }
+                        }
+                    } else if (i === edge.path.length - 1) {
+                        // segment will be vertical
+                        // there will be a previous segment, i !== 0
+                        const previous = edge.path[i - 1];
+                        if (previous.start.col > segment.end.col) {
+                            kind = EdgeKind.RIGHTCORNER;
+                        } else {
+                            kind = EdgeKind.LEFTCORNER;
+                        }
+                    } else {
+                        // there will be both a previous and a next
+                        const next = edge.path[i + 1];
+                        const previous = edge.path[i - 1];
+                        if (segment.type === SegmentType.Vertical) {
+                            if (previous.start.col < segment.start.col && next.end.col < segment.start.col) {
+                                kind = EdgeKind.LEFTU;
+                            } else if (previous.start.col > segment.start.col && next.end.col > segment.start.col) {
+                                kind = EdgeKind.RIGHTU;
+                            } else if (previous.start.col > segment.end.col) {
+                                kind = EdgeKind.RIGHTCORNER;
+                            } else {
+                                kind = EdgeKind.LEFTCORNER;
+                            }
+                        } else {
+                            // horizontal
+                            // Same logic, think rotated 90 degrees right
+                            if (previous.start.row <= segment.start.row && next.end.row < segment.start.row) {
+                                kind = EdgeKind.LEFTU;
+                            } else if (previous.start.row > segment.start.row && next.end.row > segment.start.row) {
+                                kind = EdgeKind.RIGHTU;
+                            } else if (previous.start.row > segment.end.row) {
+                                kind = EdgeKind.RIGHTCORNER;
+                            } else {
+                                kind = EdgeKind.LEFTCORNER;
+                            }
+                        }
+                    }
+                    assert((kind as any) !== EdgeKind.NULL);
+                    segments.push({
+                        segment,
+                        kind,
+                        length:
+                            Math.abs(segment.start.col - segment.end.col) +
+                            Math.abs(segment.start.row - segment.end.row),
+                        tiebreaker: 2 * edgeLength + (target.row >= block.row ? 1 : 0),
+                    });
+                }
+            }
+        }
+        segments.sort((a, b) => {
+            if (a.kind !== b.kind) {
+                return a.kind - b.kind;
+            } else {
+                const kind = a.kind; // a.kind == b.kind
+                if (a.length !== b.length) {
+                    if (kind <= 0) {
+                        // shortest first if coming from the left
+                        return a.length - b.length;
+                    } else {
+                        // coming from the right, shortest last
+                        // reverse edge length order
+                        return b.length - a.length;
+                    }
+                } else {
+                    if (kind <= 0) {
+                        return a.tiebreaker - b.tiebreaker;
+                    } else {
+                        // coming from the right, reverse
+                        return b.tiebreaker - a.tiebreaker;
+                    }
+                }
+            }
+        });
+        ///console.log(segments);
+        for (const segmentEntry of segments) {
+            const {segment} = segmentEntry;
+            if (segment.type === SegmentType.Vertical) {
+                const col = this.edgeColumns[segment.start.col];
+                let inserted = false;
+                for (const tree of col.intervals) {
+                    if (!tree.intersect_any([segment.start.row, segment.end.row])) {
+                        tree.insert([segment.start.row, segment.end.row], segment);
+                        inserted = true;
+                        break;
+                    }
+                }
+                if (!inserted) {
+                    throw Error("Vertical segment couldn't be inserted");
+                }
+            } else {
+                // Horizontal
+                const row = this.edgeRows[segment.start.row];
+                let inserted = false;
+                for (const tree of row.intervals) {
+                    if (!tree.intersect_any([segment.start.col, segment.end.col])) {
+                        tree.insert([segment.start.col, segment.end.col], segment);
+                        inserted = true;
+                        break;
+                    }
+                }
+                if (!inserted) {
+                    throw Error("Horizontal segment couldn't be inserted");
+                }
+            }
+        }
+        // Assign offsets
+        for (const edgeColumn of this.edgeColumns) {
+            edgeColumn.width = Math.max(EDGE_SPACING + edgeColumn.intervals.length * EDGE_SPACING, 2 * EDGE_SPACING);
+            for (const [i, intervalTree] of edgeColumn.intervals.entries()) {
+                for (const segment of intervalTree.values) {
+                    segment.horizontalOffset = EDGE_SPACING * (i + 1);
+                }
+            }
+        }
+        for (const edgeRow of this.edgeRows) {
+            edgeRow.height = Math.max(EDGE_SPACING + edgeRow.intervals.length * EDGE_SPACING, 2 * EDGE_SPACING);
+            for (const [i, intervalTree] of edgeRow.intervals.entries()) {
+                for (const segment of intervalTree.values) {
+                    segment.verticalOffset = EDGE_SPACING * (i + 1);
+                }
+            }
+        }
+    }
+
+    // eslint-disable-next-line max-statements
+    computeCoordinates() {
+        // Compute block row widths and heights
+        for (const block of this.blocks) {
+            // Update block width if it has a ton of incoming edges
+            block.data.width = Math.max(block.data.width, (block.incidentEdgeCount - 1) * EDGE_SPACING);
+            //console.log(this.blockRows[block.row].height, block.data.height, block.row);
+            //console.log(this.blockRows);
+            const halfWidth = (block.data.width - this.edgeColumns[block.col + 1].width) / 2;
+            //console.log("--->", block.col, this.columnCount);
+            this.blockRows[block.row].height = Math.max(this.blockRows[block.row].height, block.data.height);
+            this.blockColumns[block.col].width = Math.max(this.blockColumns[block.col].width, halfWidth);
+            this.blockColumns[block.col + 1].width = Math.max(this.blockColumns[block.col + 1].width, halfWidth);
+        }
+        // Compute row total offsets
+        for (let i = 0; i < this.rowCount; i++) {
+            // edge row 0 is already at the correct offset, this iteration will set the offset for block row 0 and edge
+            // row 1.
+            this.blockRows[i].totalOffset = this.edgeRows[i].totalOffset + this.edgeRows[i].height;
+            this.edgeRows[i + 1].totalOffset = this.blockRows[i].totalOffset + this.blockRows[i].height;
+        }
+        // Compute column total offsets
+        for (let i = 0; i < this.columnCount; i++) {
+            // same deal here
+            this.blockColumns[i].totalOffset = this.edgeColumns[i].totalOffset + this.edgeColumns[i].width;
+            this.edgeColumns[i + 1].totalOffset = this.blockColumns[i].totalOffset + this.blockColumns[i].width;
+        }
+        // Compute block coordinates and edge paths
+        for (const block of this.blocks) {
+            block.coordinates.x =
+                this.edgeColumns[block.col + 1].totalOffset -
+                (block.data.width - this.edgeColumns[block.col + 1].width) / 2;
+            block.coordinates.y = this.blockRows[block.row].totalOffset;
+            for (const edge of block.edges) {
+                if (edge.path.length === 1) {
+                    // Special case: Direct dropdown
+                    const segment = edge.path[0];
+                    const target = this.blocks[edge.dest];
+                    segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
+                    segment.start.y = block.coordinates.y + block.data.height;
+                    segment.end.x = this.edgeColumns[segment.end.col].totalOffset + segment.horizontalOffset;
+                    segment.end.y = this.edgeRows[target.row].totalOffset + this.edgeRows[target.row].height;
+                } else {
+                    // push initial point
+                    {
+                        const segment = edge.path[0];
+                        segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
+                        segment.start.y = block.coordinates.y + block.data.height;
+                        segment.end.x = this.edgeColumns[segment.end.col].totalOffset + segment.horizontalOffset;
+                        segment.end.y = 0; // this is something we need from the next segment
+                    }
+                    // first and last handled specially
+                    for (const segment of edge.path.slice(1, edge.path.length - 1)) {
+                        segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
+                        segment.start.y = this.edgeRows[segment.start.row].totalOffset + segment.verticalOffset;
+                        segment.end.x = this.edgeColumns[segment.end.col].totalOffset + segment.horizontalOffset;
+                        segment.end.y = this.edgeRows[segment.end.row].totalOffset + segment.verticalOffset;
+                    }
+                    // push final point
+                    {
+                        const target = this.blocks[edge.dest];
+                        const segment = edge.path[edge.path.length - 1];
+                        segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
+                        segment.start.y = 0; // something we need from the previous segment
+                        segment.end.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
+                        segment.end.y = this.edgeRows[target.row].totalOffset + this.edgeRows[target.row].height;
+                    }
+                    // apply offsets to neighbor segments
+                    for (let i = 0; i < edge.path.length; i++) {
+                        const segment = edge.path[i];
+                        if (segment.type === SegmentType.Vertical) {
+                            if (i > 0) {
+                                const prev = edge.path[i - 1];
+                                prev.end.x = segment.start.x;
+                            }
+                            if (i < edge.path.length - 1) {
+                                const next = edge.path[i + 1];
+                                next.start.x = segment.end.x;
+                            }
+                        } else {
+                            // Horizontal
+                            if (i > 0) {
+                                const prev = edge.path[i - 1];
+                                prev.end.y = segment.start.y;
+                            }
+                            if (i < edge.path.length - 1) {
+                                const next = edge.path[i + 1];
+                                next.start.y = segment.end.y;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    layout() {
+        const topologicalOrder = this.computeDag();
+        //console.log(topologicalOrder);
+        this.assignRows(topologicalOrder);
+        //console.log(this.blocks);
+        this.computeTree(topologicalOrder);
+        //console.log(this.blocks);
+        this.assignColumns(topologicalOrder);
+        //console.log(this.blocks);
+        this.setupRowsAndColumns();
+        // Edge routing
+        this.computeEdgeMainColumns();
+        this.addEdgePaths();
+        // -- Nothing is pixel aware above this line ---
+        // Add pixel coordinates
+        this.computeCoordinates();
+        //
+        ///console.log(this);
+    }
+
+    getWidth() {
+        const lastCol = this.edgeColumns[this.edgeColumns.length - 1];
+        return lastCol.totalOffset + lastCol.width;
+    }
+
+    getHeight() {
+        const lastRow = this.edgeRows[this.edgeRows.length - 1];
+        return lastRow.totalOffset + lastRow.height;
+    }
+}
diff --git a/static/noscript.scss b/static/noscript.scss
index 81aa4ffab5f26dc350a3fdcaef374c4e20d0838c..b035d8b505f73722dfe2f4e63b4afb098f9dd0d7 100644
--- a/static/noscript.scss
+++ b/static/noscript.scss
@@ -1,5 +1,4 @@
 @import '~@fortawesome/fontawesome-free/css/all.min.css';
-@import '~vis-network/styles/vis-network.css';
 
 html body {
     overflow: auto;
diff --git a/static/panes/cfg-view.interfaces.ts b/static/panes/cfg-view.interfaces.ts
index 5683354f2f0e22b8e17f3c4a356186e5a50d29e3..f9a8c9f9d9037fcd27883c2b37633acbe5ee4204 100644
--- a/static/panes/cfg-view.interfaces.ts
+++ b/static/panes/cfg-view.interfaces.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Compiler Explorer Authors
+// Copyright (c) 2022, Compiler Explorer Authors
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without
@@ -22,8 +22,14 @@
 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 // POSSIBILITY OF SUCH DAMAGE.
 
-import {PaneState} from './pane.interfaces';
-import * as vis from 'vis-network';
+export interface CfgState {
+    selectedFunction: string | null;
+    zoom: number;
+}
+
+/*
+
+Previous state objects looked like:
 
 export interface CfgOptions {
     physics?: boolean;
@@ -36,3 +42,5 @@ export interface CfgState extends PaneState {
     scale: number;
     options?: CfgOptions;
 }
+
+*/
diff --git a/static/panes/cfg-view.ts b/static/panes/cfg-view.ts
index 406fb2eab9d38ca46c5573219c9b5a28905615d8..86c016ee9c2ce46fbddaf75917d254e71ba72808 100644
--- a/static/panes/cfg-view.ts
+++ b/static/panes/cfg-view.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2017, Najjar Chedy
+// Copyright (c) 2022, Compiler Explorer Authors
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without
@@ -22,427 +22,332 @@
 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 // POSSIBILITY OF SUCH DAMAGE.
 
+import {Pane} from './pane';
+import * as monaco from 'monaco-editor';
 import $ from 'jquery';
-import * as vis from 'vis-network';
 import _ from 'underscore';
+
+import {CfgState} from './cfg-view.interfaces';
+import {Hub} from '../hub';
+import {Container} from 'golden-layout';
+import {PaneState} from './pane.interfaces';
 import {ga} from '../analytics';
-import {Toggles} from '../widgets/toggles';
+import * as utils from '../utils';
+
+import {
+    AnnotatedCfgDescriptor,
+    AnnotatedNodeDescriptor,
+    CfgDescriptor,
+    CFGResult,
+} from '../../types/compilation/cfg.interfaces';
+import {GraphLayoutCore} from '../graph-layout-core';
+import * as MonacoConfig from '../monaco-config';
 import TomSelect from 'tom-select';
-import {Container} from 'golden-layout';
-import {CfgOptions, CfgState} from './cfg-view.interfaces';
-import {Hub} from '../hub';
-import {Pane} from './pane';
-
-interface NodeInfo {
-    edges: string[];
-    dagEdges: string[];
-    index: string;
-    id: number;
-    level: number;
-    state: number;
-    inCount: number;
-}
-
-export class Cfg extends Pane<CfgState> {
-    defaultCfgOutput: object;
-    llvmCfgPlaceholder: object;
-    binaryModeSupport: object;
-    savedPos: any;
-    savedScale: any;
-    needsMove: boolean;
-    currentFunc: string;
-    functions: Record<string, vis.Data>;
-    networkOpts: vis.Options;
-    cfgVisualiser: vis.Network;
-    _binaryFilter: boolean;
-    functionPicker: TomSelect;
-    toggles: Toggles;
-    toggleNavigationButton: JQuery;
-    toggleNavigationTitle: string;
-    togglePhysicsButton: JQuery;
-    togglePhysicsTitle: string;
-    options: Required<CfgOptions>;
-
-    constructor(hub: Hub, container: Container, state: CfgState) {
-        super(hub, container, state);
 
-        this.llvmCfgPlaceholder = {
-            nodes: [
-                {
-                    id: 0,
-                    shape: 'box',
-                    label: '-emit-llvm currently not supported',
-                },
-            ],
-            edges: [],
-        };
-        this.binaryModeSupport = {
-            nodes: [
-                {
-                    id: 0,
-                    shape: 'box',
-                    label: 'Cfg mode cannot be used when the binary filter is set',
-                },
-            ],
-            edges: [],
-        };
+const ColorTable = {
+    red: '#FE5D5D',
+    green: '#76E381',
+    blue: '#65B7F6',
+    grey: '#c5c5c5',
+};
 
-        this.savedPos = state.pos;
-        this.savedScale = state.scale;
-        this.needsMove = this.savedPos && this.savedScale;
+type Coordinate = {
+    x: number;
+    y: number;
+};
 
-        this.currentFunc = state.selectedFn || '';
-        this.functions = {};
+const DZOOM = 0.1;
+const MINZOOM = 0.1;
 
-        this._binaryFilter = false;
-
-        const pickerEl = this.domRoot.find('.function-picker')[0] as HTMLInputElement;
-        this.functionPicker = new TomSelect(pickerEl, {
-            sortField: 'name',
-            valueField: 'name',
-            labelField: 'name',
-            searchField: ['name'],
+export class Cfg extends Pane<CfgState> {
+    graphDiv: HTMLElement;
+    svg: SVGElement;
+    blockContainer: HTMLElement;
+    graphContainer: HTMLElement;
+    graphElement: HTMLElement;
+    infoElement: HTMLElement;
+    currentPosition: Coordinate = {x: 0, y: 0};
+    dragging = false;
+    dragStart: Coordinate = {x: 0, y: 0};
+    dragStartPosition: Coordinate = {x: 0, y: 0};
+    graphDimensions = {width: 0, height: 0};
+    functionSelector: TomSelect;
+    results: CFGResult;
+    state: CfgState & PaneState;
+    layout: GraphLayoutCore;
+    bbMap: Record<string, HTMLDivElement> = {};
+
+    constructor(hub: Hub, container: Container, state: CfgState & PaneState) {
+        if ((state as any).selectedFn) {
+            state = {
+                id: state.id,
+                compilerName: state.compilerName,
+                editorid: state.editorid,
+                treeid: state.treeid,
+                selectedFunction: (state as any).selectedFn,
+                zoom: 1,
+            };
+        }
+        super(hub, container, state);
+        this.eventHub.emit('cfgViewOpened', this.compilerInfo.compilerId);
+        this.eventHub.emit('requestFilters', this.compilerInfo.compilerId);
+        this.eventHub.emit('requestCompiler', this.compilerInfo.compilerId);
+        const selector = this.domRoot.get()[0].getElementsByClassName('function-selector')[0];
+        if (!(selector instanceof HTMLSelectElement)) {
+            throw new Error('.function-selector is not an HTMLSelectElement');
+        }
+        this.functionSelector = new TomSelect(selector, {
+            valueField: 'value',
+            labelField: 'title',
+            searchField: ['title'],
+            placeholder: '🔍 Select a function...',
             dropdownParent: 'body',
-            plugins: ['input_autogrow'],
-            onChange: (e: any) => {
-                // TomSelect says it's an Event, but we receive strings
-                const val = e as string;
-                if (val in this.functions) {
-                    const selectedFn = this.functions[val];
-                    this.currentFunc = val;
-                    this.showCfgResults({
-                        nodes: selectedFn.nodes,
-                        edges: selectedFn.edges,
-                    });
-                    if (selectedFn.nodes && selectedFn.nodes.length > 0) {
-                        this.cfgVisualiser.selectNodes([selectedFn.nodes[0].id]);
-                    }
-                    this.resize();
-                    this.updateState();
-                }
+            plugins: ['dropdown_input'],
+            sortField: 'title',
+            onChange: e => {
+                this.selectFunction(e as any as string);
             },
         });
-
-        this.updateButtons();
+        this.state = state;
     }
 
-    override getInitialHTML(): string {
+    override getInitialHTML() {
         return $('#cfg').html();
     }
 
+    override getDefaultPaneName() {
+        return 'CFG';
+    }
+
     override registerOpeningAnalyticsEvent(): void {
         ga.proxy('send', {
             hitType: 'event',
             eventCategory: 'OpenViewPane',
-            eventAction: 'Cfg',
+            eventAction: 'CFGViewPane',
         });
     }
 
-    override onCompileResult(compilerId: number, compiler: any, result: any) {
-        if (this.compilerInfo.compilerId === compilerId) {
-            let functionNames: string[] = [];
-            if (compiler.supportsCfg && !$.isEmptyObject(result.cfg)) {
-                this.functions = result.cfg;
-                functionNames = Object.keys(this.functions);
-                if (functionNames.indexOf(this.currentFunc) === -1) {
-                    this.currentFunc = functionNames[0];
-                }
-                const selectedFn = this.functions[this.currentFunc];
-                this.showCfgResults({
-                    nodes: selectedFn.nodes,
-                    edges: selectedFn.edges,
-                });
-                if (selectedFn.nodes && selectedFn.nodes.length > 0) {
-                    this.cfgVisualiser.selectNodes([selectedFn.nodes[0].id]);
-                }
-            } else {
-                // We don't reset the current function here as we would lose the saved one if this happened at the beginning
-                // (Hint: It *does* happen)
-                if (!result.compilationOptions?.includes('-emit-llvm')) {
-                    this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.defaultCfgOutput);
-                } else {
-                    this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.llvmCfgPlaceholder);
-                }
-            }
-
-            this.functionPicker.clearOptions();
-            this.functionPicker.addOption(
-                functionNames.length
-                    ? this.adaptStructure(functionNames)
-                    : {name: 'The input does not contain functions'}
-            );
-            this.functionPicker.refreshOptions(false);
-
-            this.functionPicker.clear();
-            this.functionPicker.addItem(
-                functionNames.length ? this.currentFunc : 'The input does not contain any function',
-                true
-            );
-            this.updateState();
-        }
-    }
-
     override registerDynamicElements(state: CfgState) {
-        this.defaultCfgOutput = {nodes: [{id: 0, shape: 'box', label: 'No Output'}], edges: []};
-        // Note that this might be outdated if no functions were present when creating the link, but that's handled
-        // by selectize
-        this.options = {
-            navigation: state.options?.navigation ?? false,
-            physics: state.options?.physics ?? false,
-        };
-
-        this.networkOpts = {
-            autoResize: true,
-            locale: 'en',
-            edges: {
-                arrows: {to: {enabled: true}},
-                smooth: {
-                    enabled: true,
-                    type: 'dynamic',
-                    roundness: 1,
-                },
-                physics: true,
-            },
-            nodes: {
-                font: {face: 'Consolas, "Liberation Mono", Courier, monospace', align: 'left'},
-            },
-            layout: {
-                hierarchical: {
-                    enabled: true,
-                    direction: 'UD',
-                    nodeSpacing: 100,
-                    levelSeparation: 150,
-                },
-            },
-            physics: {
-                enabled: this.options.physics,
-                hierarchicalRepulsion: {
-                    nodeDistance: 160,
-                },
-            },
-            interaction: {
-                navigationButtons: this.options.navigation,
-                keyboard: {
-                    enabled: true,
-                    speed: {x: 10, y: 10, zoom: 0.03},
-                    bindToWindow: false,
-                },
-            },
-        };
-
-        this.cfgVisualiser = new vis.Network(
-            this.domRoot.find('.graph-placeholder')[0],
-            this.defaultCfgOutput,
-            this.networkOpts
-        );
-    }
-
-    override onCompiler(compilerId: number, compiler: any) {
-        if (compilerId === this.compilerInfo.compilerId) {
-            this.compilerInfo.compilerName = compiler ? compiler.name : '';
-            this.updateTitle();
-        }
-    }
-
-    onFiltersChange(compilerId: number, filters: any) {
-        if (this.compilerInfo.compilerId === compilerId) {
-            this._binaryFilter = filters.binary;
-        }
-    }
-
-    override registerButtons(state: CfgState) {
-        this.toggles = new Toggles(this.domRoot.find('.options'), this.options);
-
-        this.toggleNavigationButton = this.domRoot.find('.toggle-navigation');
-        this.toggleNavigationTitle = this.toggleNavigationButton.prop('title') as string;
-
-        this.togglePhysicsButton = this.domRoot.find('.toggle-physics');
-        this.togglePhysicsTitle = this.togglePhysicsButton.prop('title');
-
-        this.topBar = this.domRoot.find('.top-bar');
+        this.graphDiv = this.domRoot.find('.graph')[0];
+        this.svg = this.domRoot.find('svg')[0] as SVGElement;
+        this.blockContainer = this.domRoot.find('.block-container')[0];
+        this.graphContainer = this.domRoot.find('.graph-container')[0];
+        this.graphElement = this.domRoot.find('.graph')[0];
+        this.infoElement = this.domRoot.find('.cfg-info')[0];
     }
 
     override registerCallbacks() {
-        this.cfgVisualiser.on('dragEnd', this.updateState.bind(this));
-        this.cfgVisualiser.on('zoom', this.updateState.bind(this));
-
-        this.eventHub.on('filtersChange', this.onFiltersChange, this);
-
-        this.eventHub.emit('cfgViewOpened', this.compilerInfo.compilerId);
-        this.eventHub.emit('requestFilters', this.compilerInfo.compilerId);
-        this.eventHub.emit('requestCompiler', this.compilerInfo.compilerId);
-
-        this.togglePhysicsButton.on('click', () => {
-            this.networkOpts.physics.enabled = this.togglePhysicsButton.hasClass('active');
-            // change only physics.enabled option to preserve current node locations
-            this.cfgVisualiser.setOptions({
-                physics: {enabled: this.networkOpts.physics.enabled},
-            });
+        this.graphContainer.addEventListener('mousedown', e => {
+            const div = (e.target as Element).closest('div');
+            if (div && (div.classList.contains('block-container') || div.classList.contains('graph-container'))) {
+                this.dragging = true;
+                this.dragStart = {x: e.clientX, y: e.clientY};
+                this.dragStartPosition = {...this.currentPosition};
+            } else {
+                // pass, let the user select block contents and other text
+            }
         });
-
-        this.toggleNavigationButton.on('click', () => {
-            this.networkOpts.interaction.navigationButtons = this.toggleNavigationButton.hasClass('active');
-            this.cfgVisualiser.setOptions({
-                interaction: {
-                    navigationButtons: this.networkOpts.interaction.navigationButtons,
-                },
-            });
+        this.graphContainer.addEventListener('mouseup', e => {
+            this.dragging = false;
         });
-        this.toggles.on('change', () => {
-            this.updateButtons();
-            this.updateState();
+        this.graphContainer.addEventListener('mousemove', e => {
+            if (this.dragging) {
+                this.currentPosition = {
+                    x: e.clientX - this.dragStart.x + this.dragStartPosition.x,
+                    y: e.clientY - this.dragStart.y + this.dragStartPosition.y,
+                };
+                this.graphElement.style.left = this.currentPosition.x + 'px';
+                this.graphElement.style.top = this.currentPosition.y + 'px';
+            }
+        });
+        this.graphContainer.addEventListener('wheel', e => {
+            const delta = DZOOM * -Math.sign(e.deltaY) * Math.max(1, this.state.zoom - 1);
+            const prevZoom = this.state.zoom;
+            this.state.zoom += delta;
+            if (this.state.zoom >= MINZOOM) {
+                this.graphElement.style.transform = `scale(${this.state.zoom})`;
+                const mouseX = e.clientX - this.graphElement.getBoundingClientRect().x;
+                const mouseY = e.clientY - this.graphElement.getBoundingClientRect().y;
+                // Amount that the zoom will offset is mouseX / width before zoom * delta * unzoomed width
+                // And same for y. The width / height terms cancel.
+                this.currentPosition.x -= (mouseX / prevZoom) * delta;
+                this.currentPosition.y -= (mouseY / prevZoom) * delta;
+                this.graphElement.style.left = this.currentPosition.x + 'px';
+                this.graphElement.style.top = this.currentPosition.y + 'px';
+            } else {
+                this.state.zoom = MINZOOM;
+            }
         });
     }
 
-    updateButtons() {
-        const formatButtonTitle = (button: JQuery, title: string) => {
-            button.prop('title', '[' + (button.hasClass('active') ? 'ON' : 'OFF') + '] ' + title);
-        };
-        formatButtonTitle(this.togglePhysicsButton, this.togglePhysicsTitle);
-        formatButtonTitle(this.toggleNavigationButton, this.toggleNavigationTitle);
-    }
-
-    override resize() {
-        const height = (this.domRoot.height() as number) - (this.topBar.outerHeight(true) ?? 0);
-        if ((this.cfgVisualiser as any).canvas !== undefined) {
-            this.cfgVisualiser.setSize('100%', height.toString());
-            this.cfgVisualiser.redraw();
+    override onCompiler(compilerId: number, compiler: any, options: unknown, editorId: number, treeId: number): void {
+        if (this.compilerInfo.compilerId !== compilerId) return;
+        this.compilerInfo.compilerName = compiler ? compiler.name : '';
+        this.compilerInfo.editorId = editorId;
+        this.compilerInfo.treeId = treeId;
+        this.updateTitle();
+        if (compiler && !compiler.supportsLLVMOptPipelineView) {
+            //this.editor.setValue('<LLVM IR output is not supported for this compiler>');
         }
     }
 
-    override getDefaultPaneName() {
-        return 'Graph Viewer';
-    }
-
-    assignLevels(data: vis.Data) {
-        const nodes: NodeInfo[] = [];
-        const idToIdx: string[] = [];
-        for (const i in data.nodes) {
-            const node = data.nodes[i];
-            idToIdx[node.id] = i;
-            nodes.push({
-                edges: [],
-                dagEdges: [],
-                index: i,
-                id: node.id,
-                level: 0,
-                state: 0,
-                inCount: 0,
-            });
-        }
-        const isEdgeValid = (edge: vis.Edge) => edge.from && edge.to && edge.from in idToIdx && edge.to in idToIdx;
-        for (const edge of data.edges as vis.Edge[]) {
-            if (edge.from && edge.to && isEdgeValid(edge)) {
-                nodes[idToIdx[edge.from]].edges.push(idToIdx[edge.to]);
-            }
-        }
-
-        const dfs = (node: NodeInfo) => {
-            // choose which edges will be back-edges
-            node.state = 1;
-
-            node.edges.forEach(targetIndex => {
-                const target = nodes[targetIndex];
-                if (target.state !== 1) {
-                    if (target.state === 0) {
-                        dfs(target);
-                    }
-                    node.dagEdges.push(targetIndex);
-                    target.inCount += 1;
-                }
-            });
-            node.state = 2;
-        };
-        const markLevels = (node: NodeInfo) => {
-            node.dagEdges.forEach(targetIndex => {
-                const target = nodes[targetIndex];
-                target.level = Math.max(target.level, node.level + 1);
-                if (--target.inCount === 0) {
-                    markLevels(target);
-                }
-            });
-        };
-        nodes.forEach(node => {
-            if (node.state === 0) {
-                dfs(node);
-                node.level = 1;
-                markLevels(node);
+    override onCompileResult(compilerId: number, compiler: any, result: any) {
+        if (this.compilerInfo.compilerId !== compilerId) return;
+        this.functionSelector.clear(true);
+        this.functionSelector.clearOptions();
+        if (result.cfg) {
+            const cfg = result.cfg as CFGResult;
+            this.results = cfg;
+            let selectedFunction: string | null = this.state.selectedFunction;
+            const keys = Object.keys(cfg);
+            if (keys.length === 0) {
+                this.functionSelector.addOption({
+                    title: '<No functions available>',
+                    value: '<No functions available>',
+                });
             }
-        });
-        if (data.nodes) {
-            for (const node of nodes) {
-                data.nodes[node.index]['level'] = node.level;
+            for (const fn of keys) {
+                this.functionSelector.addOption({
+                    title: fn,
+                    value: fn,
+                });
             }
-        }
-
-        for (const edge of data.edges as vis.Edge[]) {
-            if (edge.from && edge.to && isEdgeValid(edge)) {
-                const nodeA = nodes[idToIdx[edge.from]];
-                const nodeB = nodes[idToIdx[edge.to]];
-                if (nodeA.level >= nodeB.level) {
-                    edge.physics = false;
-                } else {
-                    edge.physics = true;
-                    const diff = nodeB.level - nodeA.level;
-                    edge.length = diff * (200 - 5 * Math.min(5, diff));
+            if (keys.length > 0) {
+                if (selectedFunction === '' || !(selectedFunction !== null && selectedFunction in cfg)) {
+                    selectedFunction = keys[0];
                 }
+                this.functionSelector.setValue(selectedFunction, true);
+                this.state.selectedFunction = selectedFunction;
             } else {
-                edge.physics = false;
+                // this.state.selectedFunction won't change, next time the compilation results aren't errors or empty
+                // the selected function will still be the same
+                selectedFunction = null;
             }
+            this.selectFunction(selectedFunction);
+        } else {
+            // this case can be fallen into with a blank input file
+            this.selectFunction(null);
         }
     }
 
-    showCfgResults(data: vis.Data) {
-        this.assignLevels(data);
-        this.cfgVisualiser.setData(data);
-        /* FIXME: This does not work.
-         * It's here because I suspected that not having content in the constructor was
-         * breaking the move, but it does not seem like it
-         */
-        if (this.needsMove) {
-            this.cfgVisualiser.moveTo({
-                position: this.savedPos,
-                animation: false,
-                scale: this.savedScale,
-            });
-            this.needsMove = false;
+    async createBasicBlocks(fn: CfgDescriptor) {
+        for (const node of fn.nodes) {
+            const div = document.createElement('div');
+            div.classList.add('block');
+            div.innerHTML = await monaco.editor.colorize(node.label, 'asm', MonacoConfig.extendConfig({}));
+            if (node.id in this.bbMap) {
+                throw Error("Duplicate basic block node id's found while drawing cfg");
+            }
+            this.bbMap[node.id] = div;
+            this.blockContainer.appendChild(div);
+        }
+        for (const node of fn.nodes) {
+            const elem = $(this.bbMap[node.id]);
+            void this.bbMap[node.id].offsetHeight;
+            (node as AnnotatedNodeDescriptor).width = elem.outerWidth() as number;
+            (node as AnnotatedNodeDescriptor).height = elem.outerHeight() as number;
         }
     }
 
-    override onCompilerClose(compilerId: number) {
-        if (this.compilerInfo.compilerId === compilerId) {
-            // We can't immediately close as an outer loop somewhere in GoldenLayout is iterating over
-            // the hierarchy. We can't modify while it's being iterated over.
-            this.close();
-            _.defer(() => {
-                this.container.close();
-            }, this);
+    drawEdges() {
+        const width = this.layout.getWidth();
+        const height = this.layout.getHeight();
+        this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
+        // We want to assembly everything in a document fragment first, then add it to the dom
+        // If we add to the dom every iteration the performance is awful, presumably because of layout computation and
+        // rendering and whatnot.
+        const documentFragment = document.createDocumentFragment();
+        for (const block of this.layout.blocks) {
+            for (const edge of block.edges) {
+                // Sanity check
+                if (edge.path.length === 0) {
+                    throw Error('Mal-formed edge: Zero segments');
+                }
+                const points: [number, number][] = [];
+                // -1 offset is to create an overlap between the block's bottom border and start of the path, avoid any
+                // visual artifacts
+                points.push([edge.path[0].start.x, edge.path[0].start.y - 1]);
+                for (const segment of edge.path.slice(0, edge.path.length - 1)) {
+                    points.push([segment.end.x, segment.end.y]);
+                }
+                // Edge arrow is going to be a triangle
+                const triangleHeight = 7;
+                const triangleWidth = 7;
+                const endpoint = edge.path[edge.path.length - 1].end;
+                // +1 offset to create an overlap with the triangle
+                points.push([endpoint.x, endpoint.y - triangleHeight + 1]);
+                // Create the poly line
+                const line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
+                line.setAttribute('points', points.map(coord => coord.join(',')).join(' '));
+                line.setAttribute('fill', 'none');
+                line.setAttribute('stroke', ColorTable[edge.color]);
+                line.setAttribute('stroke-width', '2');
+                documentFragment.appendChild(line);
+                // Create teh triangle
+                const trianglePoints: [number, number][] = [];
+                trianglePoints.push([endpoint.x - triangleWidth / 2, endpoint.y - triangleHeight]);
+                trianglePoints.push([endpoint.x + triangleWidth / 2, endpoint.y - triangleHeight]);
+                trianglePoints.push([endpoint.x, endpoint.y]);
+                trianglePoints.push([endpoint.x - triangleWidth / 2, endpoint.y - triangleHeight]);
+                trianglePoints.push([endpoint.x + triangleWidth / 2, endpoint.y - triangleHeight]);
+                const triangle = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
+                triangle.setAttribute('points', trianglePoints.map(coord => coord.join(',')).join(' '));
+                triangle.setAttribute('fill', ColorTable[edge.color]);
+                documentFragment.appendChild(triangle);
+            }
         }
+        this.svg.appendChild(documentFragment);
     }
 
-    override close() {
-        this.eventHub.unsubscribe();
-        this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId);
-        this.cfgVisualiser.destroy();
+    applyLayout() {
+        const width = this.layout.getWidth();
+        const height = this.layout.getHeight();
+        this.graphDimensions.width = width;
+        this.graphDimensions.height = height;
+        this.graphDiv.style.height = height + 'px';
+        this.graphDiv.style.width = width + 'px';
+        this.svg.style.height = height + 'px';
+        this.svg.style.width = width + 'px';
+        this.blockContainer.style.height = height + 'px';
+        this.blockContainer.style.width = width + 'px';
+        for (const block of this.layout.blocks) {
+            const elem = this.bbMap[block.data.id];
+            elem.style.top = block.coordinates.y + 'px';
+            elem.style.left = block.coordinates.x + 'px';
+            elem.style.width = block.data.width + 'px';
+            elem.style.height = block.data.height + 'px';
+        }
     }
 
-    getEffectiveOptions() {
-        return this.toggles.get();
+    // display the cfg for the specified function if it exists
+    // this function does not change or use this.state.selectedFunction
+    async selectFunction(name: string | null) {
+        this.blockContainer.innerHTML = '';
+        this.svg.innerHTML = '';
+        if (!name || !(name in this.results)) {
+            return;
+        }
+        const fn = this.results[name];
+        this.bbMap = {};
+        await this.createBasicBlocks(fn);
+        this.layout = new GraphLayoutCore(fn as AnnotatedCfgDescriptor);
+        this.applyLayout();
+        this.drawEdges();
+        this.infoElement.innerHTML = `Layout time: ${Math.round(this.layout.layoutTime)}ms<br/>Basic blocks: ${
+            fn.nodes.length
+        }`;
     }
 
-    override getCurrentState(): CfgState {
-        return {
-            ...super.getCurrentState(),
-            selectedFn: this.currentFunc,
-            pos: this.cfgVisualiser.getViewPosition(),
-            scale: this.cfgVisualiser.getScale(),
-            options: this.getEffectiveOptions(),
-        };
+    override resize() {
+        _.defer(() => {
+            const topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable);
+            this.graphContainer.style.width = `${this.domRoot.width() as number}px`;
+            this.graphContainer.style.height = `${(this.domRoot.height() as number) - topBarHeight}px`;
+        });
     }
 
-    adaptStructure(names: string[]) {
-        return names.map(name => {
-            return {name};
-        });
+    override close(): void {
+        this.eventHub.unsubscribe();
+        this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId);
     }
 }
diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts
index 2389f9deaefcd47c4a03549a0431d322cfa9be11..cfca3cef3a260762556951c1c6427cffc82379d9 100644
--- a/static/panes/compiler.ts
+++ b/static/panes/compiler.ts
@@ -2158,7 +2158,6 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
         filters: CompilerOutputOptions & Record<string, boolean | undefined>,
         reqCompile: boolean
     ): void {
-
         if (this.id === id) {
             this.treeDumpEnabled = filters.treeDump !== false;
             this.rtlDumpEnabled = filters.rtlDump !== false;
diff --git a/static/styles/explorer.scss b/static/styles/explorer.scss
index d286c3ec935608057436b22ee49d1f5ab4207fb4..7d075e99d010221aac5efbaabf21dbb0008a6966 100644
--- a/static/styles/explorer.scss
+++ b/static/styles/explorer.scss
@@ -1,5 +1,4 @@
 @import '~@fortawesome/fontawesome-free/css/all.min.css';
-@import '~vis-network/styles/vis-network.css';
 
 /*
  * https://github.com/Microsoft/monaco-editor/issues/417
@@ -384,13 +383,44 @@ pre.content.wrap * {
     height: 100%;
 }
 
-.cfg-toolbar table {
-    width: 100%;
-}
-
-.graph-placeholder {
+.graph-container {
+    position: relative;
     width: 100%;
     height: 100%;
+    overflow: hidden;
+    .cfg-info {
+        position: absolute;
+        bottom: 5px;
+        left: 5px;
+        font-size: x-small;
+        font-style: italic;
+        z-index: 1;
+    }
+    .graph {
+        position: absolute;
+        top: 0;
+        left: 0;
+        transform-origin: top left;
+        svg {
+            position: absolute;
+            top: 0;
+            left: 0;
+        }
+        .block-container {
+            position: absolute;
+            top: 0;
+            left: 0;
+            .block {
+                position: absolute;
+                padding: 5px;
+                display: inline-block;
+                // TODO(jeremy-rifkin) settings.editorsFont
+                font-family: Consolas, 'Liberation Mono', Courier, monospace;
+                white-space: nowrap;
+                line-height: 100%;
+            }
+        }
+    }
 }
 
 .clear-cache {
diff --git a/static/styles/themes/dark-theme.scss b/static/styles/themes/dark-theme.scss
index 5bfc218ec3d033144afcae231cf43e0a3d9853e1..a6c4a51ce2437eb74c9ecc6d94f8a7245edbcb28 100644
--- a/static/styles/themes/dark-theme.scss
+++ b/static/styles/themes/dark-theme.scss
@@ -238,8 +238,15 @@ textarea.form-control {
     background-color: darken(#474747, 10%);
 }
 
-.graph-placeholder {
-    background-color: #1e1e1e !important;
+.graph-container {
+    .cfg-info {
+        color: #aaa;
+    }
+    .graph .block-container .block {
+        background: black;
+        border: 1px solid white;
+        color: white;
+    }
 }
 
 .input-group-text {
diff --git a/static/styles/themes/default-theme.scss b/static/styles/themes/default-theme.scss
index 70ab1a445fff38536956a9a0465f01ceb6757444..44e3ad3abe157f41b6dc224ffa9dce505fb9198a 100644
--- a/static/styles/themes/default-theme.scss
+++ b/static/styles/themes/default-theme.scss
@@ -204,8 +204,16 @@ a.navbar-brand img.logo.normal {
     background-color: #f5f5f5;
 }
 
-.graph-placeholder {
-    background-color: #ffffff;
+.graph-container {
+    background: rgb(245, 245, 245);
+    .cfg-info {
+        color: rgb(56, 56, 56);
+    }
+    .graph .block-container .block {
+        background: white;
+        border: 1px solid rgb(55, 55, 55);
+        color: rgb(0, 0, 0);
+    }
 }
 
 .text-count {
diff --git a/test/cfg-cases/cfg-gcc.if-else.json b/test/cfg-cases/cfg-gcc.if-else.json
index f8b61a129a200c79d99f9b69a42e933a5682b1fc..662bc3066b342be1c66a3ec3c3093de0e439e968 100644
--- a/test/cfg-cases/cfg-gcc.if-else.json
+++ b/test/cfg-cases/cfg-gcc.if-else.json
@@ -1085,7 +1085,7 @@
       "nodes": [
         {
           "id": "_GLOBAL__sub_I_main:",
-          "label": "_GLOBAL__sub_I_main:\n  sub rsp, 8\n  mov edi, OFFSET FLAT:std::__ioinit\n  call std::ios_base::Init::Init()\n  mov edx, OFFSET FLAT:__dso_handle\n  mov esi, OFFSET FLAT:std::__ioinit\n  mov edi, OFFSET FLAT:std::ios_base::Init::~Init(\n  add rsp, 8\n  jmp __cxa_atexit",
+          "label": "_GLOBAL__sub_I_main:\n  sub rsp, 8\n  mov edi, OFFSET FLAT:std::__ioinit\n  call std::ios_base::Init::Init()\n  mov edx, OFFSET FLAT:__dso_handle\n  mov esi, OFFSET FLAT:std::__ioinit\n  mov edi, OFFSET FLAT:std::ios_base::Init::~Init()\n  add rsp, 8\n  jmp __cxa_atexit",
           "color": "#99ccff",
           "shape": "box"
         }
diff --git a/test/cfg-cases/cfg-gcc.loop.json b/test/cfg-cases/cfg-gcc.loop.json
index 25e06b7bbb02a26502321ffbc038896ca13cae85..ca3b1dc71d72c744fdb5b97c56aa0790d3887b26 100644
--- a/test/cfg-cases/cfg-gcc.loop.json
+++ b/test/cfg-cases/cfg-gcc.loop.json
@@ -1169,7 +1169,7 @@
       "nodes": [
         {
           "id": "_GLOBAL__sub_I_main:",
-          "label": "_GLOBAL__sub_I_main:\n  sub rsp, 8\n  mov edi, OFFSET FLAT:std::__ioinit\n  call std::ios_base::Init::Init()\n  mov edx, OFFSET FLAT:__dso_handle\n  mov esi, OFFSET FLAT:std::__ioinit\n  mov edi, OFFSET FLAT:std::ios_base::Init::~Init(\n  add rsp, 8\n  jmp __cxa_atexit",
+          "label": "_GLOBAL__sub_I_main:\n  sub rsp, 8\n  mov edi, OFFSET FLAT:std::__ioinit\n  call std::ios_base::Init::Init()\n  mov edx, OFFSET FLAT:__dso_handle\n  mov esi, OFFSET FLAT:std::__ioinit\n  mov edi, OFFSET FLAT:std::ios_base::Init::~Init()\n  add rsp, 8\n  jmp __cxa_atexit",
           "color": "#99ccff",
           "shape": "box"
         }
diff --git a/test/cfg-cases/cfg-gcc.single-block.json b/test/cfg-cases/cfg-gcc.single-block.json
index 41d33f20736765f47db4b00996e92dee1a315c43..de7c2c76ba51269846430a1e106fa8e1b308f5fd 100644
--- a/test/cfg-cases/cfg-gcc.single-block.json
+++ b/test/cfg-cases/cfg-gcc.single-block.json
@@ -99,7 +99,7 @@
       "nodes": [
         {
           "id": "_GLOBAL__sub_I_main:",
-          "label": "_GLOBAL__sub_I_main:\n  sub rsp, 8\n  mov edi, OFFSET FLAT:std::__ioinit\n  call std::ios_base::Init::Init()\n  mov edx, OFFSET FLAT:__dso_handle\n  mov esi, OFFSET FLAT:std::__ioinit\n  mov edi, OFFSET FLAT:std::ios_base::Init::~Init(\n  add rsp, 8\n  jmp __cxa_atexit",
+          "label": "_GLOBAL__sub_I_main:\n  sub rsp, 8\n  mov edi, OFFSET FLAT:std::__ioinit\n  call std::ios_base::Init::Init()\n  mov edx, OFFSET FLAT:__dso_handle\n  mov esi, OFFSET FLAT:std::__ioinit\n  mov edi, OFFSET FLAT:std::ios_base::Init::~Init()\n  add rsp, 8\n  jmp __cxa_atexit",
           "color": "#99ccff",
           "shape": "box"
         }
diff --git a/types/compilation/cfg.interfaces.ts b/types/compilation/cfg.interfaces.ts
new file mode 100644
index 0000000000000000000000000000000000000000..54e034e370bd49304f214b2a4628c9e3d232741b
--- /dev/null
+++ b/types/compilation/cfg.interfaces.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2022, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+// TODO(jeremy-rifkin): re-visit all the types here once the back-end is more typescripted
+
+export type EdgeDescriptor = {
+    from: string;
+    to: string;
+    arrows: string; // <- useless
+    color: string;
+};
+
+export type NodeDescriptor = {
+    color: string; // <- useless
+    id: string; // typically label for the bb
+    label: string; // really the source
+    shape: string; // <- useless
+};
+
+export type AnnotatedNodeDescriptor = NodeDescriptor & {
+    width: number; // in pixels
+    height: number; // in pixels
+};
+
+type CfgDescriptor_<ND> = {
+    edges: EdgeDescriptor[];
+    nodes: ND[];
+};
+
+export type CfgDescriptor = CfgDescriptor_<NodeDescriptor>;
+export type AnnotatedCfgDescriptor = CfgDescriptor_<AnnotatedNodeDescriptor>;
+
+// function name -> cfg data
+export type CFGResult = Record<string, CfgDescriptor>;
+export type AnnotatedCFGResult = Record<string, AnnotatedCfgDescriptor>;
diff --git a/views/templates/panes/cfg.pug b/views/templates/panes/cfg.pug
index 6ff3066cb346d424e236d86a64a6406c61c1c5ad..1a6446282da8b7d814ce4bb4152552596f2f9a7e 100644
--- a/views/templates/panes/cfg.pug
+++ b/views/templates/panes/cfg.pug
@@ -1,14 +1,9 @@
 #cfg
   .top-bar.btn-toolbar.bg-light.cfg-toolbar(role="toolbar")
     .btn-group.btn-group-sm(role="group")
-      select.function-picker
-    .btn-group.btn-group-sm.options(role="group")
-      .button-checkbox
-        button.btn.btn-sm.btn-light.toggle-navigation(type="button" title="Toggle navigation buttons" aria-pressed="false" data-bind="navigation")
-          span Nav
-        input.d-none(type="checkbox")
-      .button-checkbox
-        button.btn.btn-sm.btn-light.toggle-physics(type="button" title="Toggle physics to nodes" aria-pressed="false" data-bind="physics")
-          span Physics
-        input.d-none(type="checkbox")
-  .graph-placeholder
+      select.function-selector
+  .graph-container
+    span.cfg-info
+    .graph
+      svg
+      .block-container
diff --git a/views/templates/panes/compiler.pug b/views/templates/panes/compiler.pug
index 1612fdf5c99268268204a91138aec6386f759c95..45ec083a6164c58e4bceceeabfc7f297fb328e13 100644
--- a/views/templates/panes/compiler.pug
+++ b/views/templates/panes/compiler.pug
@@ -61,7 +61,7 @@ mixin newPaneButton(classId, text, title, icon)
         +newPaneButton("view-gccdump", "GCC Tree/RTL", "Show GCC Tree/RTL dump", "fas fa-tree")
         +newPaneButton("view-gnatdebugtree", "GNAT Debug Tree", "Show GNAT debug tree", "fas fa-tree")
         +newPaneButton("view-gnatdebug", "GNAT Debug Expanded Code", "Show GNAT debug expanded code", "fas fa-tree")
-        +newPaneButton("view-cfg", "Graph", "Show graph output", "fas fa-exchange-alt")
+        +newPaneButton("view-cfg", "Control Flow Graph", "Show assembly control flow graphs", "fas fa-exchange-alt")
     .btn-group.btn-group-sm(role="group")
       button.btn.btn-sm.btn-light.dropdown-toggle.add-tool(type="button" title="Add tool" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Add tooling to this editor and compiler")
         span.fas.fa-screwdriver